diff --git a/.dockerignore b/.dockerignore index 3b81744d2d..9ba3d848a3 100644 --- a/.dockerignore +++ b/.dockerignore @@ -25,4 +25,6 @@ npm-debug.log.* # Webpack files webpack.records.json -package-lock.json + +# Yarn no longer used +yarn.lock diff --git a/.eslintrc.json b/.eslintrc.json index cb5775ef1f..5fb4c12171 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -11,7 +11,13 @@ "eslint-plugin-jsonc", "eslint-plugin-rxjs", "eslint-plugin-simple-import-sort", - "eslint-plugin-import-newlines" + "eslint-plugin-import-newlines", + "eslint-plugin-jsonc", + "dspace-angular-ts", + "dspace-angular-html" + ], + "ignorePatterns": [ + "lint/test/fixture" ], "overrides": [ { @@ -21,7 +27,8 @@ "parserOptions": { "project": [ "./tsconfig.json", - "./cypress/tsconfig.json" + "./cypress/tsconfig.json", + "./lint/tsconfig.json" ], "createDefaultProgram": true }, @@ -38,7 +45,10 @@ "error", 2, { - "SwitchCase": 1 + "SwitchCase": 1, + "ignoredNodes": [ + "ClassBody.body > PropertyDefinition[decorators.length > 0] > .key" + ] } ], "max-classes-per-file": [ @@ -152,10 +162,10 @@ } ], "@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", @@ -213,6 +223,15 @@ "@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", @@ -239,7 +258,12 @@ "method" ], - "rxjs/no-nested-subscribe": "off" // todo: go over _all_ cases + "rxjs/no-nested-subscribe": "off", // todo: go over _all_ cases + + // Custom DSpace Angular rules + "dspace-angular-ts/themed-component-classes": "error", + "dspace-angular-ts/themed-component-selectors": "error", + "dspace-angular-ts/themed-component-usages": "error" } }, { @@ -254,7 +278,10 @@ "createDefaultProgram": true }, "rules": { - "prefer-const": "off" + "prefer-const": "off", + + // Custom DSpace Angular rules + "dspace-angular-ts/themed-component-usages": "error" } }, { @@ -263,7 +290,11 @@ ], "extends": [ "plugin:@angular-eslint/template/recommended" - ] + ], + "rules": { + // Custom DSpace Angular rules + "dspace-angular-html/themed-component-usages": "error" + } }, { "files": [ diff --git a/.gitattributes b/.gitattributes index 406640bfcc..b5ad93b1bc 100644 --- a/.gitattributes +++ b/.gitattributes @@ -13,4 +13,7 @@ *.css eol=lf *.scss eol=lf *.html eol=lf -*.svg eol=lf \ No newline at end of file +*.svg eol=lf + +# Generated documentation should have LF line endings to reduce git noise +docs/lint/**/*.md eol=lf \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 8e4ed0811d..0cce2848a7 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -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. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 34cc2c9e4f..9eaa4d9f3f 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -7,14 +7,14 @@ assignees: '' --- -**Is your feature request related to a problem? Please describe.** -A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] +## 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 [...] -**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 context** -Add any other context or screenshots about the feature request here. +## Additional information +Add any other information, related tickets or screenshots about the feature request here. diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000000..b53e501d29 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,298 @@ +#------------------- +# 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: "/" + schedule: + interval: "weekly" + # 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*" + update-types: + - "minor" + - "patch" + # Group together all security updates for Angular. Only accept minor/patch types. + angular-security: + applies-to: security-updates + patterns: + - "@angular*" + 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 security updates for NgRx. Only accept minor/patch types. + ngrx-security: + applies-to: security-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 all security updates for eslint. + eslint-security: + applies-to: security-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 testing related security updates + testing-security: + applies-to: security-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 postcss related security updates + postcss-security: + applies-to: security-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 sass related security updates + sass-security: + applies-to: security-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" + # Group together any webpack related seurity updates + webpack-security: + applies-to: security-updates + patterns: + - "webpack*" + update-types: + - "minor" + - "patch" + ignore: + # 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: "weekly" + # 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*" + 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: + # 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: "weekly" + # 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*" + 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" + # Ignore all major version updates for all dependencies. We'll only automate minor/patch updates. + - dependency-name: "*" + update-types: ["version-update:semver-major"] diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index e50105b879..14bd09f5e3 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -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,13 +16,18 @@ 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 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!_ +_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!_ -- [ ] 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). +- [ ] 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/DSDOC8x/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. - [ ] 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). diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 52f20470a3..ab03cb3885 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,7 +7,8 @@ name: Build on: [push, pull_request] permissions: - contents: read # to fetch code (actions/checkout) + contents: read # to fetch code (actions/checkout) + packages: read # to fetch private images from GitHub Container Registry (GHCR) jobs: tests: @@ -33,12 +34,15 @@ jobs: #CHROME_VERSION: "90.0.4430.212-1" # Bump Node heap size (OOM in CI after upgrading to Angular 15) NODE_OPTIONS: '--max-old-space-size=4096' - # Project name to use when running docker-compose prior to e2e tests + # Project name to use when running "docker compose" prior to e2e tests COMPOSE_PROJECT_NAME: 'ci' + # Docker Registry to use for Docker compose scripts below. + # We use GitHub's Container Registry to avoid aggressive rate limits at DockerHub. + DOCKER_REGISTRY: ghcr.io strategy: # Create a matrix of Node versions to test against (in parallel) matrix: - node-version: [16.x, 18.x] + node-version: [18.x, 20.x] # Do NOT exit immediately if one matrix job fails fail-fast: false # These are the actual CI steps to perform per job @@ -69,51 +73,65 @@ jobs: fi google-chrome --version - # 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@v3 + # 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 + uses: actions/cache@v4 with: - # 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- + # 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- - - name: Install Yarn dependencies - run: yarn install --frozen-lockfile + - 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: Run lint - run: yarn run lint --quiet + run: npm run lint:nobuild -- --quiet - name: Check for circular dependencies - run: yarn run check-circ-deps + run: npm run check-circ-deps - name: Run build - run: yarn run build:prod + run: npm run build:prod - name: Run specs (unit tests) - run: yarn run test:headless + run: npm 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) # NOTE: Angular CLI only supports code coverage for specs. See https://github.com/angular/angular-cli/issues/6286 - name: Upload code coverage report to Artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: matrix.node-version == '18.x' with: - name: dspace-angular coverage report + name: coverage-report-${{ matrix.node-version }} path: 'coverage/dspace-angular/lcov.info' retention-days: 14 - # Using docker-compose start backend using CI configuration + # Login to our Docker registry, so that we can access private Docker images using "docker compose" below. + - name: Login to ${{ env.DOCKER_REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.DOCKER_REGISTRY }} + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + # Using "docker compose" start backend using CI configuration # and load assetstore from a cached copy - name: Start DSpace REST Backend via Docker (for e2e tests) run: | - docker-compose -f ./docker/docker-compose-ci.yml up -d - docker-compose -f ./docker/cli.yml -f ./docker/cli.assetstore.yml run --rm dspace-cli + docker compose -f ./docker/docker-compose-ci.yml up -d + docker compose -f ./docker/cli.yml -f ./docker/cli.assetstore.yml run --rm dspace-cli docker container ls # Run integration tests via Cypress.io @@ -125,7 +143,7 @@ jobs: # Run tests in Chrome, headless mode (default) browser: chrome # Start app before running tests (will be stopped automatically after tests finish) - start: yarn run serve:ssr + start: npm 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 @@ -135,19 +153,19 @@ jobs: # Cypress always creates a video of all e2e tests (whether they succeeded or failed) # Save those in an Artifact - name: Upload e2e test videos to Artifacts - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: always() with: - name: e2e-test-videos + name: e2e-test-videos-${{ matrix.node-version }} path: cypress/videos # If e2e tests fail, Cypress creates a screenshot of what happened # Save those in an Artifact - name: Upload e2e test failure screenshots to Artifacts - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: failure() with: - name: e2e-test-screenshots + name: e2e-test-screenshots-${{ matrix.node-version }} path: cypress/screenshots - name: Stop app (in case it stays up after e2e tests) @@ -161,7 +179,7 @@ jobs: # Start up the app with SSR enabled (run in background) - name: Start app in SSR (server-side rendering) mode run: | - nohup yarn run serve:ssr & + nohup npm 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 '.' @@ -182,7 +200,7 @@ jobs: run: kill -9 $(lsof -t -i:4000) - name: Shutdown Docker containers - run: docker-compose -f ./docker/docker-compose-ci.yml down + run: docker compose -f ./docker/docker-compose-ci.yml down # Codecov upload is a separate job in order to allow us to restart this separate from the entire build/test # job above. This is necessary because Codecov uploads seem to randomly fail at times. @@ -197,7 +215,7 @@ jobs: # Download artifacts from previous 'tests' job - name: Download coverage artifacts - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 # Now attempt upload to Codecov using its action. # NOTE: We use a retry action to retry the Codecov upload if it fails the first time. @@ -207,11 +225,12 @@ jobs: - name: Upload coverage to Codecov.io uses: Wandalen/wretry.action@v1.3.0 with: - action: codecov/codecov-action@v3 + action: codecov/codecov-action@v4 # Ensure codecov-action throws an error when it fails to upload # This allows us to auto-restart the action if an error is thrown with: | fail_ci_if_error: true + token: ${{ secrets.CODECOV_TOKEN }} # Try re-running action 5 times max attempt_limit: 5 # Run again in 30 seconds diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 85a7216113..bae8c01300 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -16,7 +16,8 @@ on: pull_request: permissions: - contents: read # to fetch code (actions/checkout) + contents: read # to fetch code (actions/checkout) + packages: write # to write images to GitHub Container Registry (GHCR) jobs: ############################################################# @@ -28,7 +29,7 @@ jobs: # 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 with: - build_id: dspace-angular + build_id: dspace-angular-dev image_name: dspace/dspace-angular dockerfile_path: ./Dockerfile secrets: diff --git a/.github/workflows/issue_opened.yml b/.github/workflows/issue_opened.yml index b4436dca3a..0a35a6a950 100644 --- a/.github/workflows/issue_opened.yml +++ b/.github/workflows/issue_opened.yml @@ -16,7 +16,7 @@ jobs: # Only add to project board if issue is flagged as "needs triage" or has no labels # NOTE: By default we flag new issues as "needs triage" in our issue template if: (contains(github.event.issue.labels.*.name, 'needs triage') || join(github.event.issue.labels.*.name) == '') - uses: actions/add-to-project@v0.5.0 + uses: actions/add-to-project@v1.0.0 # Note, the authentication token below is an ORG level Secret. # It must be created/recreated manually via a personal access token with admin:org, project, public_repo permissions # See: https://docs.github.com/en/actions/configuring-and-managing-workflows/authenticating-with-the-github_token#permissions-for-the-github_token diff --git a/.github/workflows/pull_request_opened.yml b/.github/workflows/pull_request_opened.yml index f16e81c9fd..bbac52af24 100644 --- a/.github/workflows/pull_request_opened.yml +++ b/.github/workflows/pull_request_opened.yml @@ -21,4 +21,4 @@ jobs: # Assign the PR to whomever created it. This is useful for visualizing assignments on project boards # See https://github.com/toshimaru/auto-author-assign - name: Assign PR to creator - uses: toshimaru/auto-author-assign@v2.0.1 + uses: toshimaru/auto-author-assign@v2.1.0 diff --git a/.gitignore b/.gitignore index 7d065aca06..7af424c50f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /.angular/cache +/.nx /__build__ /__server_build__ /node_modules @@ -27,12 +28,12 @@ webpack.records.json morgan.log +# Yarn no longer used +yarn.lock yarn-error.log *.css -package-lock.json - .java-version .env diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4e732302f4..6525750e4e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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/DSDOC7x/Release+Notes). +Contributors to each release are recognized in our [Release Notes](https://wiki.lyrasis.org/display/DSDOC8x/Release+Notes). Code Contribution Checklist - [ ] PRs _should_ be smaller in size (ideally less than 1,000 lines of code, not including comments & tests) @@ -18,6 +18,9 @@ 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/DSDOC8x/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). @@ -26,7 +29,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/DSDOC7x +DSpace Documentation is a collaborative effort in a shared Wiki. The latest documentation is at https://wiki.lyrasis.org/display/DSDOC 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. @@ -34,7 +37,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 @@ -42,5 +45,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 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. \ No newline at end of file +* [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. diff --git a/Dockerfile b/Dockerfile index 8fac7495e1..a2a7841396 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ # This image will be published as dspace/dspace-angular # See https://github.com/DSpace/dspace-angular/tree/main/docker for usage details -FROM node:18-alpine +FROM docker.io/node:18-alpine # Ensure Python and other build tools are available # These are needed to install some node modules, especially on linux/arm64 @@ -11,9 +11,7 @@ WORKDIR /app ADD . /app/ EXPOSE 4000 -# We run yarn install with an increased network timeout (5min) to avoid "ESOCKETTIMEDOUT" errors from hub.docker.com -# See, for example https://github.com/yarnpkg/yarn/issues/5540 -RUN yarn install --network-timeout 300000 +RUN npm install # 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. @@ -24,5 +22,5 @@ ENV NODE_OPTIONS="--max_old_space_size=4096" # Listen / accept connections from all IP addresses. # 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 yarn serve --host 0.0.0.0 +ENV NODE_ENV=development +CMD npm run serve -- --host 0.0.0.0 diff --git a/Dockerfile.dist b/Dockerfile.dist index e4b467ae26..9bb7b1519b 100644 --- a/Dockerfile.dist +++ b/Dockerfile.dist @@ -4,18 +4,18 @@ # Test build: # docker build -f Dockerfile.dist -t dspace/dspace-angular:latest-dist . -FROM node:18-alpine as build +FROM docker.io/node:18-alpine AS build # Ensure Python and other build tools are available # These are needed to install some node modules, especially on linux/arm64 RUN apk add --update python3 make g++ && rm -rf /var/cache/apk/* WORKDIR /app -COPY package.json yarn.lock ./ -RUN yarn install --network-timeout 300000 +COPY package.json package-lock.json ./ +RUN npm install ADD . /app/ -RUN yarn build:prod +RUN npm run build:prod FROM node:18-alpine RUN npm install --global pm2 @@ -26,6 +26,6 @@ COPY --chown=node:node docker/dspace-ui.json /app/dspace-ui.json WORKDIR /app USER node -ENV NODE_ENV production +ENV NODE_ENV=production EXPOSE 4000 CMD pm2-runtime start dspace-ui.json --json diff --git a/README.md b/README.md index ebc24f8b91..c5ad7fc7ed 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ https://wiki.lyrasis.org/display/DSDOC7x/Installing+DSpace Quick start ----------- -**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`** +**Ensure you're running [Node](https://nodejs.org) `v18.x` or `v20.x`, [npm](https://www.npmjs.com/) >= `v10.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 -yarn install +npm install # start the server -yarn start +npm 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 yarn)](#managing-dependencies-via-yarn) +- [Managing Dependencies (via npm)](#managing-dependencies-via-npm) - [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) and [yarn](https://yarnpkg.com) -- Ensure you're running node `v16.x` or `v18.x` and yarn == `v1.x` +- [Node.js](https://nodejs.org) +- Ensure you're running node `v18.x` or `v20.x` If you have [`nvm`](https://github.com/creationix/nvm#install-script) or [`nvm-windows`](https://github.com/coreybutler/nvm-windows) installed, which is highly recommended, you can run `nvm install --lts && nvm use` to install and start using the latest Node LTS. Installing ---------- -- `yarn install` to install the local dependencies +- `npm 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 `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`. +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`. ### 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 -yarn start +npm 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 -yarn run build:prod +npm 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 -yarn run serve:ssr +npm run serve:ssr ``` ### Running the application with Docker @@ -238,14 +238,14 @@ Cleaning -------- ```bash -# clean everything, including node_modules. You'll need to run yarn install again afterwards. -yarn run clean +# clean everything, including node_modules. You'll need to run npm install again afterwards. +npm run clean # clean files generated by the production build (.ngfactory files, css files, etc) -yarn run clean:prod +npm run clean:prod # cleans the distribution directory -yarn run clean:dist +npm 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. `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) +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) 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: `yarn 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: `npm 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: `yarn test` +and run: `npm 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. @@ -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:`yarn run docs` to produce the documentation that will be available in the 'doc' folder. +Run:`npm 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 `yarn 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 `npm run start` the `prestart` script will run first, then the `start` script will trigger. Recommended Editors/IDEs ------------------------ @@ -456,6 +456,7 @@ 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 * @@ -466,30 +467,29 @@ 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 -└── yarn.lock * Yarn lockfile (https://yarnpkg.com/en/docs/yarn-lock) +└── typedoc.json * TYPEDOC configuration ``` -Managing Dependencies (via yarn) +Managing Dependencies (via npm) ------------- -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. +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. -* `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. +* `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. -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.* +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.* ### Adding Typings for libraries -If the library does not include typings, you can install them using yarn: +If the library does not include typings, you can install them using npm: ```bash -yarn add d3 -yarn add @types/d3 --dev +npm install d3 +npm install @types/d3 --save-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 intrest of build speed. -- node-pre-gyp ERR in yarn 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 interest of build speed. +- node-pre-gyp ERR in npm 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 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 +- 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 - and `git commit` to conclude the merge Getting Help diff --git a/angular.json b/angular.json index 5e597d4d30..02fd69b1e1 100644 --- a/angular.json +++ b/angular.json @@ -30,7 +30,6 @@ "lodash", "jwt-decode", "uuid", - "webfontloader", "zone.js" ], "outputPath": "dist/browser", @@ -109,22 +108,22 @@ "serve": { "builder": "@angular-builders/custom-webpack:dev-server", "options": { - "browserTarget": "dspace-angular:build", + "buildTarget": "dspace-angular:build", "port": 4000 }, "configurations": { "development": { - "browserTarget": "dspace-angular:build:development" + "buildTarget": "dspace-angular:build:development" }, "production": { - "browserTarget": "dspace-angular:build:production" + "buildTarget": "dspace-angular:build:production" } } }, "extract-i18n": { "builder": "@angular-devkit/build-angular:extract-i18n", "options": { - "browserTarget": "dspace-angular:build" + "buildTarget": "dspace-angular:build" } }, "test": { @@ -217,23 +216,23 @@ } }, "serve-ssr": { - "builder": "@nguniversal/builders:ssr-dev-server", + "builder": "@angular-devkit/build-angular:ssr-dev-server", "options": { - "browserTarget": "dspace-angular:build", + "buildTarget": "dspace-angular:build", "serverTarget": "dspace-angular:server", "port": 4000 }, "configurations": { "production": { - "browserTarget": "dspace-angular:build:production", + "buildTarget": "dspace-angular:build:production", "serverTarget": "dspace-angular:server:production" } } }, "prerender": { - "builder": "@nguniversal/builders:prerender", + "builder": "@angular-devkit/build-angular:prerender", "options": { - "browserTarget": "dspace-angular:build:production", + "buildTarget": "dspace-angular:build:production", "serverTarget": "dspace-angular:server:production", "routes": [ "/" @@ -266,6 +265,8 @@ "options": { "lintFilePatterns": [ "src/**/*.ts", + "cypress/**/*.ts", + "lint/**/*.ts", "src/**/*.html", "src/**/*.json5" ] diff --git a/config/config.example.yml b/config/config.example.yml index 36d6a009d3..099bea2614 100644 --- a/config/config.example.yml +++ b/config/config.example.yml @@ -1,7 +1,7 @@ # NOTE: will log all redux actions and transfers in console debug: false -# Angular Universal server settings +# Angular User Inteface settings # NOTE: these settings define where Node.js will start your UI application. Therefore, these # "ui" settings usually specify a localhost port/URL which is later proxied to a public URL (using Apache or similar) ui: @@ -17,6 +17,23 @@ ui: # Trust X-FORWARDED-* headers from proxies (default = true) useProxies: true +# Angular Server Side Rendering (SSR) settings +ssr: + # 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. + inlineCriticalCss: false + # Path prefixes to enable SSR for. By default these are limited to paths of primary DSpace objects. + paths: [ '/home', '/items/', '/entities/', '/collections/', '/communities/', '/bitstream/', '/bitstreams/', '/handle/' ] + # Whether to enable rendering of Search component on SSR. + # If set to true the component will be included in the HTML returned from the server side rendering. + # If set to false the component will not be included in the HTML returned from the server side rendering. + enableSearchComponent: false, + # Whether to enable rendering of Browse component on SSR. + # If set to true the component will be included in the HTML returned from the server side rendering. + # If set to false the component will not be included in the HTML returned from the server side rendering. + enableBrowseComponent: 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. @@ -52,7 +69,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 guarranteed. It is possible some bots will bypass this cache.) + # (Keep in mind, bot detection cannot be guaranteed. 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. @@ -195,6 +212,12 @@ languages: - code: en label: English active: true + - code: ar + label: العربية + active: true + - code: bn + label: বাংলা + active: true - code: ca label: Català active: true @@ -204,24 +227,36 @@ languages: - code: de label: Deutsch active: true + - code: el + label: Ελληνικά + active: true - code: es label: Español active: true + - code: fi + label: Suomi + active: true - code: fr label: Français active: true - code: gd label: Gàidhlig active: true - - code: it - label: Italiano - active: true - - code: lv - label: Latviešu + - code: hi + label: हिंदी active: true - code: hu label: Magyar active: true + - code: it + label: Italiano + active: true + - code: kk + label: Қазақ + active: true + - code: lv + label: Latviešu + active: true - code: nl label: Nederlands active: true @@ -237,8 +272,8 @@ languages: - code: sr-lat label: Srpski (lat) active: true - - code: fi - label: Suomi + - code: sr-cyr + label: Српски active: true - code: sv label: Svenska @@ -246,27 +281,12 @@ languages: - code: tr label: Türkçe active: true - - code: vi - label: Tiếng Việt - active: true - - code: kk - label: Қазақ - active: true - - code: bn - label: বাংলা - active: true - - code: hi - label: हिंदी - active: true - - code: el - label: Ελληνικά - active: true - - code: sr-cyr - label: Српски - active: true - code: uk label: Yкраї́нська active: true + - code: vi + label: Tiếng Việt + active: true # Browse-By Pages @@ -400,10 +420,11 @@ 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 should exist or not. +# And whether the privacy statement/COAR notify support page should exist or not. info: enableEndUserAgreement: true enablePrivacyStatement: true + enableCOARNotifySupport: 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. @@ -437,6 +458,12 @@ search: 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 @@ -492,6 +519,16 @@ notifyMetrics: 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 +# result of an external event when user focus may be elsewhere. +# +# The DSpace live region is a component present at the bottom of all pages that is invisible by default, but is useful +# for screen readers. Any message pushed to the live region will be announced by the screen reader. These messages +# usually contain information about changes on the page that might not be in focus. +liveRegion: + # The duration after which messages disappear from the live region in milliseconds + messageTimeOutDurationMs: 30000 + # The visibility of the live region. Setting this to true is only useful for debugging purposes. + isVisible: false diff --git a/cypress.config.ts b/cypress.config.ts index 458b035a48..36d8120342 100644 --- a/cypress.config.ts +++ b/cypress.config.ts @@ -1,6 +1,7 @@ import { defineConfig } from 'cypress'; export default defineConfig({ + video: true, videosFolder: 'cypress/videos', screenshotsFolder: 'cypress/screenshots', fixturesFolder: 'cypress/fixtures', @@ -18,6 +19,7 @@ export default defineConfig({ // Admin account used for administrative tests DSPACE_TEST_ADMIN_USER: 'dspacedemo+admin@gmail.com', + DSPACE_TEST_ADMIN_USER_UUID: '335647b6-8a52-4ecb-a8c1-7ebabb199bda', DSPACE_TEST_ADMIN_PASSWORD: 'dspace', // Community/collection/publication used for view/edit tests DSPACE_TEST_COMMUNITY: '0958c910-2037-42a9-81c7-dca80e3892b4', @@ -33,6 +35,8 @@ export default defineConfig({ // Account used to test basic submission process DSPACE_TEST_SUBMIT_USER: 'dspacedemo+submit@gmail.com', DSPACE_TEST_SUBMIT_USER_PASSWORD: 'dspace', + // Administrator users group + DSPACE_ADMINISTRATOR_GROUP: 'e59f5659-bff9-451e-b28f-439e7bd467e4' }, e2e: { // Setup our plugins for e2e tests diff --git a/cypress/e2e/admin-add-new-modals.cy.ts b/cypress/e2e/admin-add-new-modals.cy.ts new file mode 100644 index 0000000000..e2ade53488 --- /dev/null +++ b/cypress/e2e/admin-add-new-modals.cy.ts @@ -0,0 +1,54 @@ +import { testA11y } from 'cypress/support/utils'; + +describe('Admin Add New Modals', () => { + 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('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(); + + // 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('a[data-test="menu.section.new_community"]').click(); + + // Analyze for accessibility + testA11y('ds-create-community-parent-selector'); + }); + + 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(); + + // 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('a[data-test="menu.section.new_collection"]').click(); + + // Analyze for accessibility + testA11y('ds-create-collection-parent-selector'); + }); + + 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(); + + // 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('a[data-test="menu.section.new_item"]').click(); + + // Analyze for accessibility + testA11y('ds-create-item-parent-selector'); + }); +}); diff --git a/cypress/e2e/admin-curation-tasks.cy.ts b/cypress/e2e/admin-curation-tasks.cy.ts new file mode 100644 index 0000000000..e66f0ccaad --- /dev/null +++ b/cypress/e2e/admin-curation-tasks.cy.ts @@ -0,0 +1,16 @@ +import { testA11y } from 'cypress/support/utils'; + +describe('Admin Curation Tasks', () => { + beforeEach(() => { + // Must login as an Admin to see the page + cy.visit('/admin/curation-tasks'); + 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-curation-task').should('be.visible'); + // Analyze for accessibility issues + testA11y('ds-admin-curation-task'); + }); +}); diff --git a/cypress/e2e/admin-edit-modals.cy.ts b/cypress/e2e/admin-edit-modals.cy.ts new file mode 100644 index 0000000000..d4e2438061 --- /dev/null +++ b/cypress/e2e/admin-edit-modals.cy.ts @@ -0,0 +1,54 @@ +import { testA11y } from 'cypress/support/utils'; + +describe('Admin Edit Modals', () => { + 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('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(); + + // 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('a[data-test="menu.section.edit_community"]').click(); + + // Analyze for accessibility + testA11y('ds-edit-community-selector'); + }); + + 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(); + + // 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('a[data-test="menu.section.edit_collection"]').click(); + + // Analyze for accessibility + testA11y('ds-edit-collection-selector'); + }); + + 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(); + + // 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('a[data-test="menu.section.edit_item"]').click(); + + // Analyze for accessibility + testA11y('ds-edit-item-selector'); + }); +}); diff --git a/cypress/e2e/admin-export-modals.cy.ts b/cypress/e2e/admin-export-modals.cy.ts new file mode 100644 index 0000000000..873d16535f --- /dev/null +++ b/cypress/e2e/admin-export-modals.cy.ts @@ -0,0 +1,39 @@ +import { testA11y } from 'cypress/support/utils'; + +describe('Admin Export Modals', () => { + 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('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(); + + // 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('a[data-test="menu.section.export_metadata"]').click(); + + // Analyze for accessibility + testA11y('ds-export-metadata-selector'); + }); + + 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(); + + // 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('a[data-test="menu.section.export_batch"]').click(); + + // Analyze for accessibility + testA11y('ds-export-batch-selector'); + }); +}); diff --git a/cypress/e2e/admin-notifications-publication-claim-page.cy.ts b/cypress/e2e/admin-notifications-publication-claim-page.cy.ts new file mode 100644 index 0000000000..877a0542e2 --- /dev/null +++ b/cypress/e2e/admin-notifications-publication-claim-page.cy.ts @@ -0,0 +1,17 @@ +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 for accessibility issues + testA11y('ds-admin-notifications-publication-claim-page'); + }); +}); diff --git a/cypress/e2e/admin-search-page.cy.ts b/cypress/e2e/admin-search-page.cy.ts new file mode 100644 index 0000000000..4fbf8939fe --- /dev/null +++ b/cypress/e2e/admin-search-page.cy.ts @@ -0,0 +1,21 @@ +import { testA11y } from 'cypress/support/utils'; + +describe('Admin Search Page', () => { + beforeEach(() => { + // Must login as an Admin to see the page + cy.visit('/admin/search'); + 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-search-page').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 }); + // Analyze for accessibility issues + testA11y('ds-admin-search-page'); + }); +}); diff --git a/cypress/e2e/admin-sidebar.cy.ts b/cypress/e2e/admin-sidebar.cy.ts index 7612eb5313..318bf2b27e 100644 --- a/cypress/e2e/admin-sidebar.cy.ts +++ b/cypress/e2e/admin-sidebar.cy.ts @@ -1,28 +1,28 @@ -import { Options } from 'cypress-axe'; import { testA11y } from 'cypress/support/utils'; +import { Options } from 'cypress-axe'; 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('#sidebar-collapse-toggle').click(); + it('should be pinnable and pass accessibility tests', () => { + // Pin the sidebar open + cy.get('[data-test="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 for accessibility - testA11y('ds-admin-sidebar', + // Analyze 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 }, - } + rules: { + // Currently all expandable sections have nested interactive elements + // See https://github.com/DSpace/dspace-angular/issues/2178 + 'nested-interactive': { enabled: false }, + }, } as Options); - }); + }); }); diff --git a/cypress/e2e/admin-workflow-page.cy.ts b/cypress/e2e/admin-workflow-page.cy.ts new file mode 100644 index 0000000000..c3c235e346 --- /dev/null +++ b/cypress/e2e/admin-workflow-page.cy.ts @@ -0,0 +1,21 @@ +import { testA11y } from 'cypress/support/utils'; + +describe('Admin Workflow Page', () => { + beforeEach(() => { + // Must login as an Admin to see the page + cy.visit('/admin/workflow'); + 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-workflow-page').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 }); + // Analyze for accessibility issues + testA11y('ds-admin-workflow-page'); + }); +}); diff --git a/cypress/e2e/batch-import-page.cy.ts b/cypress/e2e/batch-import-page.cy.ts new file mode 100644 index 0000000000..871b8644ce --- /dev/null +++ b/cypress/e2e/batch-import-page.cy.ts @@ -0,0 +1,16 @@ +import { testA11y } from 'cypress/support/utils'; + +describe('Batch Import Page', () => { + beforeEach(() => { + // Must login as an Admin to see processes + cy.visit('/admin/batch-import'); + cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD')); + }); + + it('should pass accessibility tests', () => { + // Batch import form must first be visible + cy.get('ds-batch-import-page').should('be.visible'); + // Analyze for accessibility issues + testA11y('ds-batch-import-page'); + }); +}); diff --git a/cypress/e2e/bitstreams-format.cy.ts b/cypress/e2e/bitstreams-format.cy.ts new file mode 100644 index 0000000000..f113d45ebc --- /dev/null +++ b/cypress/e2e/bitstreams-format.cy.ts @@ -0,0 +1,16 @@ +import { testA11y } from 'cypress/support/utils'; + +describe('Bitstreams Formats', () => { + beforeEach(() => { + // Must login as an Admin to see the page + cy.visit('/admin/registries/bitstream-formats'); + 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-bitstream-formats').should('be.visible'); + // Analyze for accessibility issues + testA11y('ds-bitstream-formats'); + }); +}); diff --git a/cypress/e2e/breadcrumbs.cy.ts b/cypress/e2e/breadcrumbs.cy.ts index 0cddbc723c..f660f47a54 100644 --- a/cypress/e2e/breadcrumbs.cy.ts +++ b/cypress/e2e/breadcrumbs.cy.ts @@ -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 for accessibility - testA11y('ds-breadcrumbs'); - }); + // Analyze for accessibility + testA11y('ds-breadcrumbs'); + }); }); diff --git a/cypress/e2e/browse-by-author.cy.ts b/cypress/e2e/browse-by-author.cy.ts index 32c470231d..3e914a2f8c 100644 --- a/cypress/e2e/browse-by-author.cy.ts +++ b/cypress/e2e/browse-by-author.cy.ts @@ -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 to be visible - cy.get('ds-browse-by-metadata').should('be.visible'); + // Wait for to be visible + cy.get('ds-browse-by-metadata').should('be.visible'); - // Analyze for accessibility - testA11y('ds-browse-by-metadata'); - }); + // Analyze for accessibility + testA11y('ds-browse-by-metadata'); + }); }); diff --git a/cypress/e2e/browse-by-dateissued.cy.ts b/cypress/e2e/browse-by-dateissued.cy.ts index 7966f1c82e..5fe0543315 100644 --- a/cypress/e2e/browse-by-dateissued.cy.ts +++ b/cypress/e2e/browse-by-dateissued.cy.ts @@ -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 to be visible - cy.get('ds-browse-by-date').should('be.visible'); + // Wait for to be visible + cy.get('ds-browse-by-date').should('be.visible'); - // Analyze for accessibility - testA11y('ds-browse-by-date'); - }); + // Analyze for accessibility + testA11y('ds-browse-by-date'); + }); }); diff --git a/cypress/e2e/browse-by-subject.cy.ts b/cypress/e2e/browse-by-subject.cy.ts index 57ca88d103..0937a2542b 100644 --- a/cypress/e2e/browse-by-subject.cy.ts +++ b/cypress/e2e/browse-by-subject.cy.ts @@ -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 to be visible - cy.get('ds-browse-by-metadata').should('be.visible'); + // Wait for to be visible + cy.get('ds-browse-by-metadata').should('be.visible'); - // Analyze for accessibility - testA11y('ds-browse-by-metadata'); - }); + // Analyze for accessibility + testA11y('ds-browse-by-metadata'); + }); }); diff --git a/cypress/e2e/browse-by-title.cy.ts b/cypress/e2e/browse-by-title.cy.ts index 09195c30df..71a7356ce3 100644 --- a/cypress/e2e/browse-by-title.cy.ts +++ b/cypress/e2e/browse-by-title.cy.ts @@ -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 to be visible - cy.get('ds-browse-by-title').should('be.visible'); + // Wait for to be visible + cy.get('ds-browse-by-title').should('be.visible'); - // Analyze for accessibility - testA11y('ds-browse-by-title'); - }); + // Analyze for accessibility + testA11y('ds-browse-by-title'); + }); }); diff --git a/cypress/e2e/bulk-access.cy.ts b/cypress/e2e/bulk-access.cy.ts new file mode 100644 index 0000000000..87033e13e4 --- /dev/null +++ b/cypress/e2e/bulk-access.cy.ts @@ -0,0 +1,31 @@ +import { testA11y } from 'cypress/support/utils'; +import { Options } from 'cypress-axe'; + +describe('Bulk Access', () => { + beforeEach(() => { + // Must login as an Admin to see the page + cy.visit('/access-control/bulk-access'); + 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-bulk-access').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 }); + // Analyze for accessibility issues + testA11y('ds-bulk-access', { + 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 }, + // Card titles fail this test currently + 'heading-order': { enabled: false }, + }, + } as Options); + }); +}); diff --git a/cypress/e2e/collection-create.cy.ts b/cypress/e2e/collection-create.cy.ts new file mode 100644 index 0000000000..29f7dd5cac --- /dev/null +++ b/cypress/e2e/collection-create.cy.ts @@ -0,0 +1,13 @@ +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'); +}); diff --git a/cypress/e2e/collection-edit.cy.ts b/cypress/e2e/collection-edit.cy.ts index 63d873db3e..e1ba1c5eed 100644 --- a/cypress/e2e/collection-edit.cy.ts +++ b/cypress/e2e/collection-edit.cy.ts @@ -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', () => { - // tag must be loaded - cy.get('ds-edit-collection').should('be.visible'); + it('should pass accessibility tests', () => { + // tag must be loaded + cy.get('ds-edit-collection').should('be.visible'); - // Analyze for accessibility issues - testA11y('ds-edit-collection'); - }); + // Analyze 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(); - // tag must be loaded - cy.get('ds-collection-roles').should('be.visible'); + // 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(); - // tag must be loaded - cy.get('ds-collection-source').should('be.visible'); + // 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(); - // tag must be loaded - cy.get('ds-collection-curate').should('be.visible'); + // 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(); - // tag must be loaded - cy.get('ds-collection-access-control').should('be.visible'); + // 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(); - // tag must be loaded - cy.get('ds-collection-authorizations').should('be.visible'); + // 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(); - // tag must be loaded - cy.get('ds-collection-item-mapper').should('be.visible'); + // 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(); - // tag must be loaded - cy.get('ds-delete-collection').should('be.visible'); + // 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'); + }); }); diff --git a/cypress/e2e/collection-page.cy.ts b/cypress/e2e/collection-page.cy.ts index 55c10cc6e2..d12536d332 100644 --- a/cypress/e2e/collection-page.cy.ts +++ b/cypress/e2e/collection-page.cy.ts @@ -2,13 +2,13 @@ import { testA11y } from 'cypress/support/utils'; describe('Collection Page', () => { - it('should pass accessibility tests', () => { - cy.visit('/collections/'.concat(Cypress.env('DSPACE_TEST_COLLECTION'))); + it('should pass accessibility tests', () => { + cy.visit('/collections/'.concat(Cypress.env('DSPACE_TEST_COLLECTION'))); - // tag must be loaded - cy.get('ds-collection-page').should('be.visible'); + // tag must be loaded + cy.get('ds-collection-page').should('be.visible'); - // Analyze for accessibility issues - testA11y('ds-collection-page'); - }); + // Analyze for accessibility issues + testA11y('ds-collection-page'); + }); }); diff --git a/cypress/e2e/collection-statistics.cy.ts b/cypress/e2e/collection-statistics.cy.ts index a08f8cb198..3e5a465e39 100644 --- a/cypress/e2e/collection-statistics.cy.ts +++ b/cypress/e2e/collection-statistics.cy.ts @@ -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); - // tag must be loaded - cy.get('ds-collection-statistics-page').should('be.visible'); + // 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 for accessibility issues - testA11y('ds-collection-statistics-page'); - }); + // Analyze for accessibility issues + testA11y('ds-collection-statistics-page'); + }); }); diff --git a/cypress/e2e/community-create.cy.ts b/cypress/e2e/community-create.cy.ts new file mode 100644 index 0000000000..96bc003ba2 --- /dev/null +++ b/cypress/e2e/community-create.cy.ts @@ -0,0 +1,13 @@ +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'); +}); diff --git a/cypress/e2e/community-edit.cy.ts b/cypress/e2e/community-edit.cy.ts index 8fc1a7733e..77e260feec 100644 --- a/cypress/e2e/community-edit.cy.ts +++ b/cypress/e2e/community-edit.cy.ts @@ -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', () => { - // tag must be loaded - cy.get('ds-edit-community').should('be.visible'); + it('should pass accessibility tests', () => { + // tag must be loaded + cy.get('ds-edit-community').should('be.visible'); - // Analyze for accessibility issues - testA11y('ds-edit-community'); - }); + // Analyze 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(); - // tag must be loaded - cy.get('ds-community-roles').should('be.visible'); + // 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(); - // tag must be loaded - cy.get('ds-community-curate').should('be.visible'); + // 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(); - // tag must be loaded - cy.get('ds-community-access-control').should('be.visible'); + // 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(); - // tag must be loaded - cy.get('ds-community-authorizations').should('be.visible'); + // 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(); - // tag must be loaded - cy.get('ds-delete-community').should('be.visible'); + // 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'); + }); }); diff --git a/cypress/e2e/community-list.cy.ts b/cypress/e2e/community-list.cy.ts index c371f6ceae..9b9c87b112 100644 --- a/cypress/e2e/community-list.cy.ts +++ b/cypress/e2e/community-list.cy.ts @@ -2,16 +2,16 @@ import { testA11y } from 'cypress/support/utils'; describe('Community List Page', () => { - it('should pass accessibility tests', () => { - cy.visit('/community-list'); + it('should pass accessibility tests', () => { + cy.visit('/community-list'); - // tag must be loaded - cy.get('ds-community-list-page').should('be.visible'); + // 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 }); + // 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 for accessibility issues - testA11y('ds-community-list-page'); - }); + // Analyze for accessibility issues + testA11y('ds-community-list-page'); + }); }); diff --git a/cypress/e2e/community-page.cy.ts b/cypress/e2e/community-page.cy.ts index 386bb592a0..5a4441dbae 100644 --- a/cypress/e2e/community-page.cy.ts +++ b/cypress/e2e/community-page.cy.ts @@ -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'))); - // tag must be loaded - cy.get('ds-community-page').should('be.visible'); + // tag must be loaded + cy.get('ds-community-page').should('be.visible'); - // Analyze for accessibility issues - testA11y('ds-community-page'); - }); + // Analyze for accessibility issues + testA11y('ds-community-page'); + }); }); diff --git a/cypress/e2e/community-statistics.cy.ts b/cypress/e2e/community-statistics.cy.ts index 6cafed0350..00e23a90b3 100644 --- a/cypress/e2e/community-statistics.cy.ts +++ b/cypress/e2e/community-statistics.cy.ts @@ -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('ds-navbar ds-link-menu-item 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); - // tag must be loaded - cy.get('ds-community-statistics-page').should('be.visible'); + // 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 for accessibility issues - testA11y('ds-community-statistics-page'); - }); + // Analyze for accessibility issues + testA11y('ds-community-statistics-page'); + }); }); diff --git a/cypress/e2e/create-eperson.cy.ts b/cypress/e2e/create-eperson.cy.ts new file mode 100644 index 0000000000..d23986ba29 --- /dev/null +++ b/cypress/e2e/create-eperson.cy.ts @@ -0,0 +1,16 @@ +import { testA11y } from 'cypress/support/utils'; + +describe('Create Eperson', () => { + beforeEach(() => { + // Must login as an Admin to see the page + cy.visit('/access-control/epeople/create'); + cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD')); + }); + + it('should pass accessibility tests', () => { + // Form must first be visible + cy.get('ds-eperson-form').should('be.visible'); + // Analyze for accessibility issues + testA11y('ds-eperson-form'); + }); +}); diff --git a/cypress/e2e/create-group.cy.ts b/cypress/e2e/create-group.cy.ts new file mode 100644 index 0000000000..135c041a8d --- /dev/null +++ b/cypress/e2e/create-group.cy.ts @@ -0,0 +1,16 @@ +import { testA11y } from 'cypress/support/utils'; + +describe('Create Group', () => { + beforeEach(() => { + // Must login as an Admin to see the page + cy.visit('/access-control/groups/create'); + cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD')); + }); + + it('should pass accessibility tests', () => { + // Form must first be visible + cy.get('ds-group-form').should('be.visible'); + // Analyze for accessibility issues + testA11y('ds-group-form'); + }); +}); diff --git a/cypress/e2e/edit-eperson.cy.ts b/cypress/e2e/edit-eperson.cy.ts new file mode 100644 index 0000000000..166c913b8c --- /dev/null +++ b/cypress/e2e/edit-eperson.cy.ts @@ -0,0 +1,16 @@ +import { testA11y } from 'cypress/support/utils'; + +describe('Edit Eperson', () => { + beforeEach(() => { + // Must login as an Admin to see the page + cy.visit('/access-control/epeople/'.concat(Cypress.env('DSPACE_TEST_ADMIN_USER_UUID')).concat('/edit')); + cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD')); + }); + + it('should pass accessibility tests', () => { + // Form must first be visible + cy.get('ds-eperson-form').should('be.visible'); + // Analyze for accessibility issues + testA11y('ds-eperson-form'); + }); +}); diff --git a/cypress/e2e/edit-group.cy.ts b/cypress/e2e/edit-group.cy.ts new file mode 100644 index 0000000000..e43ede978a --- /dev/null +++ b/cypress/e2e/edit-group.cy.ts @@ -0,0 +1,16 @@ +import { testA11y } from 'cypress/support/utils'; + +describe('Edit Group', () => { + beforeEach(() => { + // Must login as an Admin to see the page + cy.visit('/access-control/groups/'.concat(Cypress.env('DSPACE_ADMINISTRATOR_GROUP')).concat('/edit')); + cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD')); + }); + + it('should pass accessibility tests', () => { + // Form must first be visible + cy.get('ds-group-form').should('be.visible'); + // Analyze for accessibility issues + testA11y('ds-group-form'); + }); +}); diff --git a/cypress/e2e/end-user-agreement.cy.ts b/cypress/e2e/end-user-agreement.cy.ts new file mode 100644 index 0000000000..989d21ce60 --- /dev/null +++ b/cypress/e2e/end-user-agreement.cy.ts @@ -0,0 +1,13 @@ +import { testA11y } from 'cypress/support/utils'; + +describe('End User Agreement', () => { + it('should pass accessibility tests', () => { + cy.visit('/info/end-user-agreement'); + + // Page must first be visible + cy.get('ds-end-user-agreement').should('be.visible'); + + // Analyze for accessibility + testA11y('ds-end-user-agreement'); + }); +}); diff --git a/cypress/e2e/epeople-registry.cy.ts b/cypress/e2e/epeople-registry.cy.ts new file mode 100644 index 0000000000..a6192f13d9 --- /dev/null +++ b/cypress/e2e/epeople-registry.cy.ts @@ -0,0 +1,16 @@ +import { testA11y } from 'cypress/support/utils'; + +describe('Epeople registry', () => { + beforeEach(() => { + // Must login as an Admin to see the page + cy.visit('/access-control/epeople'); + cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD')); + }); + + it('should pass accessibility tests', () => { + // Epeople registry page must first be visible + cy.get('ds-epeople-registry').should('be.visible'); + // Analyze for accessibility issues + testA11y('ds-epeople-registry'); + }); +}); diff --git a/cypress/e2e/feedback.cy.ts b/cypress/e2e/feedback.cy.ts new file mode 100644 index 0000000000..75fe1097c6 --- /dev/null +++ b/cypress/e2e/feedback.cy.ts @@ -0,0 +1,13 @@ +import { testA11y } from 'cypress/support/utils'; + +describe('Feedback', () => { + it('should pass accessibility tests', () => { + cy.visit('/info/feedback'); + + // Page must first be visible + cy.get('ds-feedback').should('be.visible'); + + // Analyze for accessibility + testA11y('ds-feedback'); + }); +}); diff --git a/cypress/e2e/footer.cy.ts b/cypress/e2e/footer.cy.ts index 656e9d4701..4ee1d6669a 100644 --- a/cypress/e2e/footer.cy.ts +++ b/cypress/e2e/footer.cy.ts @@ -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 for accessibility - testA11y('ds-footer'); - }); + // Analyze for accessibility + testA11y('ds-footer'); + }); }); diff --git a/cypress/e2e/groups-registry.cy.ts b/cypress/e2e/groups-registry.cy.ts new file mode 100644 index 0000000000..5c0099c2f1 --- /dev/null +++ b/cypress/e2e/groups-registry.cy.ts @@ -0,0 +1,16 @@ +import { testA11y } from 'cypress/support/utils'; + +describe('Groups registry', () => { + beforeEach(() => { + // Must login as an Admin to see the page + cy.visit('/access-control/groups'); + cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD')); + }); + + it('should pass accessibility tests', () => { + // Epeople registry page must first be visible + cy.get('ds-groups-registry').should('be.visible'); + // Analyze for accessibility issues + testA11y('ds-groups-registry'); + }); +}); diff --git a/cypress/e2e/header.cy.ts b/cypress/e2e/header.cy.ts index 9852216e43..1471e5ae6c 100644 --- a/cypress/e2e/header.cy.ts +++ b/cypress/e2e/header.cy.ts @@ -1,13 +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 for accessibility - testA11y('ds-header'); - }); + // Analyze for accessibility + testA11y('ds-header'); + }); + + it('should allow for changing language to German (for example)', () => { + cy.visit('/'); + + // Click the language switcher (globe) in header + cy.get('a[data-test="lang-switch"]').click(); + // Click on the "Deusch" language in dropdown + cy.get('#language-menu-list li').contains('Deutsch').click(); + + // HTML "lang" attribute should switch to "de" + cy.get('html').invoke('attr', 'lang').should('eq', 'de'); + + // Login menu should now be in German + cy.get('a[data-test="login-menu"]').contains('Anmelden'); + + // Change back to English from language switcher + cy.get('a[data-test="lang-switch"]').click(); + cy.get('#language-menu-list li').contains('English').click(); + + // HTML "lang" attribute should switch to "en" + cy.get('html').invoke('attr', 'lang').should('eq', 'en'); + + // Login menu should now be in English + cy.get('a[data-test="login-menu"]').contains('Log In'); + }); }); diff --git a/cypress/e2e/health-page.cy.ts b/cypress/e2e/health-page.cy.ts new file mode 100644 index 0000000000..c702fa72d7 --- /dev/null +++ b/cypress/e2e/health-page.cy.ts @@ -0,0 +1,62 @@ +import { testA11y } from 'cypress/support/utils'; +import { Options } from 'cypress-axe'; + + +beforeEach(() => { + // Must login as an Admin to see the page + 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(); + // Page must first be visible + cy.get('ds-health-page').should('be.visible'); + cy.get('ds-health-panel').should('be.visible'); + + // wait for all the ds-health-info-component components to be rendered + cy.get('div[role="tabpanel"]').each(($panel: HTMLDivElement) => { + cy.wrap($panel).find('ds-health-component').should('be.visible'); + }); + // Analyze for accessibility issues + testA11y('ds-health-page', { + 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); + }); +}); + +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(); + // Page must first be visible + cy.get('ds-health-page').should('be.visible'); + cy.get('ds-health-info').should('be.visible'); + + // wait for all the ds-health-info-component components to be rendered + cy.get('div[role="tabpanel"]').each(($panel: HTMLDivElement) => { + cy.wrap($panel).find('ds-health-info-component').should('be.visible'); + }); + + // Analyze for accessibility issues + testA11y('ds-health-info', { + 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 }, + }, + } as Options); + }); +}); diff --git a/cypress/e2e/homepage-statistics.cy.ts b/cypress/e2e/homepage-statistics.cy.ts index ece38686b9..0e0fca3c5b 100644 --- a/cypress/e2e/homepage-statistics.cy.ts +++ b/cypress/e2e/homepage-statistics.cy.ts @@ -1,31 +1,32 @@ -import { REGEX_MATCH_NON_EMPTY_TEXT } from 'cypress/support/e2e'; -import { testA11y } from 'cypress/support/utils'; import '../support/commands'; +import { REGEX_MATCH_NON_EMPTY_TEXT } from 'cypress/support/e2e'; +import { testA11y } from 'cypress/support/utils'; + describe('Site Statistics Page', () => { - it('should load if you click on "Statistics" from homepage', () => { - cy.visit('/'); - cy.get('ds-navbar ds-link-menu-item a[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('ds-navbar ds-link-menu-item 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'); - // tag must be visable - cy.get('ds-site-statistics-page').should('be.visible'); + // tag must be visible + 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 for accessibility issues - testA11y('ds-site-statistics-page'); - }); + // Analyze for accessibility issues + testA11y('ds-site-statistics-page'); + }); }); diff --git a/cypress/e2e/item-edit.cy.ts b/cypress/e2e/item-edit.cy.ts index b4c01a1a94..ad5d8ea093 100644 --- a/cypress/e2e/item-edit.cy.ts +++ b/cypress/e2e/item-edit.cy.ts @@ -1,135 +1,180 @@ -import { Options } from 'cypress-axe'; import { testA11y } from 'cypress/support/utils'; +import { Options } from 'cypress-axe'; const ITEM_EDIT_PAGE = '/items/'.concat(Cypress.env('DSPACE_TEST_ENTITY_PUBLICATION')).concat('/edit'); beforeEach(() => { - // All tests start with visiting the Edit Item Page - cy.visit(ITEM_EDIT_PAGE); + // All tests start with visiting the Edit Item Page + cy.visit(ITEM_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 Item > Edit Metadata tab', () => { - it('should pass accessibility tests', () => { - cy.get('a[data-test="metadata"]').click(); + it('should pass accessibility tests', () => { + cy.get('a[data-test="metadata"]').should('be.visible'); + cy.get('a[data-test="metadata"]').click(); - // tag must be loaded - cy.get('ds-edit-item-page').should('be.visible'); + // Our selected tab should be both visible & active + cy.get('a[data-test="metadata"]').should('be.visible'); + cy.get('a[data-test="metadata"]').should('have.class', 'active'); - // Analyze for accessibility issues - testA11y('ds-edit-item-page'); + // tag must be loaded + cy.get('ds-edit-item-page').should('be.visible'); + + // wait for all the ds-dso-edit-metadata-value components to be rendered + cy.get('ds-dso-edit-metadata-value div[role="row"]').each(($row: HTMLDivElement) => { + cy.wrap($row).find('div[role="cell"]').should('be.visible'); }); + + // Analyze for accessibility issues + testA11y('ds-edit-item-page'); + }); }); describe('Edit Item > Status tab', () => { - it('should pass accessibility tests', () => { - cy.get('a[data-test="status"]').click(); + it('should pass accessibility tests', () => { + cy.get('a[data-test="status"]').should('be.visible'); + cy.get('a[data-test="status"]').click(); - // tag must be loaded - cy.get('ds-item-status').should('be.visible'); + // Our selected tab should be both visible & active + cy.get('a[data-test="status"]').should('be.visible'); + cy.get('a[data-test="status"]').should('have.class', 'active'); - // Analyze for accessibility issues - testA11y('ds-item-status'); - }); + // tag must be loaded + cy.get('ds-item-status').should('be.visible'); + + // Analyze for accessibility issues + testA11y('ds-item-status'); + }); }); describe('Edit Item > Bitstreams tab', () => { - it('should pass accessibility tests', () => { - cy.get('a[data-test="bitstreams"]').click(); + it('should pass accessibility tests', () => { + cy.get('a[data-test="bitstreams"]').should('be.visible'); + cy.get('a[data-test="bitstreams"]').click(); - // tag must be loaded - cy.get('ds-item-bitstreams').should('be.visible'); + // Our selected tab should be both visible & active + cy.get('a[data-test="bitstreams"]').should('be.visible'); + cy.get('a[data-test="bitstreams"]').should('have.class', 'active'); - // Table of item bitstreams must also be loaded - cy.get('div.item-bitstreams').should('be.visible'); + // tag must be loaded + cy.get('ds-item-bitstreams').should('be.visible'); - // Analyze for accessibility issues - testA11y('ds-item-bitstreams', + // Table of item bitstreams must also be loaded + cy.get('div.item-bitstreams').should('be.visible'); + + // Analyze for accessibility issues + testA11y('ds-item-bitstreams', { - rules: { - // Currently Bitstreams page loads a pagination component per Bundle - // and they all use the same 'id="p-dad"'. - 'duplicate-id': { enabled: false }, - } - } as Options - ); - }); + rules: { + // Currently Bitstreams page loads a pagination component per Bundle + // and they all use the same 'id="p-dad"'. + 'duplicate-id': { enabled: false }, + }, + } as Options, + ); + }); }); describe('Edit Item > 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"]').should('be.visible'); + cy.get('a[data-test="curate"]').click(); - // tag must be loaded - cy.get('ds-item-curate').should('be.visible'); + // Our selected tab should be both visible & active + cy.get('a[data-test="curate"]').should('be.visible'); + cy.get('a[data-test="curate"]').should('have.class', 'active'); - // Analyze for accessibility issues - testA11y('ds-item-curate'); - }); + // tag must be loaded + cy.get('ds-item-curate').should('be.visible'); + + // Analyze for accessibility issues + testA11y('ds-item-curate'); + }); }); describe('Edit Item > Relationships tab', () => { - it('should pass accessibility tests', () => { - cy.get('a[data-test="relationships"]').click(); + it('should pass accessibility tests', () => { + cy.get('a[data-test="relationships"]').should('be.visible'); + cy.get('a[data-test="relationships"]').click(); - // tag must be loaded - cy.get('ds-item-relationships').should('be.visible'); + // Our selected tab should be both visible & active + cy.get('a[data-test="relationships"]').should('be.visible'); + cy.get('a[data-test="relationships"]').should('have.class', 'active'); - // Analyze for accessibility issues - testA11y('ds-item-relationships'); - }); + // tag must be loaded + cy.get('ds-item-relationships').should('be.visible'); + + // Analyze for accessibility issues + testA11y('ds-item-relationships'); + }); }); describe('Edit Item > Version History tab', () => { - it('should pass accessibility tests', () => { - cy.get('a[data-test="versionhistory"]').click(); + it('should pass accessibility tests', () => { + cy.get('a[data-test="versionhistory"]').should('be.visible'); + cy.get('a[data-test="versionhistory"]').click(); - // tag must be loaded - cy.get('ds-item-version-history').should('be.visible'); + // Our selected tab should be both visible & active + cy.get('a[data-test="versionhistory"]').should('be.visible'); + cy.get('a[data-test="versionhistory"]').should('have.class', 'active'); - // Analyze for accessibility issues - testA11y('ds-item-version-history'); - }); + // tag must be loaded + cy.get('ds-item-version-history').should('be.visible'); + + // Analyze for accessibility issues + testA11y('ds-item-version-history'); + }); }); describe('Edit Item > 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"]').should('be.visible'); + cy.get('a[data-test="access-control"]').click(); - // tag must be loaded - cy.get('ds-item-access-control').should('be.visible'); + // Our selected tab should be both visible & active + cy.get('a[data-test="access-control"]').should('be.visible'); + cy.get('a[data-test="access-control"]').should('have.class', 'active'); - // Analyze for accessibility issues - testA11y('ds-item-access-control'); - }); + // tag must be loaded + cy.get('ds-item-access-control').should('be.visible'); + + // Analyze for accessibility issues + testA11y('ds-item-access-control'); + }); }); describe('Edit Item > Collection 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"]').should('be.visible'); + cy.get('a[data-test="mapper"]').click(); - // tag must be loaded - cy.get('ds-item-collection-mapper').should('be.visible'); + // Our selected tab should be both visible & active + cy.get('a[data-test="mapper"]').should('be.visible'); + cy.get('a[data-test="mapper"]').should('have.class', 'active'); - // Analyze entire page for accessibility issues - testA11y('ds-item-collection-mapper'); + // tag must be loaded + cy.get('ds-item-collection-mapper').should('be.visible'); - // Click on the "Map new collections" tab - cy.get('li[data-test="mapTab"] a').click(); + // Analyze entire page for accessibility issues + testA11y('ds-item-collection-mapper'); - // Make sure search form is now visible - cy.get('ds-search-form').should('be.visible'); + // Click on the "Map new collections" tab + cy.get('li[data-test="mapTab"] a').click(); - // Analyze entire page (again) for accessibility issues - testA11y('ds-item-collection-mapper'); - }); + // Make sure search form is now visible + cy.get('ds-search-form').should('be.visible'); + + // Analyze entire page (again) for accessibility issues + testA11y('ds-item-collection-mapper'); + }); }); diff --git a/cypress/e2e/item-page.cy.ts b/cypress/e2e/item-page.cy.ts index a6a208e9f4..b79b6ac31d 100644 --- a/cypress/e2e/item-page.cy.ts +++ b/cypress/e2e/item-page.cy.ts @@ -1,32 +1,32 @@ 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.location('pathname').should('eq', ENTITYPAGE); + }); - it('should pass accessibility tests', () => { - cy.visit(ENTITYPAGE); + it('should pass accessibility tests', () => { + cy.visit(ENTITYPAGE); - // tag must be loaded - cy.get('ds-item-page').should('be.visible'); + // tag must be loaded + cy.get('ds-item-page').should('be.visible'); - // Analyze for accessibility issues - testA11y('ds-item-page'); - }); + // Analyze for accessibility issues + testA11y('ds-item-page'); + }); - it('should pass accessibility tests on full item page', () => { - cy.visit(ENTITYPAGE + '/full'); + it('should pass accessibility tests on full item page', () => { + cy.visit(ENTITYPAGE + '/full'); - // tag must be loaded - cy.get('ds-full-item-page').should('be.visible'); + // tag must be loaded + cy.get('ds-full-item-page').should('be.visible'); - // Analyze for accessibility issues - testA11y('ds-full-item-page'); - }); + // Analyze for accessibility issues + testA11y('ds-full-item-page'); + }); }); diff --git a/cypress/e2e/item-statistics.cy.ts b/cypress/e2e/item-statistics.cy.ts index 6caeacae8e..6518f595a9 100644 --- a/cypress/e2e/item-statistics.cy.ts +++ b/cypress/e2e/item-statistics.cy.ts @@ -2,42 +2,42 @@ 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', ITEMSTATISTICSPAGE); - }); + 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', 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); + // 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 pass accessibility tests', () => { - cy.visit(ITEMSTATISTICSPAGE); + it('should pass accessibility tests', () => { + cy.visit(ITEMSTATISTICSPAGE); - // tag must be loaded - cy.get('ds-item-statistics-page').should('be.visible'); + // 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 for accessibility issues - testA11y('ds-item-statistics-page'); - }); + // Analyze for accessibility issues + testA11y('ds-item-statistics-page'); + }); }); diff --git a/cypress/e2e/item-template.cy.ts b/cypress/e2e/item-template.cy.ts new file mode 100644 index 0000000000..5f5b21a16a --- /dev/null +++ b/cypress/e2e/item-template.cy.ts @@ -0,0 +1,15 @@ +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'); + }); +}); diff --git a/cypress/e2e/login-modal.cy.ts b/cypress/e2e/login-modal.cy.ts index 673041e9f3..80d36a0309 100644 --- a/cypress/e2e/login-modal.cy.ts +++ b/cypress/e2e/login-modal.cy.ts @@ -1,150 +1,150 @@ import { testA11y } from 'cypress/support/utils'; const page = { - openLoginMenu() { - // Click the "Log In" dropdown menu in header - cy.get('ds-themed-header [data-test="login-menu"]').click(); - }, - openUserMenu() { - // Once logged in, click the User menu in header - cy.get('ds-themed-header [data-test="user-menu"]').click(); - }, - submitLoginAndPasswordByPressingButton(email, password) { - // Enter email - cy.get('ds-themed-header [data-test="email"]').type(email); - // Enter password - cy.get('ds-themed-header [data-test="password"]').type(password); - // Click login button - cy.get('ds-themed-header [data-test="login-button"]').click(); - }, - submitLoginAndPasswordByPressingEnter(email, password) { - // In opened Login modal, fill out email & password, then click Enter - cy.get('ds-themed-header [data-test="email"]').type(email); - cy.get('ds-themed-header [data-test="password"]').type(password); - cy.get('ds-themed-header [data-test="password"]').type('{enter}'); - }, - submitLogoutByPressingButton() { - // This is the POST command that will actually log us out - cy.intercept('POST', '/server/api/authn/logout').as('logout'); - // Click logout button - cy.get('ds-themed-header [data-test="logout-button"]').click(); - // Wait until above POST command responds before continuing - // (This ensures next action waits until logout completes) - cy.wait('@logout'); - } + openLoginMenu() { + // Click the "Log In" dropdown menu in header + cy.get('[data-test="login-menu"]').click(); + }, + openUserMenu() { + // Once logged in, click the User menu in header + cy.get('[data-test="user-menu"]').click(); + }, + submitLoginAndPasswordByPressingButton(email, password) { + // Enter email + cy.get('[data-test="email"]').type(email); + // Enter password + cy.get('[data-test="password"]').type(password); + // Click login button + cy.get('[data-test="login-button"]').click(); + }, + submitLoginAndPasswordByPressingEnter(email, password) { + // In opened Login modal, fill out email & password, then click Enter + cy.get('[data-test="email"]').type(email); + cy.get('[data-test="password"]').type(password); + cy.get('[data-test="password"]').type('{enter}'); + }, + submitLogoutByPressingButton() { + // This is the POST command that will actually log us out + cy.intercept('POST', '/server/api/authn/logout').as('logout'); + // Click logout button + cy.get('[data-test="logout-button"]').click(); + // Wait until above POST command responds before continuing + // (This ensures next action waits until logout completes) + cy.wait('@logout'); + }, }; 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 tag should no longer exist - page.openLoginMenu(); - cy.get('.form-login').should('be.visible'); + // Login, and the 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 tag is visible - page.openLoginMenu(); - cy.get('.form-login').should('be.visible'); + // Open login menu in header & verify tag is visible + page.openLoginMenu(); + cy.get('.form-login').should('be.visible'); - // Login, and the tag should no longer exist - page.submitLoginAndPasswordByPressingEnter(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD')); - cy.get('.form-login').should('not.exist'); + // Login, and the 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-themed-header [data-test="register"]').should('be.visible'); + // Registration link should be visible + cy.get('ds-header [data-test="register"]').should('be.visible'); - // 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'); + // 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'); - // 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-themed-header [data-test="forgot"]').should('be.visible'); + // Forgot password link should be visible + cy.get('ds-header [data-test="forgot"]').should('be.visible'); - // 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'); + // 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'); - // 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 accesibility - page.openUserMenu(); - cy.get('ds-user-menu').should('be.visible'); - testA11y('ds-user-menu'); - }); + // Open user menu, verify user menu accessibility + page.openUserMenu(); + cy.get('ds-user-menu').should('be.visible'); + testA11y('ds-user-menu'); + }); }); diff --git a/cypress/e2e/metadata-import-page.cy.ts b/cypress/e2e/metadata-import-page.cy.ts new file mode 100644 index 0000000000..a31c18e4eb --- /dev/null +++ b/cypress/e2e/metadata-import-page.cy.ts @@ -0,0 +1,16 @@ +import { testA11y } from 'cypress/support/utils'; + +describe('Metadata Import Page', () => { + beforeEach(() => { + // Must login as an Admin to see the page + cy.visit('/admin/metadata-import'); + cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD')); + }); + + it('should pass accessibility tests', () => { + // Metadata import form must first be visible + cy.get('ds-metadata-import-page').should('be.visible'); + // Analyze for accessibility issues + testA11y('ds-metadata-import-page'); + }); +}); diff --git a/cypress/e2e/metadata-registry.cy.ts b/cypress/e2e/metadata-registry.cy.ts new file mode 100644 index 0000000000..0402d33153 --- /dev/null +++ b/cypress/e2e/metadata-registry.cy.ts @@ -0,0 +1,16 @@ +import { testA11y } from 'cypress/support/utils'; + +describe('Metadata Registry', () => { + beforeEach(() => { + // Must login as an Admin to see the page + cy.visit('/admin/registries/metadata'); + 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-metadata-registry').should('be.visible'); + // Analyze for accessibility issues + testA11y('ds-metadata-registry'); + }); +}); diff --git a/cypress/e2e/metadata-schema.cy.ts b/cypress/e2e/metadata-schema.cy.ts new file mode 100644 index 0000000000..9ff0db0714 --- /dev/null +++ b/cypress/e2e/metadata-schema.cy.ts @@ -0,0 +1,16 @@ +import { testA11y } from 'cypress/support/utils'; + +describe('Metadata Schema', () => { + beforeEach(() => { + // Must login as an Admin to see the page + cy.visit('/admin/registries/metadata/dc'); + 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-metadata-schema').should('be.visible'); + // Analyze for accessibility issues + testA11y('ds-metadata-schema'); + }); +}); diff --git a/cypress/e2e/my-dspace.cy.ts b/cypress/e2e/my-dspace.cy.ts index c48656ffcc..159bb4f5e6 100644 --- a/cypress/e2e/my-dspace.cy.ts +++ b/cypress/e2e/my-dspace.cy.ts @@ -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 for accessibility issues - testA11y('ds-my-dspace-page'); + // Analyze 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 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 (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 have a working detailed view that passes accessibility tests', () => { - cy.visit('/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')); + // 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'); + // 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(); - // Click button in sidebar to display detailed view - cy.get('ds-search-sidebar [data-test="detail-view"]').click(); + // New URL should include /import-external, as we've moved to the import page + cy.url().should('include', '/import-external'); - cy.get('ds-object-detail').should('be.visible'); + // The external import searchbox should be visible + cy.get('ds-submission-import-external-searchbar').should('be.visible'); - // Analyze 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 (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'); - }); + // Test for accessibility issues + testA11y('ds-submission-import-external'); + }); }); diff --git a/cypress/e2e/new-process.cy.ts b/cypress/e2e/new-process.cy.ts new file mode 100644 index 0000000000..d26da7cc4d --- /dev/null +++ b/cypress/e2e/new-process.cy.ts @@ -0,0 +1,16 @@ +import { testA11y } from 'cypress/support/utils'; + +describe('New Process', () => { + beforeEach(() => { + // Must login as an Admin to see the page + cy.visit('/processes/new'); + cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD')); + }); + + it('should pass accessibility tests', () => { + // Process form must first be visible + cy.get('ds-new-process').should('be.visible'); + // Analyze for accessibility issues + testA11y('ds-new-process'); + }); +}); diff --git a/cypress/e2e/pagenotfound.cy.ts b/cypress/e2e/pagenotfound.cy.ts index d02aa8541c..5f84a22df9 100644 --- a/cypress/e2e/pagenotfound.cy.ts +++ b/cypress/e2e/pagenotfound.cy.ts @@ -1,18 +1,18 @@ import { testA11y } from 'cypress/support/utils'; describe('PageNotFound', () => { - it('should contain element ds-pagenotfound when navigating to page that doesnt exist', () => { - // request an invalid page (UUIDs at root path aren't valid) - cy.visit('/e9019a69-d4f1-4773-b6a3-bd362caa46f2', { failOnStatusCode: false }); - cy.get('ds-pagenotfound').should('be.visible'); + 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'); - // Analyze for accessibility issues - testA11y('ds-pagenotfound'); - }); + // Analyze 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'); + }); }); diff --git a/cypress/e2e/privacy.cy.ts b/cypress/e2e/privacy.cy.ts new file mode 100644 index 0000000000..16e049f701 --- /dev/null +++ b/cypress/e2e/privacy.cy.ts @@ -0,0 +1,13 @@ +import { testA11y } from 'cypress/support/utils'; + +describe('Privacy', () => { + it('should pass accessibility tests', () => { + cy.visit('/info/privacy'); + + // Page must first be visible + cy.get('ds-privacy').should('be.visible'); + + // Analyze for accessibility + testA11y('ds-privacy'); + }); +}); diff --git a/cypress/e2e/processes-overview.cy.ts b/cypress/e2e/processes-overview.cy.ts new file mode 100644 index 0000000000..2be3bd4c18 --- /dev/null +++ b/cypress/e2e/processes-overview.cy.ts @@ -0,0 +1,17 @@ +import { testA11y } from 'cypress/support/utils'; + +describe('Processes Overview', () => { + beforeEach(() => { + // Must login as an Admin to see the page + cy.visit('/processes'); + cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD')); + }); + + it('should pass accessibility tests', () => { + + // Process overview must first be visible + cy.get('ds-process-overview').should('be.visible'); + // Analyze for accessibility issues + testA11y('ds-process-overview'); + }); +}); diff --git a/cypress/e2e/profile-page.cy.ts b/cypress/e2e/profile-page.cy.ts new file mode 100644 index 0000000000..911ef33ba5 --- /dev/null +++ b/cypress/e2e/profile-page.cy.ts @@ -0,0 +1,16 @@ +import { testA11y } from 'cypress/support/utils'; + +describe('Profile page', () => { + beforeEach(() => { + // Must login as an Admin to see the page + cy.visit('/profile'); + cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD')); + }); + + it('should pass accessibility tests', () => { + // Process form must first be visible + cy.get('ds-profile-page').should('be.visible'); + // Analyze for accessibility issues + testA11y('ds-profile-page'); + }); +}); diff --git a/cypress/e2e/quality-assurance-source-page.cy.ts b/cypress/e2e/quality-assurance-source-page.cy.ts new file mode 100644 index 0000000000..722917ef16 --- /dev/null +++ b/cypress/e2e/quality-assurance-source-page.cy.ts @@ -0,0 +1,16 @@ +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 for accessibility issues + testA11y('ds-quality-assurance-source-page-component'); + }); +}); diff --git a/cypress/e2e/search-navbar.cy.ts b/cypress/e2e/search-navbar.cy.ts index 28a72bcc78..0613e5e712 100644 --- a/cypress/e2e/search-navbar.cy.ts +++ b/cypress/e2e/search-navbar.cy.ts @@ -1,64 +1,64 @@ const page = { - 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(); - } + 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(); + }, }; 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'); + }); }); diff --git a/cypress/e2e/search-page.cy.ts b/cypress/e2e/search-page.cy.ts index 429f4e6da4..62e73c3877 100644 --- a/cypress/e2e/search-page.cy.ts +++ b/cypress/e2e/search-page.cy.ts @@ -1,57 +1,57 @@ -import { Options } from 'cypress-axe'; import { testA11y } from 'cypress/support/utils'; +import { Options } from 'cypress-axe'; 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); - // tag must be loaded - cy.get('ds-search-page').should('be.visible'); + // 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 for accessibility issues - testA11y('ds-search-page'); - }); + // Analyze 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(); - // tag must be loaded - cy.get('ds-search-page').should('be.visible'); + // 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 for accessibility issues - testA11y('ds-search-page', + // Analyze 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, + ); + }); }); diff --git a/cypress/e2e/submission.cy.ts b/cypress/e2e/submission.cy.ts index 4402410f23..ebdabbde2e 100644 --- a/cypress/e2e/submission.cy.ts +++ b/cypress/e2e/submission.cy.ts @@ -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 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 }, - } + 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 }, + }, - } 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(); + // 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-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 ) + 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', }); - 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 upload to complete before proceeding + cy.wait('@upload'); - // 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')); + // Wait for deposit button to not be disabled & click it. + cy.get('button#deposit').should('not.be.disabled').click(); - // Attempt an immediate deposit without filling out any fields - cy.get('button#deposit').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'); + }); - // 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'); + 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'); - // 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'); + // 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')); - // Title field should have class "is-invalid" applied, as it's required - cy.get('input#dc_title').should('have.class', 'is-invalid'); + // 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(); - // Date Year field should also have "is-valid" class - cy.get('input#dc_date_issued_year').should('have.class', 'is-invalid'); + // This should display the (popup window) + cy.get('ds-create-item-parent-selector').should('be.visible'); - // 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]; + // 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')); - // Even though form is incomplete, the "Save for Later" button should still work - cy.get('button#saveForLater').click(); + // 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(); - // "Save for Later" should send us to MyDSpace - cy.url().should('include', '/mydspace'); + // New URL should include /workspaceitems, as we've started a new submission + cy.url().should('include', '/workspaceitems'); - // 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}); + // The Submission edit form tag should be visible + cy.get('ds-submission-edit').should('be.visible'); - // 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(); + // 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')); - // Wait for search results to come back from the above GET command - cy.wait('@search-results'); + // 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'); - // 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 ) - 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.png', { - 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 (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', + // Test entire page for accessibility + testA11y('ds-submission-edit', { - 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 }, - } + 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 }, + }, - } 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 - ], - }); - - // 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(); + // 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(); + }); }); diff --git a/cypress/e2e/system-wide-alert.cy.ts b/cypress/e2e/system-wide-alert.cy.ts new file mode 100644 index 0000000000..046bfe619f --- /dev/null +++ b/cypress/e2e/system-wide-alert.cy.ts @@ -0,0 +1,16 @@ +import { testA11y } from 'cypress/support/utils'; + +describe('System Wide Alert', () => { + beforeEach(() => { + // Must login as an Admin to see the page + cy.visit('/admin/system-wide-alert'); + 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-system-wide-alert-form').should('be.visible'); + // Analyze for accessibility issues + testA11y('ds-system-wide-alert-form'); + }); +}); diff --git a/cypress/plugins/index.ts b/cypress/plugins/index.ts index cc3dccba38..091f11d0f7 100644 --- a/cypress/plugins/index.ts +++ b/cypress/plugins/index.ts @@ -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 ; + }, + }); }; diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts index 7da454e2d0..8cc2c5c721 100644 --- a/cypress/support/commands.ts +++ b/cypress/support/commands.ts @@ -3,8 +3,14 @@ // 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 @@ -57,33 +63,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); @@ -94,12 +100,12 @@ Cypress.Commands.add('login', login); * @param password password to login as */ function loginViaForm(email: string, password: string): void { - // Enter email - cy.get('ds-log-in [data-test="email"]').type(email); - // Enter password - cy.get('ds-log-in [data-test="password"]').type(password); - // Click login button - cy.get('ds-log-in [data-test="login-button"]').click(); + // Enter email + cy.get('[data-test="email"]').type(email); + // Enter password + cy.get('[data-test="password"]').type(password); + // Click login button + cy.get('[data-test="login-button"]').click(); } // Add as a Cypress command (i.e. assign to 'cy.loginViaForm') Cypress.Commands.add('loginViaForm', loginViaForm); @@ -117,29 +123,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); @@ -153,17 +159,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); diff --git a/cypress/support/e2e.ts b/cypress/support/e2e.ts index b2255a7da6..48985e7911 100644 --- a/cypress/support/e2e.ts +++ b/cypress/support/e2e.ts @@ -15,10 +15,10 @@ // 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 @@ -34,18 +34,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 +54,12 @@ before(() => { // Runs once before the first test in each "block" beforeEach(() => { - // 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}'); + // 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}'); - // 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 diff --git a/cypress/support/utils.ts b/cypress/support/utils.ts index 96575969e8..9a9ea1121b 100644 --- a/cypress/support/utils.ts +++ b/cypress/support/utils.ts @@ -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); }; diff --git a/cypress/tsconfig.json b/cypress/tsconfig.json index 58083003cd..51237b5e95 100644 --- a/cypress/tsconfig.json +++ b/cypress/tsconfig.json @@ -4,10 +4,11 @@ "**/*.ts" ], "compilerOptions": { + "sourceMap": false, "types": [ "cypress", "cypress-axe", "node" ] } -} \ No newline at end of file +} diff --git a/docker/README.md b/docker/README.md index d0cee3f52a..3dc5fd5055 100644 --- a/docker/README.md +++ b/docker/README.md @@ -20,7 +20,7 @@ the Docker compose scripts in this 'docker' folder. ### Dockerfile -This Dockerfile is used to build a *development* DSpace 7 Angular UI image, published as 'dspace/dspace-angular' +This Dockerfile is used to build a *development* DSpace Angular UI image, published as 'dspace/dspace-angular' ``` docker build -t dspace/dspace-angular:latest . @@ -46,11 +46,11 @@ 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 7 REST instance will also be started in Docker. + - Starts DSpace Angular with Docker Compose from the current branch. This file assumes that a DSpace REST instance will also be started in Docker. - docker-compose-rest.yml - - Runs a published instance of the DSpace 7 REST API - persists data in Docker volumes + - Runs a published instance of the DSpace REST API - persists data in Docker volumes - docker-compose-ci.yml - - 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. + - Runs a published instance of the DSpace 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 d7 -f docker/docker-compose.yml -f docker/docker-compose-rest.yml up -d +docker compose -p d8 -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 d7 up -d +docker compose -p d8 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 d7 -f docker/docker-compose.yml up -d +docker compose -p d8 -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 d7 -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 d8 -f docker/docker-compose-dist.yml up -d ``` ## Ingest test data from AIPDIR Create an administrator ``` -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 +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 ``` Load content from AIP files ``` -docker-compose -p d7 -f docker/cli.yml -f ./docker/cli.ingest.yml run --rm dspace-cli +docker compose -p d8 -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 d7 -f docker/docker-compose.yml -f docker/docker-compose-rest.yml -f docker/db.entities.yml up -d +docker compose -p d8 -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 d7 -f docker/cli.yml -f docker/cli.assetstore.yml run --rm dspace-cli +docker compose -p d8 -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 d7ci -f docker/docker-compose-ci.yml up -d +docker compose -p d8ci -f docker/docker-compose-ci.yml up -d ``` diff --git a/docker/cli.assetstore.yml b/docker/cli.assetstore.yml index 31bc53f64d..98f7414861 100644 --- a/docker/cli.assetstore.yml +++ b/docker/cli.assetstore.yml @@ -12,8 +12,6 @@ # https://github.com/DSpace/DSpace/blob/main/dspace/src/main/docker-compose/cli.assetstore.yml # # Therefore, it should be kept in sync with that file -version: "3.7" - services: dspace-cli: environment: diff --git a/docker/cli.ingest.yml b/docker/cli.ingest.yml index 1db241af3b..31563ccc08 100644 --- a/docker/cli.ingest.yml +++ b/docker/cli.ingest.yml @@ -12,8 +12,6 @@ # https://github.com/DSpace/DSpace/blob/main/dspace/src/main/docker-compose/cli.ingest.yml # # Therefore, it should be kept in sync with that file -version: "3.7" - services: dspace-cli: environment: @@ -34,5 +32,7 @@ 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 index-discovery + /dspace/bin/dspace oai import + /dspace/bin/dspace oai clean-cache diff --git a/docker/cli.yml b/docker/cli.yml index cc266b186f..bbb9bd5619 100644 --- a/docker/cli.yml +++ b/docker/cli.yml @@ -12,7 +12,6 @@ # https://github.com/DSpace/DSpace/blob/main/docker-compose-cli.yml # # Therefore, it should be kept in sync with that file -version: "3.7" networks: # Default to using network named 'dspacenet' from docker-compose-rest.yml. # Its full name will be prepended with the project name (e.g. "-p d7" means it will be named "d7_dspacenet") @@ -22,7 +21,7 @@ networks: external: true services: dspace-cli: - image: "${DOCKER_OWNER:-dspace}/dspace-cli:${DSPACE_VER:-latest}" + image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace-cli:${DSPACE_VER:-latest}" container_name: dspace-cli environment: # Below syntax may look odd, but it is how to override dspace.cfg settings via env variables. diff --git a/docker/db.entities.yml b/docker/db.entities.yml index 6473bf2e38..c532d15857 100644 --- a/docker/db.entities.yml +++ b/docker/db.entities.yml @@ -12,11 +12,9 @@ # https://github.com/DSpace/DSpace/blob/main/dspace/src/main/docker-compose/db.entities.yml # # # Therefore, it should be kept in sync with that file -version: "3.7" - services: dspacedb: - image: dspace/dspace-postgres-pgcrypto:loadsql + image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace-postgres-pgcrypto:${DSPACE_VER:-latest}-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 @@ -29,23 +27,11 @@ 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 Tomcat + # 4. Finally, start DSpace entrypoint: - /bin/bash - '-c' - | while (! /dev/null 2>&1; do sleep 1; done; /dspace/bin/dspace database migrate ignored - sed -i '/name-map collection-handle="default".*/a \\n \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - ' /dspace/config/item-submission.xml - catalina.sh run \ No newline at end of file + java -jar /dspace/webapps/server-boot.jar --dspace.dir=/dspace diff --git a/docker/docker-compose-ci.yml b/docker/docker-compose-ci.yml index 07993e20c6..98825605d3 100644 --- a/docker/docker-compose-ci.yml +++ b/docker/docker-compose-ci.yml @@ -10,7 +10,6 @@ # This is used by our GitHub CI at .github/workflows/build.yml # It is based heavily on the Backend's Docker Compose: # https://github.com/DSpace/DSpace/blob/main/docker-compose.yml -version: '3.7' networks: dspacenet: services: @@ -33,7 +32,8 @@ services: # Tell Statistics to commit all views immediately instead of waiting on Solr's autocommit. # This allows us to generate statistics in e2e tests so that statistics pages can be tested thoroughly. solr__D__statistics__P__autoCommit: 'false' - image: "${DOCKER_OWNER:-dspace}/dspace:${DSPACE_VER:-latest-test}" + LOGGING_CONFIG: /dspace/config/log4j2-container.xml + image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace:${DSPACE_VER:-latest-test}" depends_on: - dspacedb networks: @@ -48,27 +48,31 @@ 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 Tomcat + # 3. Finally, start DSpace entrypoint: - /bin/bash - '-c' - | while (! /dev/null 2>&1; do sleep 1; done; /dspace/bin/dspace database migrate ignored - catalina.sh run + java -jar /dspace/webapps/server-boot.jar --dspace.dir=/dspace # 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-pgcrypto:${DSPACE_VER:-latest}-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 - image: dspace/dspace-postgres-pgcrypto:loadsql + POSTGRES_PASSWORD: dspace networks: - dspacenet + ports: + - published: 5432 + target: 5432 stdin_open: true tty: true volumes: @@ -77,7 +81,7 @@ services: # DSpace Solr container dspacesolr: container_name: dspacesolr - image: "${DOCKER_OWNER:-dspace}/dspace-solr:${DSPACE_VER:-latest}" + image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace-solr:${DSPACE_VER:-latest}" networks: - dspacenet ports: @@ -105,6 +109,8 @@ services: cp -r /opt/solr/server/solr/configsets/statistics/* statistics precreate-core qaevent /opt/solr/server/solr/configsets/qaevent cp -r /opt/solr/server/solr/configsets/qaevent/* qaevent + precreate-core suggestion /opt/solr/server/solr/configsets/suggestion + cp -r /opt/solr/server/solr/configsets/suggestion/* suggestion exec solr -f volumes: assetstore: diff --git a/docker/docker-compose-dist.yml b/docker/docker-compose-dist.yml index 38278085cd..5b06faa6a2 100644 --- a/docker/docker-compose-dist.yml +++ b/docker/docker-compose-dist.yml @@ -8,7 +8,6 @@ # Docker Compose for running the DSpace Angular UI dist build # for previewing with the DSpace Demo site backend -version: '3.7' networks: dspacenet: services: @@ -27,7 +26,7 @@ services: DSPACE_REST_HOST: sandbox.dspace.org DSPACE_REST_PORT: 443 DSPACE_REST_NAMESPACE: /server - image: dspace/dspace-angular:${DSPACE_VER:-latest}-dist + image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace-angular:${DSPACE_VER:-latest}-dist" build: context: .. dockerfile: Dockerfile.dist diff --git a/docker/docker-compose-rest.yml b/docker/docker-compose-rest.yml index e1577ec837..e650f09eb5 100644 --- a/docker/docker-compose-rest.yml +++ b/docker/docker-compose-rest.yml @@ -10,7 +10,6 @@ # This is based heavily on the docker-compose.yml that is available in the DSpace/DSpace # (Backend) at: # https://github.com/DSpace/DSpace/blob/main/docker-compose.yml -version: '3.7' networks: dspacenet: ipam: @@ -29,8 +28,9 @@ services: # __D__ => "-" (e.g. google__D__metadata => google-metadata) # dspace.dir, dspace.server.url, dspace.ui.url and dspace.name dspace__P__dir: /dspace - dspace__P__server__P__url: http://localhost:8080/server - dspace__P__ui__P__url: http://localhost:4000 + # Uncomment to set a non-default value for dspace.server.url or dspace.ui.url + # dspace__P__server__P__url: http://localhost:8080/server + # dspace__P__ui__P__url: http://localhost:4000 dspace__P__name: 'DSpace Started with Docker Compose' # db.url: Ensure we are using the 'dspacedb' image for our database db__P__url: 'jdbc:postgresql://dspacedb:5432/dspace' @@ -39,7 +39,8 @@ services: # proxies.trusted.ipranges: This setting is required for a REST API running in Docker to trust requests # from the host machine. This IP range MUST correspond to the 'dspacenet' subnet defined above. proxies__P__trusted__P__ipranges: '172.23.0' - image: "${DOCKER_OWNER:-dspace}/dspace:${DSPACE_VER:-latest-test}" + LOGGING_CONFIG: /dspace/config/log4j2-container.xml + image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace:${DSPACE_VER:-latest-test}" depends_on: - dspacedb networks: @@ -50,24 +51,27 @@ 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 Tomcat + # 3. Finally, start DSpace entrypoint: - /bin/bash - '-c' - | while (! /dev/null 2>&1; do sleep 1; done; /dspace/bin/dspace database migrate - catalina.sh run + java -jar /dspace/webapps/server-boot.jar --dspace.dir=/dspace # DSpace database container dspacedb: container_name: dspacedb + # Uses a custom Postgres image with pgcrypto installed + image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace-postgres-pgcrypto:${DSPACE_VER:-latest}" environment: PGDATA: /pgdata - image: "${DOCKER_OWNER:-dspace}/dspace-postgres-pgcrypto:${DSPACE_VER:-latest}" + POSTGRES_PASSWORD: dspace networks: - dspacenet ports: @@ -81,7 +85,7 @@ services: # DSpace Solr container dspacesolr: container_name: dspacesolr - image: "${DOCKER_OWNER:-dspace}/dspace-solr:${DSPACE_VER:-latest}" + image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace-solr:${DSPACE_VER:-latest}" networks: - dspacenet ports: @@ -97,7 +101,7 @@ services: # * 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` + # `docker compose -p d7 -f docker/docker-compose.yml -f docker/docker-compose-rest.yml up -d --build dspacesolr` entrypoint: - /bin/bash - '-c' @@ -113,6 +117,8 @@ services: cp -r /opt/solr/server/solr/configsets/statistics/* statistics precreate-core qaevent /opt/solr/server/solr/configsets/qaevent cp -r /opt/solr/server/solr/configsets/qaevent/* qaevent + precreate-core suggestion /opt/solr/server/solr/configsets/suggestion + cp -r /opt/solr/server/solr/configsets/suggestion/* suggestion exec solr -f volumes: assetstore: diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 1071b8d6ce..b13ab505f1 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -9,7 +9,6 @@ # Docker Compose for running the DSpace Angular UI for testing/development # Requires also running a REST API backend (either locally or remotely), # for example via 'docker-compose-rest.yml' -version: '3.7' networks: dspacenet: services: @@ -24,7 +23,7 @@ services: DSPACE_REST_HOST: localhost DSPACE_REST_PORT: 8080 DSPACE_REST_NAMESPACE: /server - image: dspace/dspace-angular:${DSPACE_VER:-latest} + image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace-angular:${DSPACE_VER:-latest}" build: context: .. dockerfile: Dockerfile diff --git a/docs/Configuration.md b/docs/Configuration.md index 01fd83c94d..aa6be0f682 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -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 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`. +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`. To change this configuration, change the options `ui.host`, `ui.port` and `ui.ssl` in the appropriate configuration file (see above): diff --git a/docs/lint/html/index.md b/docs/lint/html/index.md new file mode 100644 index 0000000000..15d693843c --- /dev/null +++ b/docs/lint/html/index.md @@ -0,0 +1,4 @@ +[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 diff --git a/docs/lint/html/rules/themed-component-usages.md b/docs/lint/html/rules/themed-component-usages.md new file mode 100644 index 0000000000..a04fe1c770 --- /dev/null +++ b/docs/lint/html/rules/themed-component-usages.md @@ -0,0 +1,110 @@ +[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 + + + +``` + +##### use no-prefix selectors in TypeScript templates + +```html +@Component({ + template: '' +}) +class Test { +} +``` + +##### use no-prefix selectors in TypeScript test templates + +Filename: `lint/test/fixture/src/test.spec.ts` + +```html +@Component({ + template: '' +}) +class Test { +} +``` + +##### base selectors are also allowed in TypeScript test templates + +Filename: `lint/test/fixture/src/test.spec.ts` + +```html +@Component({ + template: '' +}) +class Test { +} +``` + + + + +#### Invalid code & automatic fixes + +##### themed override selectors are not allowed in HTML templates + +```html + + + +``` +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 + + + +``` + + +##### base selectors are not allowed in HTML templates + +```html + + + +``` +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 + + + +``` + + + diff --git a/docs/lint/ts/index.md b/docs/lint/ts/index.md new file mode 100644 index 0000000000..ed060c946e --- /dev/null +++ b/docs/lint/ts/index.md @@ -0,0 +1,6 @@ +[DSpace ESLint plugins](../../../lint/README.md) > TypeScript rules +_______ + +- [`dspace-angular-ts/themed-component-classes`](./rules/themed-component-classes.md): Formatting rules for themeable component classes +- [`dspace-angular-ts/themed-component-selectors`](./rules/themed-component-selectors.md): Themeable component selectors should follow the DSpace convention +- [`dspace-angular-ts/themed-component-usages`](./rules/themed-component-usages.md): Themeable components should be used via their `ThemedComponent` wrapper class diff --git a/docs/lint/ts/rules/themed-component-classes.md b/docs/lint/ts/rules/themed-component-classes.md new file mode 100644 index 0000000000..1f4ec72801 --- /dev/null +++ b/docs/lint/ts/rules/themed-component-classes.md @@ -0,0 +1,257 @@ +[DSpace ESLint plugins](../../../../lint/README.md) > [TypeScript rules](../index.md) > `dspace-angular-ts/themed-component-classes` +_______ + +Formatting rules for themeable component classes + +- All themeable components must be standalone. +- The base component must always be imported in the `ThemedComponent` wrapper. This ensures that it is always sufficient to import just the wrapper whenever we use the component. + + +_______ + +[Source code](../../../../lint/src/rules/ts/themed-component-classes.ts) + +### Examples + + +#### Valid code + +##### Regular non-themeable component + +```typescript +@Component({ + selector: 'ds-something', + standalone: true, +}) +class Something { +} +``` + +##### Base component + +```typescript +@Component({ + selector: 'ds-base-test-themable', + standalone: true, +}) +class TestThemeableTomponent { +} +``` + +##### Wrapper component + +Filename: `lint/test/fixture/src/app/test/themed-test-themeable.component.ts` + +```typescript +@Component({ + selector: 'ds-test-themable', + standalone: true, + imports: [ + TestThemeableComponent, + ], +}) +class ThemedTestThemeableTomponent extends ThemedComponent { +} +``` + +##### Override component + +Filename: `lint/test/fixture/src/themes/test/app/test/test-themeable.component.ts` + +```typescript +@Component({ + selector: 'ds-themed-test-themable', + standalone: true, +}) +class Override extends BaseComponent { +} +``` + + + + +#### Invalid code & automatic fixes + +##### Base component must be standalone + +```typescript +@Component({ + selector: 'ds-base-test-themable', +}) +class TestThemeableComponent { +} +``` +Will produce the following error(s): +``` +Themeable components must be standalone +``` + +Result of `yarn lint --fix`: +```typescript +@Component({ + selector: 'ds-base-test-themable', + standalone: true, +}) +class TestThemeableComponent { +} +``` + + +##### Wrapper component must be standalone and import base component + +Filename: `lint/test/fixture/src/app/test/themed-test-themeable.component.ts` + +```typescript +@Component({ + selector: 'ds-test-themable', +}) +class ThemedTestThemeableComponent extends ThemedComponent { +} +``` +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 { +} +``` + + +##### 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 { +} +``` +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 { +} +``` + + +##### 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 { +} +``` +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 { +} +``` + + +##### 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 { +} +``` +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 { +} +``` + + +##### 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 { +} +``` + + + diff --git a/docs/lint/ts/rules/themed-component-selectors.md b/docs/lint/ts/rules/themed-component-selectors.md new file mode 100644 index 0000000000..f4d0ea177c --- /dev/null +++ b/docs/lint/ts/rules/themed-component-selectors.md @@ -0,0 +1,156 @@ +[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 { +} + +@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 { +} +``` + + + + +#### Invalid code & automatic fixes + +##### Wrong selector for base component + +Filename: `lint/test/fixture/src/app/test/test-themeable.component.ts` + +```typescript +@Component({ + selector: 'ds-something', +}) +class TestThemeableComponent { +} +``` +Will produce the following error(s): +``` +Unthemed version of themeable component should have a selector starting with 'ds-base-' +``` + +Result of `yarn lint --fix`: +```typescript +@Component({ + selector: 'ds-base-something', +}) +class TestThemeableComponent { +} +``` + + +##### Wrong selector for wrapper component + +Filename: `lint/test/fixture/src/app/test/themed-test-themeable.component.ts` + +```typescript +@Component({ + selector: 'ds-themed-something', +}) +class ThemedTestThemeableComponent extends ThemedComponent { +} +``` +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 { +} +``` + + +##### 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 { +} +``` + + + diff --git a/docs/lint/ts/rules/themed-component-usages.md b/docs/lint/ts/rules/themed-component-usages.md new file mode 100644 index 0000000000..16ccb701c2 --- /dev/null +++ b/docs/lint/ts/rules/themed-component-usages.md @@ -0,0 +1,332 @@ +[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 { +} +``` + +##### allow base class in ViewChild + +```typescript +import { TestThemeableComponent } from './app/test/test-themeable.component'; + +export class Something { + @ViewChild(TestThemeableComponent) test: TestThemeableComponent; +} +``` + +##### allow wrapper selectors in test queries + +Filename: `lint/test/fixture/src/app/test/test.component.spec.ts` + +```typescript +By.css('ds-themeable'); +By.css('#test > ds-themeable > #nest'); +``` + +##### allow wrapper selectors in cypress queries + +Filename: `lint/test/fixture/src/app/test/test.component.cy.ts` + +```typescript +By.css('ds-themeable'); +By.css('#test > ds-themeable > #nest'); +``` + + + + +#### Invalid code & automatic fixes + +##### disallow direct usages of base class + +```typescript +import { TestThemeableComponent } from './app/test/test-themeable.component'; +import { TestComponent } from './app/test/test.component'; + +const config = { + a: TestThemeableComponent, + b: TestComponent, +} +``` +Will produce the following error(s): +``` +Themeable components should be used via their ThemedComponent wrapper +Themeable components should be used via their ThemedComponent wrapper +``` + +Result of `yarn lint --fix`: +```typescript +import { ThemedTestThemeableComponent } from './app/test/themed-test-themeable.component'; +import { TestComponent } from './app/test/test.component'; + +const config = { + a: ThemedTestThemeableComponent, + b: TestComponent, +} +``` + + +##### disallow direct usages of base class, keep other imports + +```typescript +import { Something, TestThemeableComponent } from './app/test/test-themeable.component'; +import { TestComponent } from './app/test/test.component'; + +const config = { + a: TestThemeableComponent, + b: TestComponent, + c: Something, +} +``` +Will produce the following error(s): +``` +Themeable components should be used via their ThemedComponent wrapper +Themeable components should be used via their ThemedComponent wrapper +``` + +Result of `yarn lint --fix`: +```typescript +import { Something } from './app/test/test-themeable.component'; +import { ThemedTestThemeableComponent } from './app/test/themed-test-themeable.component'; +import { TestComponent } from './app/test/test.component'; + +const config = { + a: ThemedTestThemeableComponent, + b: TestComponent, + c: Something, +} +``` + + +##### handle array replacements correctly + +```typescript +const DECLARATIONS = [ + Something, + TestThemeableComponent, + Something, + ThemedTestThemeableComponent, +]; +``` +Will produce the following error(s): +``` +Themeable components should be used via their ThemedComponent wrapper +``` + +Result of `yarn lint --fix`: +```typescript +const DECLARATIONS = [ + Something, + Something, + ThemedTestThemeableComponent, +]; +``` + + +##### disallow override selector in test queries + +Filename: `lint/test/fixture/src/app/test/test.component.spec.ts` + +```typescript +By.css('ds-themed-themeable'); +By.css('#test > ds-themed-themeable > #nest'); +``` +Will produce the following error(s): +``` +Themeable components should be used via their ThemedComponent wrapper +Themeable components should be used via their ThemedComponent wrapper +``` + +Result of `yarn lint --fix`: +```typescript +By.css('ds-themeable'); +By.css('#test > ds-themeable > #nest'); +``` + + +##### disallow base selector in test queries + +Filename: `lint/test/fixture/src/app/test/test.component.spec.ts` + +```typescript +By.css('ds-base-themeable'); +By.css('#test > ds-base-themeable > #nest'); +``` +Will produce the following error(s): +``` +Themeable components should be used via their ThemedComponent wrapper +Themeable components should be used via their ThemedComponent wrapper +``` + +Result of `yarn lint --fix`: +```typescript +By.css('ds-themeable'); +By.css('#test > ds-themeable > #nest'); +``` + + +##### disallow override selector in cypress queries + +Filename: `lint/test/fixture/src/app/test/test.component.cy.ts` + +```typescript +cy.get('ds-themed-themeable'); +cy.get('#test > ds-themed-themeable > #nest'); +``` +Will produce the following error(s): +``` +Themeable components should be used via their ThemedComponent wrapper +Themeable components should be used via their ThemedComponent wrapper +``` + +Result of `yarn lint --fix`: +```typescript +cy.get('ds-themeable'); +cy.get('#test > ds-themeable > #nest'); +``` + + +##### disallow base selector in cypress queries + +Filename: `lint/test/fixture/src/app/test/test.component.cy.ts` + +```typescript +cy.get('ds-base-themeable'); +cy.get('#test > ds-base-themeable > #nest'); +``` +Will produce the following error(s): +``` +Themeable components should be used via their ThemedComponent wrapper +Themeable components should be used via their ThemedComponent wrapper +``` + +Result of `yarn lint --fix`: +```typescript +cy.get('ds-themeable'); +cy.get('#test > ds-themeable > #nest'); +``` + + +##### edge case: unable to find usage node through usage token, but import is still flagged and fixed + +Filename: `lint/test/fixture/src/themes/test/app/test/other-themeable.component.ts` + +```typescript +import { Component } from '@angular/core'; + +import { Context } from './app/core/shared/context.model'; +import { TestThemeableComponent } from '../../../../app/test/test-themeable.component'; + +@Component({ + standalone: true, + imports: [TestThemeableComponent], +}) +export class UsageComponent { +} +``` +Will produce the following error(s): +``` +Themeable components should be used via their ThemedComponent wrapper +Themeable components should be used via their ThemedComponent wrapper +``` + +Result of `yarn lint --fix`: +```typescript +import { Component } from '@angular/core'; + +import { Context } from './app/core/shared/context.model'; +import { ThemedTestThemeableComponent } from '../../../../app/test/themed-test-themeable.component'; + +@Component({ + standalone: true, + imports: [ThemedTestThemeableComponent], +}) +export class UsageComponent { +} +``` + + +##### edge case edge case: both are imported, only wrapper is retained + +Filename: `lint/test/fixture/src/themes/test/app/test/other-themeable.component.ts` + +```typescript +import { Component } from '@angular/core'; + +import { Context } from './app/core/shared/context.model'; +import { TestThemeableComponent } from '../../../../app/test/test-themeable.component'; +import { ThemedTestThemeableComponent } from '../../../../app/test/themed-test-themeable.component'; + +@Component({ + standalone: true, + imports: [TestThemeableComponent, ThemedTestThemeableComponent], +}) +export class UsageComponent { +} +``` +Will produce the following error(s): +``` +Themeable components should be used via their ThemedComponent wrapper +Themeable components should be used via their ThemedComponent wrapper +``` + +Result of `yarn lint --fix`: +```typescript +import { Component } from '@angular/core'; + +import { Context } from './app/core/shared/context.model'; +import { ThemedTestThemeableComponent } from '../../../../app/test/themed-test-themeable.component'; + +@Component({ + standalone: true, + imports: [ThemedTestThemeableComponent], +}) +export class UsageComponent { +} +``` + + + diff --git a/lint/.gitignore b/lint/.gitignore new file mode 100644 index 0000000000..0d22081b3b --- /dev/null +++ b/lint/.gitignore @@ -0,0 +1,3 @@ +/dist/ +/coverage/ +/node-modules/ diff --git a/lint/README.md b/lint/README.md new file mode 100644 index 0000000000..7251a35c06 --- /dev/null +++ b/lint/README.md @@ -0,0 +1,50 @@ +# 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) \ No newline at end of file diff --git a/lint/dist/src/rules/html/package.json b/lint/dist/src/rules/html/package.json new file mode 100644 index 0000000000..d3f310d23b --- /dev/null +++ b/lint/dist/src/rules/html/package.json @@ -0,0 +1,6 @@ +{ + "name": "eslint-plugin-dspace-angular-html", + "version": "0.0.0", + "main": "./index.js", + "private": true +} diff --git a/lint/dist/src/rules/ts/package.json b/lint/dist/src/rules/ts/package.json new file mode 100644 index 0000000000..f19e18756a --- /dev/null +++ b/lint/dist/src/rules/ts/package.json @@ -0,0 +1,6 @@ +{ + "name": "eslint-plugin-dspace-angular-ts", + "version": "0.0.0", + "main": "./index.js", + "private": true +} diff --git a/lint/generate-docs.ts b/lint/generate-docs.ts new file mode 100644 index 0000000000..fb2bf53fb5 --- /dev/null +++ b/lint/generate-docs.ts @@ -0,0 +1,85 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ + +import { + existsSync, + mkdirSync, + readFileSync, + rmSync, + writeFileSync, +} from 'fs'; +import { join } from 'path'; + +import { default as htmlPlugin } from './src/rules/html'; +import { default as tsPlugin } from './src/rules/ts'; + +const templates = new Map(); + +function lazyEJS(path: string, data: object): string { + if (!templates.has(path)) { + templates.set(path, require('ejs').compile(readFileSync(path).toString())); + } + + return templates.get(path)(data).replace(/\r\n/g, '\n'); +} + +const docsDir = join('docs', 'lint'); +const tsDir = join(docsDir, 'ts'); +const htmlDir = join(docsDir, 'html'); + +if (existsSync(docsDir)) { + rmSync(docsDir, { recursive: true }); +} + +mkdirSync(join(tsDir, 'rules'), { recursive: true }); +mkdirSync(join(htmlDir, 'rules'), { recursive: true }); + +function template(name: string): string { + return join('lint', 'src', 'util', 'templates', name); +} + +// TypeScript docs +writeFileSync( + join(tsDir, 'index.md'), + lazyEJS(template('index.ejs'), { + plugin: tsPlugin, + rules: tsPlugin.index.map(rule => rule.info), + }), +); + +for (const rule of tsPlugin.index) { + writeFileSync( + join(tsDir, 'rules', rule.info.name + '.md'), + lazyEJS(template('rule.ejs'), { + plugin: tsPlugin, + rule: rule.info, + tests: rule.tests, + }), + ); +} + +// HTML docs +writeFileSync( + join(htmlDir, 'index.md'), + lazyEJS(template('index.ejs'), { + plugin: htmlPlugin, + rules: htmlPlugin.index.map(rule => rule.info), + }), +); + +for (const rule of htmlPlugin.index) { + writeFileSync( + join(htmlDir, 'rules', rule.info.name + '.md'), + lazyEJS(template('rule.ejs'), { + plugin: htmlPlugin, + rule: rule.info, + tests: rule.tests, + }), + ); +} + diff --git a/lint/jasmine.json b/lint/jasmine.json new file mode 100644 index 0000000000..dfacd41a96 --- /dev/null +++ b/lint/jasmine.json @@ -0,0 +1,7 @@ +{ + "spec_files": ["**/*.spec.js"], + "spec_dir": "lint/dist/test", + "helpers": [ + "./test/helpers.js" + ] +} diff --git a/lint/src/rules/html/index.ts b/lint/src/rules/html/index.ts new file mode 100644 index 0000000000..7c1370ae2d --- /dev/null +++ b/lint/src/rules/html/index.ts @@ -0,0 +1,22 @@ +/** + * 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 themedComponentUsages from './themed-component-usages'; + +const index = [ + themedComponentUsages, +] as unknown as RuleExports[]; + +export = { + parser: require('@angular-eslint/template-parser'), + ...bundle('dspace-angular-html', 'HTML', index), +}; diff --git a/lint/src/rules/html/themed-component-usages.ts b/lint/src/rules/html/themed-component-usages.ts new file mode 100644 index 0000000000..e907285dbc --- /dev/null +++ b/lint/src/rules/html/themed-component-usages.ts @@ -0,0 +1,189 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +import { TmplAstElement } from '@angular-eslint/bundled-angular-compiler'; +import { TemplateParserServices } from '@angular-eslint/utils'; +import { ESLintUtils } from '@typescript-eslint/utils'; +import { RuleContext } from '@typescript-eslint/utils/ts-eslint'; + +import { fixture } from '../../../test/fixture'; +import { + DSpaceESLintRuleInfo, + NamedTests, +} from '../../util/structure'; +import { + DISALLOWED_THEME_SELECTORS, + fixSelectors, +} from '../../util/theme-support'; +import { + getFilename, + getSourceCode, +} from '../../util/typescript'; + +export enum Message { + WRONG_SELECTOR = 'mustUseThemedWrapperSelector', +} + +export const info = { + name: 'themed-component-usages', + meta: { + docs: { + description: `Themeable components should be used via the selector of their \`ThemedComponent\` wrapper class + +This ensures that custom themes can correctly override _all_ instances of this component. +The only exception to this rule are unit tests, where we may want to use the base component in order to keep the test setup simple. + `, + }, + type: 'problem', + fixable: 'code', + schema: [], + messages: { + [Message.WRONG_SELECTOR]: 'Themeable components should be used via their ThemedComponent wrapper\'s selector', + }, + }, + defaultOptions: [], +} as DSpaceESLintRuleInfo; + +export const rule = ESLintUtils.RuleCreator.withoutDocs({ + ...info, + create(context: RuleContext) { + 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: ` + + + + `, + }, + { + name: 'use no-prefix selectors in TypeScript templates', + code: ` +@Component({ + template: '' +}) +class Test { +} + `, + }, + { + name: 'use no-prefix selectors in TypeScript test templates', + filename: fixture('src/test.spec.ts'), + code: ` +@Component({ + template: '' +}) +class Test { +} + `, + }, + { + name: 'base selectors are also allowed in TypeScript test templates', + filename: fixture('src/test.spec.ts'), + code: ` +@Component({ + template: '' +}) +class Test { +} + `, + }, + ], + invalid: [ + { + name: 'themed override selectors are not allowed in HTML templates', + code: ` + + + + `, + errors: [ + { + messageId: Message.WRONG_SELECTOR, + }, + { + messageId: Message.WRONG_SELECTOR, + }, + { + messageId: Message.WRONG_SELECTOR, + }, + ], + output: ` + + + + `, + }, + { + name: 'base selectors are not allowed in HTML templates', + code: ` + + + + `, + errors: [ + { + messageId: Message.WRONG_SELECTOR, + }, + { + messageId: Message.WRONG_SELECTOR, + }, + { + messageId: Message.WRONG_SELECTOR, + }, + ], + output: ` + + + + `, + }, + ], +} as NamedTests; + +export default rule; diff --git a/lint/src/rules/ts/index.ts b/lint/src/rules/ts/index.ts new file mode 100644 index 0000000000..a7fdfe41ef --- /dev/null +++ b/lint/src/rules/ts/index.ts @@ -0,0 +1,25 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +import { + bundle, + RuleExports, +} from '../../util/structure'; +/* eslint-disable import/no-namespace */ +import * as themedComponentClasses from './themed-component-classes'; +import * as themedComponentSelectors from './themed-component-selectors'; +import * as themedComponentUsages from './themed-component-usages'; + +const index = [ + themedComponentClasses, + themedComponentSelectors, + themedComponentUsages, +] as unknown as RuleExports[]; + +export = { + ...bundle('dspace-angular-ts', 'TypeScript', index), +}; diff --git a/lint/src/rules/ts/themed-component-classes.ts b/lint/src/rules/ts/themed-component-classes.ts new file mode 100644 index 0000000000..527655adfa --- /dev/null +++ b/lint/src/rules/ts/themed-component-classes.ts @@ -0,0 +1,382 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +import { + ESLintUtils, + TSESTree, +} from '@typescript-eslint/utils'; +import { RuleContext } from '@typescript-eslint/utils/ts-eslint'; + +import { fixture } from '../../../test/fixture'; +import { + getComponentImportNode, + getComponentInitializer, + getComponentStandaloneNode, +} from '../../util/angular'; +import { appendObjectProperties } from '../../util/fix'; +import { DSpaceESLintRuleInfo } from '../../util/structure'; +import { + getBaseComponentClassName, + inThemedComponentOverrideFile, + isThemeableComponent, + isThemedComponentWrapper, +} from '../../util/theme-support'; +import { getFilename } from '../../util/typescript'; + +export enum Message { + NOT_STANDALONE = 'mustBeStandalone', + NOT_STANDALONE_IMPORTS_BASE = 'mustBeStandaloneAndImportBase', + WRAPPER_IMPORTS_BASE = 'wrapperShouldImportBase', +} + +export const info = { + name: 'themed-component-classes', + meta: { + docs: { + description: `Formatting rules for themeable component classes + +- All themeable components must be standalone. +- The base component must always be imported in the \`ThemedComponent\` wrapper. This ensures that it is always sufficient to import just the wrapper whenever we use the component. + `, + }, + type: 'problem', + fixable: 'code', + schema: [], + messages: { + [Message.NOT_STANDALONE]: 'Themeable components must be standalone', + [Message.NOT_STANDALONE_IMPORTS_BASE]: 'Themeable component wrapper classes must be standalone and import the base class', + [Message.WRAPPER_IMPORTS_BASE]: 'Themed component wrapper classes must only import the base class', + }, + }, + defaultOptions: [], +} as DSpaceESLintRuleInfo; + +export const rule = ESLintUtils.RuleCreator.withoutDocs({ + ...info, + create(context: RuleContext) { + const filename = getFilename(context); + + if (filename.endsWith('.spec.ts')) { + return {}; + } + + function enforceStandalone(decoratorNode: TSESTree.Decorator, withBaseImport = false) { + const standaloneNode = getComponentStandaloneNode(decoratorNode); + + if (standaloneNode === undefined) { + // We may need to add these properties in one go + if (!withBaseImport) { + context.report({ + messageId: Message.NOT_STANDALONE, + node: decoratorNode, + fix(fixer) { + const initializer = getComponentInitializer(decoratorNode); + return appendObjectProperties(context, fixer, initializer, ['standalone: true']); + }, + }); + } + } else if (!standaloneNode.value) { + context.report({ + messageId: Message.NOT_STANDALONE, + node: standaloneNode, + fix(fixer) { + return fixer.replaceText(standaloneNode, 'true'); + }, + }); + } + + if (withBaseImport) { + const baseClass = getBaseComponentClassName(decoratorNode); + + if (baseClass === undefined) { + return; + } + + const importsNode = getComponentImportNode(decoratorNode); + + if (importsNode === undefined) { + if (standaloneNode === undefined) { + context.report({ + messageId: Message.NOT_STANDALONE_IMPORTS_BASE, + node: decoratorNode, + fix(fixer) { + const initializer = getComponentInitializer(decoratorNode); + return appendObjectProperties(context, fixer, initializer, ['standalone: true', `imports: [${baseClass}]`]); + }, + }); + } else { + context.report({ + messageId: Message.WRAPPER_IMPORTS_BASE, + node: decoratorNode, + fix(fixer) { + const initializer = getComponentInitializer(decoratorNode); + return appendObjectProperties(context, fixer, initializer, [`imports: [${baseClass}]`]); + }, + }); + } + } else { + // If we have an imports node, standalone: true will be enforced by another rule + + const imports = importsNode.elements.map(e => (e as TSESTree.Identifier).name); + + if (!imports.includes(baseClass) || imports.length > 1) { + // The wrapper should _only_ import the base component + context.report({ + messageId: Message.WRAPPER_IMPORTS_BASE, + node: importsNode, + fix(fixer) { + // todo: this may leave unused imports, but that's better than mangling things + return fixer.replaceText(importsNode, `[${baseClass}]`); + }, + }); + } + } + } + } + + return { + 'ClassDeclaration > Decorator[expression.callee.name = "Component"]'(node: TSESTree.Decorator) { + const classNode = node.parent as TSESTree.ClassDeclaration; + const className = classNode.id?.name; + + if (className === undefined) { + return; + } + + if (isThemedComponentWrapper(node)) { + enforceStandalone(node, true); + } else if (inThemedComponentOverrideFile(filename)) { + enforceStandalone(node); + } else if (isThemeableComponent(className)) { + enforceStandalone(node); + } + }, + }; + }, +}); + +export const tests = { + plugin: info.name, + valid: [ + { + name: 'Regular non-themeable component', + code: ` +@Component({ + selector: 'ds-something', + standalone: true, +}) +class Something { +} + `, + }, + { + name: 'Base component', + code: ` +@Component({ + selector: 'ds-base-test-themable', + standalone: true, +}) +class TestThemeableTomponent { +} + `, + }, + { + name: 'Wrapper component', + filename: fixture('src/app/test/themed-test-themeable.component.ts'), + code: ` +@Component({ + selector: 'ds-test-themable', + standalone: true, + imports: [ + TestThemeableComponent, + ], +}) +class ThemedTestThemeableTomponent extends ThemedComponent { +} + `, + }, + { + 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 { +} + `, + errors:[ + { + messageId: Message.NOT_STANDALONE_IMPORTS_BASE, + }, + ], + output: ` +@Component({ + selector: 'ds-test-themable', + standalone: true, + imports: [TestThemeableComponent], +}) +class ThemedTestThemeableComponent extends ThemedComponent { +} + `, + }, + + { + 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 { +} + `, + errors:[ + { + messageId: Message.WRAPPER_IMPORTS_BASE, + }, + ], + output: ` +@Component({ + selector: 'ds-test-themable', + standalone: true, + imports: [TestThemeableComponent], +}) +class ThemedTestThemeableComponent extends ThemedComponent { +} + `, + }, + { + 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 { +} + `, + 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 { +} + `, + }, { + 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 { +} + `, + 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 { +} + `, + }, + { + 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 { +} + `, + }, + ], +}; diff --git a/lint/src/rules/ts/themed-component-selectors.ts b/lint/src/rules/ts/themed-component-selectors.ts new file mode 100644 index 0000000000..c27fd66d66 --- /dev/null +++ b/lint/src/rules/ts/themed-component-selectors.ts @@ -0,0 +1,257 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +import { + ESLintUtils, + TSESTree, +} from '@typescript-eslint/utils'; +import { RuleContext } from '@typescript-eslint/utils/ts-eslint'; + +import { fixture } from '../../../test/fixture'; +import { getComponentSelectorNode } from '../../util/angular'; +import { stringLiteral } from '../../util/misc'; +import { DSpaceESLintRuleInfo } from '../../util/structure'; +import { + inThemedComponentOverrideFile, + isThemeableComponent, + isThemedComponentWrapper, +} from '../../util/theme-support'; +import { getFilename } from '../../util/typescript'; + +export enum Message { + BASE = 'wrongSelectorUnthemedComponent', + WRAPPER = 'wrongSelectorThemedComponentWrapper', + THEMED = 'wrongSelectorThemedComponentOverride', +} + +export const info = { + name: 'themed-component-selectors', + meta: { + docs: { + description: `Themeable component selectors should follow the DSpace convention + +Each themeable component is comprised of a base component, a wrapper component and any number of themed components +- Base components should have a selector starting with \`ds-base-\` +- Themed components should have a selector starting with \`ds-themed-\` +- Wrapper components should have a selector starting with \`ds-\`, but not \`ds-base-\` or \`ds-themed-\` + - This is the regular DSpace selector prefix + - **When making a regular component themeable, its selector prefix should be changed to \`ds-base-\`, and the new wrapper's component should reuse the previous selector** + +Unit tests are exempt from this rule, because they may redefine components using the same class name as other themeable components elsewhere in the source. + `, + }, + type: 'problem', + schema: [], + fixable: 'code', + messages: { + [Message.BASE]: 'Unthemed version of themeable component should have a selector starting with \'ds-base-\'', + [Message.WRAPPER]: 'Themed component wrapper of themeable component shouldn\'t have a selector starting with \'ds-themed-\'', + [Message.THEMED]: 'Theme override of themeable component should have a selector starting with \'ds-themed-\'', + }, + }, + defaultOptions: [], +} as DSpaceESLintRuleInfo; + +export const rule = ESLintUtils.RuleCreator.withoutDocs({ + ...info, + create(context: RuleContext) { + 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 { +} + +@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 { +} + `, + }, + ], + 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 { +} + `, + errors: [ + { + messageId: Message.WRAPPER, + }, + ], + output: ` +@Component({ + selector: 'ds-something', +}) +class ThemedTestThemeableComponent extends ThemedComponent { +} + `, + }, + { + 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; diff --git a/lint/src/rules/ts/themed-component-usages.ts b/lint/src/rules/ts/themed-component-usages.ts new file mode 100644 index 0000000000..83fe6f8ea8 --- /dev/null +++ b/lint/src/rules/ts/themed-component-usages.ts @@ -0,0 +1,502 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +import { + ESLintUtils, + TSESTree, +} from '@typescript-eslint/utils'; +import { RuleContext } from '@typescript-eslint/utils/ts-eslint'; + +import { fixture } from '../../../test/fixture'; +import { + removeWithCommas, + replaceOrRemoveArrayIdentifier, +} from '../../util/fix'; +import { DSpaceESLintRuleInfo } from '../../util/structure'; +import { + allThemeableComponents, + DISALLOWED_THEME_SELECTORS, + fixSelectors, + getThemeableComponentByBaseClass, + isAllowedUnthemedUsage, +} from '../../util/theme-support'; +import { + findImportSpecifier, + findUsages, + findUsagesByName, + getFilename, + relativePath, +} from '../../util/typescript'; + +export enum Message { + WRONG_CLASS = 'mustUseThemedWrapperClass', + WRONG_IMPORT = 'mustImportThemedWrapper', + WRONG_SELECTOR = 'mustUseThemedWrapperSelector', + BASE_IN_MODULE = 'baseComponentNotNeededInModule', +} + +export const info = { + name: 'themed-component-usages', + meta: { + docs: { + description: `Themeable components should be used via their \`ThemedComponent\` wrapper class + +This ensures that custom themes can correctly override _all_ instances of this component. +There are a few exceptions where the base class can still be used: +- Class declaration expressions (otherwise we can't declare, extend or override the class in the first place) +- Angular modules (except for routing modules) +- Angular \`@ViewChild\` decorators +- Type annotations + `, + }, + type: 'problem', + schema: [], + fixable: 'code', + messages: { + [Message.WRONG_CLASS]: 'Themeable components should be used via their ThemedComponent wrapper', + [Message.WRONG_IMPORT]: 'Themeable components should be used via their ThemedComponent wrapper', + [Message.WRONG_SELECTOR]: 'Themeable components should be used via their ThemedComponent wrapper', + [Message.BASE_IN_MODULE]: 'Base themeable components shouldn\'t be declared in modules', + }, + }, + defaultOptions: [], +} as DSpaceESLintRuleInfo; + +export const rule = ESLintUtils.RuleCreator.withoutDocs({ + ...info, + create(context: RuleContext) { + 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 { +} + `, + }, + { + 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; diff --git a/lint/src/util/angular.ts b/lint/src/util/angular.ts new file mode 100644 index 0000000000..70ee903fb8 --- /dev/null +++ b/lint/src/util/angular.ts @@ -0,0 +1,83 @@ +/** + * 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'; +} diff --git a/lint/src/util/fix.ts b/lint/src/util/fix.ts new file mode 100644 index 0000000000..10408cc316 --- /dev/null +++ b/lint/src/util/fix.ts @@ -0,0 +1,125 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +import { TSESTree } from '@typescript-eslint/utils'; +import { + RuleContext, + RuleFix, + RuleFixer, +} from '@typescript-eslint/utils/ts-eslint'; + +import { getSourceCode } from './typescript'; + + + +export function appendObjectProperties(context: RuleContext, fixer: RuleFixer, objectNode: TSESTree.ObjectExpression, properties: string[]): RuleFix { + // todo: may not handle empty objects too well + const lastProperty = objectNode.properties[objectNode.properties.length - 1]; + const source = getSourceCode(context); + const nextToken = source.getTokenAfter(lastProperty); + + // todo: newline & indentation are hardcoded for @Component({}) + // todo: we're assuming that we need trailing commas, what if we don't? + const newPart = '\n' + properties.map(p => ` ${p},`).join('\n'); + + if (nextToken !== null && nextToken.value === ',') { + return fixer.insertTextAfter(nextToken, newPart); + } else { + return fixer.insertTextAfter(lastProperty, ',' + newPart); + } +} + +export function appendArrayElement(context: RuleContext, fixer: RuleFixer, arrayNode: TSESTree.ArrayExpression, value: string): RuleFix { + const source = getSourceCode(context); + + if (arrayNode.elements.length === 0) { + // This is the first element + const openArray = source.getTokenByRangeStart(arrayNode.range[0]); + + if (openArray == null) { + throw new Error('Unexpected null token for opening square bracket'); + } + + // safe to assume the list is single-line + return fixer.insertTextAfter(openArray, `${value}`); + } else { + const lastElement = arrayNode.elements[arrayNode.elements.length - 1]; + + if (lastElement == null) { + throw new Error('Unexpected null node in array'); + } + + const nextToken = source.getTokenAfter(lastElement); + + // todo: we don't know if the list is chopped or not, so we can't make any assumptions -- may produce output that will be flagged by other rules on the next run! + // todo: we're assuming that we need trailing commas, what if we don't? + if (nextToken !== null && nextToken.value === ',') { + return fixer.insertTextAfter(nextToken, ` ${value},`); + } else { + return fixer.insertTextAfter(lastElement, `, ${value},`); + } + } + +} + +export function isLast(elementNode: TSESTree.Node): boolean { + if (!elementNode.parent) { + return false; + } + + let siblingNodes: (TSESTree.Node | null)[] = [null]; + if (elementNode.parent.type === TSESTree.AST_NODE_TYPES.ArrayExpression) { + siblingNodes = elementNode.parent.elements; + } else if (elementNode.parent.type === TSESTree.AST_NODE_TYPES.ImportDeclaration) { + siblingNodes = elementNode.parent.specifiers; + } + + return elementNode === siblingNodes[siblingNodes.length - 1]; +} + +export function removeWithCommas(context: RuleContext, fixer: RuleFixer, elementNode: TSESTree.Node): RuleFix[] { + const ops = []; + + const source = getSourceCode(context); + let nextToken = source.getTokenAfter(elementNode); + let prevToken = source.getTokenBefore(elementNode); + + if (nextToken !== null && prevToken !== null) { + if (nextToken.value === ',') { + nextToken = source.getTokenAfter(nextToken); + if (nextToken !== null) { + ops.push(fixer.removeRange([elementNode.range[0], nextToken.range[0]])); + } + } + if (isLast(elementNode) && prevToken.value === ',') { + prevToken = source.getTokenBefore(prevToken); + if (prevToken !== null) { + ops.push(fixer.removeRange([prevToken.range[1], elementNode.range[1]])); + } + } + } else if (nextToken !== null) { + ops.push(fixer.removeRange([elementNode.range[0], nextToken.range[0]])); + } + + return ops; +} + +export function replaceOrRemoveArrayIdentifier(context: RuleContext, fixer: RuleFixer, identifierNode: TSESTree.Identifier, newValue: string): RuleFix[] { + if (identifierNode.parent.type !== TSESTree.AST_NODE_TYPES.ArrayExpression) { + throw new Error('Parent node is not an array expression!'); + } + + const array = identifierNode.parent as TSESTree.ArrayExpression; + + for (const element of array.elements) { + if (element !== null && element.type === TSESTree.AST_NODE_TYPES.Identifier && element.name === newValue) { + return removeWithCommas(context, fixer, identifierNode); + } + } + + return [fixer.replaceText(identifierNode, newValue)]; +} diff --git a/lint/src/util/misc.ts b/lint/src/util/misc.ts new file mode 100644 index 0000000000..49cb60124e --- /dev/null +++ b/lint/src/util/misc.ts @@ -0,0 +1,28 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ + +export function match(rangeA: number[], rangeB: number[]) { + return rangeA[0] === rangeB[0] && rangeA[1] === rangeB[1]; +} + + +export function stringLiteral(value: string): string { + return `'${value}'`; +} + +/** + * Transform Windows-style paths into Unix-style paths + */ +export function toUnixStylePath(path: string): string { + // note: we're assuming that none of the directory/file names contain '\' or '/' characters. + // using these characters in paths is very bad practice in general, so this should be a safe assumption. + if (path.includes('\\')) { + return path.replace(/^[A-Z]:\\/, '/').replaceAll('\\', '/'); + } + return path; +} diff --git a/lint/src/util/structure.ts b/lint/src/util/structure.ts new file mode 100644 index 0000000000..2e3aebd9ab --- /dev/null +++ b/lint/src/util/structure.ts @@ -0,0 +1,61 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +import { + InvalidTestCase, + RuleMetaData, + RuleModule, + ValidTestCase, +} from '@typescript-eslint/utils/ts-eslint'; +import { EnumType } from 'typescript'; + +export type Meta = RuleMetaData; +export type Valid = ValidTestCase; +export type Invalid = InvalidTestCase; + +export interface DSpaceESLintRuleInfo { + name: string; + meta: Meta, + defaultOptions: unknown[], +} + +export interface NamedTests { + plugin: string; + valid: Valid[]; + invalid: Invalid[]; +} + +export interface RuleExports { + Message: EnumType, + info: DSpaceESLintRuleInfo, + rule: RuleModule, + tests: NamedTests, + default: unknown, +} + +export interface PluginExports { + name: string, + language: string, + rules: Record, + index: RuleExports[], +} + +export function bundle( + name: string, + language: string, + index: RuleExports[], +): PluginExports { + return index.reduce((o: PluginExports, i: RuleExports) => { + o.rules[i.info.name] = i.rule; + return o; + }, { + name, + language, + rules: {}, + index, + }); +} diff --git a/lint/src/util/templates/index.ejs b/lint/src/util/templates/index.ejs new file mode 100644 index 0000000000..d959f29291 --- /dev/null +++ b/lint/src/util/templates/index.ejs @@ -0,0 +1,5 @@ +[DSpace ESLint plugins](../../../lint/README.md) > <%= plugin.language %> rules +_______ +<% rules.forEach(rule => { %> +- [`<%= plugin.name %>/<%= rule.name %>`](./rules/<%= rule.name %>.md)<% if (rule.meta?.docs?.description) {%>: <%= rule.meta.docs.description.split('\n')[0].trim() -%><% }-%> +<% }) %> diff --git a/lint/src/util/templates/rule.ejs b/lint/src/util/templates/rule.ejs new file mode 100644 index 0000000000..b39d193cc1 --- /dev/null +++ b/lint/src/util/templates/rule.ejs @@ -0,0 +1,48 @@ +[DSpace ESLint plugins](../../../../lint/README.md) > [<%= plugin.language %> rules](../index.md) > `<%= plugin.name %>/<%= rule.name %>` +_______ + +<%- rule.meta.docs?.description %> + +_______ + +[Source code](../../../../lint/src/rules/<%- plugin.name.replace('dspace-angular-', '') %>/<%- rule.name %>.ts) + +### Examples + +<% if (tests.valid) {%> +#### Valid code + <% tests.valid.forEach(test => { %> +##### <%= test.name !== undefined ? test.name : 'UNNAMED' %> + <% if (test.filename) { %> +Filename: `<%- test.filename %>` + <% } %> +```<%- plugin.language.toLowerCase() %> +<%- test.code.trim() %> +``` + <% }) %> +<% } %> + +<% if (tests.invalid) {%> +#### Invalid code <%= rule.meta.fixable ? ' & automatic fixes' : '' %> + <% tests.invalid.forEach(test => { %> +##### <%= test.name !== undefined ? test.name : 'UNNAMED' %> + <% if (test.filename) { %> +Filename: `<%- test.filename %>` + <% } %> +```<%- plugin.language.toLowerCase() %> +<%- test.code.trim() %> +``` +Will produce the following error(s): +``` +<% for (const error of test.errors) { -%> +<%- rule.meta.messages[error.messageId] %> +<% } -%> +``` + <% if (test.output) { %> +Result of `yarn lint --fix`: +```<%- plugin.language.toLowerCase() %> +<%- test.output.trim() %> +``` + <% } %> + <% }) %> +<% } %> diff --git a/lint/src/util/theme-support.ts b/lint/src/util/theme-support.ts new file mode 100644 index 0000000000..64644145fa --- /dev/null +++ b/lint/src/util/theme-support.ts @@ -0,0 +1,265 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ + +import { TSESTree } from '@typescript-eslint/utils'; +import { readFileSync } from 'fs'; +import { basename } from 'path'; +import ts, { Identifier } from 'typescript'; + +import { + getComponentClassName, + isPartOfViewChild, +} from './angular'; +import { + isPartOfClassDeclaration, + isPartOfTypeExpression, +} from './typescript'; + +/** + * Couples a themeable Component to its ThemedComponent wrapper + */ +export interface ThemeableComponentRegistryEntry { + basePath: string; + baseFileName: string, + baseClass: string; + + wrapperPath: string; + wrapperFileName: string, + wrapperClass: string; +} + +function isAngularComponentDecorator(node: ts.Node) { + if (node.kind === ts.SyntaxKind.Decorator && node.parent.kind === ts.SyntaxKind.ClassDeclaration) { + const decorator = node as ts.Decorator; + + if (decorator.expression.kind === ts.SyntaxKind.CallExpression) { + const method = decorator.expression as ts.CallExpression; + + if (method.expression.kind === ts.SyntaxKind.Identifier) { + return (method.expression as Identifier).text === 'Component'; + } + } + } + + return false; +} + +function findImportDeclaration(source: ts.SourceFile, identifierName: string): ts.ImportDeclaration | undefined { + return ts.forEachChild(source, (topNode: ts.Node) => { + if (topNode.kind === ts.SyntaxKind.ImportDeclaration) { + const importDeclaration = topNode as ts.ImportDeclaration; + + if (importDeclaration.importClause?.namedBindings?.kind === ts.SyntaxKind.NamedImports) { + const namedImports = importDeclaration.importClause?.namedBindings as ts.NamedImports; + + for (const element of namedImports.elements) { + if (element.name.text === identifierName) { + return importDeclaration; + } + } + } + } + + return undefined; + }); +} + +/** + * Listing of all themeable Components + */ +class ThemeableComponentRegistry { + public readonly entries: Set; + public readonly byBaseClass: Map; + public readonly byWrapperClass: Map; + public readonly byBasePath: Map; + public readonly byWrapperPath: Map; + + constructor() { + this.entries = new Set(); + this.byBaseClass = new Map(); + this.byWrapperClass = new Map(); + this.byBasePath = new Map(); + this.byWrapperPath = new Map(); + } + + public initialize(prefix = '') { + if (this.entries.size > 0) { + return; + } + + function registerWrapper(path: string) { + const source = getSource(path); + + function traverse(node: ts.Node) { + if (node.parent !== undefined && isAngularComponentDecorator(node)) { + const classNode = node.parent as ts.ClassDeclaration; + + if (classNode.name === undefined || classNode.heritageClauses === undefined) { + return; + } + + const wrapperClass = classNode.name?.escapedText as string; + + for (const heritageClause of classNode.heritageClauses) { + for (const type of heritageClause.types) { + if ((type as any).expression.escapedText === 'ThemedComponent') { + if (type.kind !== ts.SyntaxKind.ExpressionWithTypeArguments || type.typeArguments === undefined) { + continue; + } + + const firstTypeArg = type.typeArguments[0] as ts.TypeReferenceNode; + const baseClass = (firstTypeArg.typeName as ts.Identifier)?.escapedText; + + if (baseClass === undefined) { + continue; + } + + const importDeclaration = findImportDeclaration(source, baseClass); + + if (importDeclaration === undefined) { + continue; + } + + const basePath = resolveLocalPath((importDeclaration.moduleSpecifier as ts.StringLiteral).text, path); + + themeableComponents.add({ + baseClass, + basePath: basePath.replace(new RegExp(`^${prefix}`), ''), + baseFileName: basename(basePath).replace(/\.ts$/, ''), + wrapperClass, + wrapperPath: path.replace(new RegExp(`^${prefix}`), ''), + wrapperFileName: basename(path).replace(/\.ts$/, ''), + }); + } + } + } + + return; + } else { + ts.forEachChild(node, traverse); + } + } + + traverse(source); + } + + const glob = require('glob'); + + // note: this outputs Unix-style paths on Windows + const wrappers: string[] = glob.GlobSync(prefix + 'src/app/**/themed-*.component.ts', { ignore: 'node_modules/**' }).found; + + for (const wrapper of wrappers) { + registerWrapper(wrapper); + } + } + + private add(entry: ThemeableComponentRegistryEntry) { + this.entries.add(entry); + this.byBaseClass.set(entry.baseClass, entry); + this.byWrapperClass.set(entry.wrapperClass, entry); + this.byBasePath.set(entry.basePath, entry); + this.byWrapperPath.set(entry.wrapperPath, entry); + } +} + +export const themeableComponents = new ThemeableComponentRegistry(); + +/** + * Construct the AST of a TypeScript source file + * @param file + */ +function getSource(file: string): ts.SourceFile { + return ts.createSourceFile( + file, + readFileSync(file).toString(), + ts.ScriptTarget.ES2020, // todo: actually use tsconfig.json? + /*setParentNodes */ true, + ); +} + +/** + * Resolve a possibly relative local path into an absolute path starting from the root directory of the project + */ +function resolveLocalPath(path: string, relativeTo: string) { + if (path.startsWith('src/')) { + return path; + } else if (path.startsWith('./')) { + const parts = relativeTo.split('/'); + return [ + ...parts.slice(0, parts.length - 1), + path.replace(/^.\//, ''), + ].join('/') + '.ts'; + } else { + throw new Error(`Unsupported local path: ${path}`); + } +} + +export function isThemedComponentWrapper(decoratorNode: TSESTree.Decorator): boolean { + if (decoratorNode.parent.type !== TSESTree.AST_NODE_TYPES.ClassDeclaration) { + return false; + } + + if (decoratorNode.parent.superClass?.type !== TSESTree.AST_NODE_TYPES.Identifier) { + return false; + } + + return (decoratorNode.parent.superClass as any)?.name === 'ThemedComponent'; +} + +export function getBaseComponentClassName(decoratorNode: TSESTree.Decorator): string | undefined { + const wrapperClass = getComponentClassName(decoratorNode); + + if (wrapperClass === undefined) { + return; + } + + themeableComponents.initialize(); + const entry = themeableComponents.byWrapperClass.get(wrapperClass); + + if (entry === undefined) { + return undefined; + } + + return entry.baseClass; +} + +export function isThemeableComponent(className: string): boolean { + themeableComponents.initialize(); + return themeableComponents.byBaseClass.has(className); +} + +export function inThemedComponentOverrideFile(filename: string): boolean { + const match = filename.match(/src\/themes\/[^\/]+\/(app\/.*)/); + + if (!match) { + return false; + } + themeableComponents.initialize(); + // todo: this is fragile! + return themeableComponents.byBasePath.has(`src/${match[1]}`); +} + +export function allThemeableComponents(): ThemeableComponentRegistryEntry[] { + themeableComponents.initialize(); + return [...themeableComponents.entries]; +} + +export function getThemeableComponentByBaseClass(baseClass: string): ThemeableComponentRegistryEntry | undefined { + themeableComponents.initialize(); + return themeableComponents.byBaseClass.get(baseClass); +} + +export function isAllowedUnthemedUsage(usageNode: TSESTree.Identifier) { + return isPartOfClassDeclaration(usageNode) || isPartOfTypeExpression(usageNode) || isPartOfViewChild(usageNode); +} + +export const DISALLOWED_THEME_SELECTORS = 'ds-(base|themed)-'; + +export function fixSelectors(text: string): string { + return text.replaceAll(/ds-(base|themed)-/g, 'ds-'); +} diff --git a/lint/src/util/typescript.ts b/lint/src/util/typescript.ts new file mode 100644 index 0000000000..0d04ef1a3d --- /dev/null +++ b/lint/src/util/typescript.ts @@ -0,0 +1,155 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +import { TSESTree } from '@typescript-eslint/utils'; +import { + RuleContext, + SourceCode, +} from '@typescript-eslint/utils/ts-eslint'; + +import { + match, + toUnixStylePath, +} from './misc'; + +export type AnyRuleContext = RuleContext; + +/** + * Return the current filename based on the ESLint rule context as a Unix-style path. + * This is easier for regex and comparisons to glob paths. + */ +export function getFilename(context: AnyRuleContext): string { + // TSESLint claims this is deprecated, but the suggested alternative is undefined (could be a version mismatch between ESLint and TSESlint?) + // eslint-disable-next-line deprecation/deprecation + return toUnixStylePath(context.getFilename()); +} + +export function getSourceCode(context: AnyRuleContext): SourceCode { + // TSESLint claims this is deprecated, but the suggested alternative is undefined (could be a version mismatch between ESLint and TSESlint?) + // eslint-disable-next-line deprecation/deprecation + return context.getSourceCode(); +} + +export function getObjectPropertyNodeByName(objectNode: TSESTree.ObjectExpression, propertyName: string): TSESTree.Node | undefined { + for (const propertyNode of objectNode.properties) { + if ( + propertyNode.type === TSESTree.AST_NODE_TYPES.Property + && ( + ( + propertyNode.key?.type === TSESTree.AST_NODE_TYPES.Identifier + && propertyNode.key?.name === propertyName + ) || ( + propertyNode.key?.type === TSESTree.AST_NODE_TYPES.Literal + && propertyNode.key?.value === propertyName + ) + ) + ) { + return propertyNode.value; + } + } + return undefined; +} + +export function findUsages(context: AnyRuleContext, localNode: TSESTree.Identifier): TSESTree.Identifier[] { + const source = getSourceCode(context); + + const usages: TSESTree.Identifier[] = []; + + for (const token of source.ast.tokens) { + if (token.type === TSESTree.AST_TOKEN_TYPES.Identifier && token.value === localNode.name && !match(token.range, localNode.range)) { + const node = source.getNodeByRangeIndex(token.range[0]); + // todo: in some cases, the resulting node can actually be the whole program (!) + if (node !== null) { + usages.push(node as TSESTree.Identifier); + } + } + } + + return usages; +} + +export function findUsagesByName(context: AnyRuleContext, identifier: string): TSESTree.Identifier[] { + const source = getSourceCode(context); + + const usages: TSESTree.Identifier[] = []; + + for (const token of source.ast.tokens) { + if (token.type === TSESTree.AST_TOKEN_TYPES.Identifier && token.value === identifier) { + const node = source.getNodeByRangeIndex(token.range[0]); + // todo: in some cases, the resulting node can actually be the whole program (!) + if (node !== null) { + usages.push(node as TSESTree.Identifier); + } + } + } + + return usages; +} + +export function isPartOfTypeExpression(node: TSESTree.Identifier): boolean { + return node.parent?.type?.valueOf().startsWith('TSType'); +} + +export function isPartOfClassDeclaration(node: TSESTree.Identifier): boolean { + return node.parent?.type === TSESTree.AST_NODE_TYPES.ClassDeclaration; +} + +function fromSrc(path: string): string { + const m = path.match(/^.*(src\/.+)(\.(ts|json|js)?)$/); + + if (m) { + return m[1]; + } else { + throw new Error(`Can't infer project-absolute TS/resource path from: ${path}`); + } +} + + +export function relativePath(thisFile: string, importFile: string): string { + const fromParts = fromSrc(thisFile).split('/'); + const toParts = fromSrc(importFile).split('/'); + + let lastCommon = 0; + for (let i = 0; i < fromParts.length - 1; i++) { + if (fromParts[i] === toParts[i]) { + lastCommon++; + } else { + break; + } + } + + const path = toParts.slice(lastCommon, toParts.length).join('/'); + const backtrack = fromParts.length - lastCommon - 1; + + let prefix: string; + if (backtrack > 0) { + prefix = '../'.repeat(backtrack); + } else { + prefix = './'; + } + + return prefix + path; +} + + +export function findImportSpecifier(context: AnyRuleContext, identifier: string): TSESTree.ImportSpecifier | undefined { + const source = getSourceCode(context); + + const usages: TSESTree.Identifier[] = []; + + for (const token of source.ast.tokens) { + if (token.type === TSESTree.AST_TOKEN_TYPES.Identifier && token.value === identifier) { + const node = source.getNodeByRangeIndex(token.range[0]); + // todo: in some cases, the resulting node can actually be the whole program (!) + if (node && node.parent && node.parent.type === TSESTree.AST_NODE_TYPES.ImportSpecifier) { + return node.parent; + } + } + } + + return undefined; +} diff --git a/lint/test/fixture/README.md b/lint/test/fixture/README.md new file mode 100644 index 0000000000..b19ae11b55 --- /dev/null +++ b/lint/test/fixture/README.md @@ -0,0 +1,9 @@ +# ESLint testing fixtures + +The files in this directory are used for the ESLint testing environment +- Some rules rely on registries that must be built up _before_ the rule is run + - In order to test these registries, the fixture sources contain a few dummy components +- The TypeScript ESLint test runner requires at least one dummy file to exist to run any tests + - By default, [`test.ts`](./src/test.ts) is used. Note that this file is empty; it's only there for the TypeScript configuration, the actual content is injected from the `code` property in the tests. + - To test rules that make assertions based on the path of the file, you'll need to include the `filename` property in the test configuration. Note that it must point to an existing file too! + - The `filename` must be provided as `fixture('src/something.ts')` \ No newline at end of file diff --git a/lint/test/fixture/index.ts b/lint/test/fixture/index.ts new file mode 100644 index 0000000000..1d4f33f7e2 --- /dev/null +++ b/lint/test/fixture/index.ts @@ -0,0 +1,13 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ + +export const FIXTURE = 'lint/test/fixture/'; + +export function fixture(path: string): string { + return FIXTURE + path; +} diff --git a/lint/test/fixture/src/app/test/test-routing.module.ts b/lint/test/fixture/src/app/test/test-routing.module.ts new file mode 100644 index 0000000000..1ccbccc599 --- /dev/null +++ b/lint/test/fixture/src/app/test/test-routing.module.ts @@ -0,0 +1,14 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +import { ThemedTestThemeableComponent } from './themed-test-themeable.component'; + +export const ROUTES = [ + { + component: ThemedTestThemeableComponent, + }, +]; diff --git a/lint/test/fixture/src/app/test/test-themeable.component.ts b/lint/test/fixture/src/app/test/test-themeable.component.ts new file mode 100644 index 0000000000..b445040539 --- /dev/null +++ b/lint/test/fixture/src/app/test/test-themeable.component.ts @@ -0,0 +1,16 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +import { Component } from '@angular/core'; + +@Component({ + selector: 'ds-base-test-themeable', + template: '', + standalone: true, +}) +export class TestThemeableComponent { +} diff --git a/lint/test/fixture/src/app/test/test.component.cy.ts b/lint/test/fixture/src/app/test/test.component.cy.ts new file mode 100644 index 0000000000..2300ac4a56 --- /dev/null +++ b/lint/test/fixture/src/app/test/test.component.cy.ts @@ -0,0 +1,8 @@ +/** + * 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/ + */ + diff --git a/lint/test/fixture/src/app/test/test.component.spec.ts b/lint/test/fixture/src/app/test/test.component.spec.ts new file mode 100644 index 0000000000..2300ac4a56 --- /dev/null +++ b/lint/test/fixture/src/app/test/test.component.spec.ts @@ -0,0 +1,8 @@ +/** + * 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/ + */ + diff --git a/lint/test/fixture/src/app/test/test.component.ts b/lint/test/fixture/src/app/test/test.component.ts new file mode 100644 index 0000000000..c01f104c98 --- /dev/null +++ b/lint/test/fixture/src/app/test/test.component.ts @@ -0,0 +1,15 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +import { Component } from '@angular/core'; + +@Component({ + selector: 'ds-test', + template: '', +}) +export class TestComponent { +} diff --git a/lint/test/fixture/src/app/test/test.module.ts b/lint/test/fixture/src/app/test/test.module.ts new file mode 100644 index 0000000000..a37396ef45 --- /dev/null +++ b/lint/test/fixture/src/app/test/test.module.ts @@ -0,0 +1,24 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +// @ts-ignore +import { NgModule } from '@angular/core'; + +import { TestComponent } from './test.component'; +import { TestThemeableComponent } from './test-themeable.component'; +import { ThemedTestThemeableComponent } from './themed-test-themeable.component'; + +@NgModule({ + declarations: [ + TestComponent, + TestThemeableComponent, + ThemedTestThemeableComponent, + ], +}) +export class TestModule { + +} diff --git a/lint/test/fixture/src/app/test/themed-test-themeable.component.ts b/lint/test/fixture/src/app/test/themed-test-themeable.component.ts new file mode 100644 index 0000000000..2697a8c598 --- /dev/null +++ b/lint/test/fixture/src/app/test/themed-test-themeable.component.ts @@ -0,0 +1,31 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +import { Component } from '@angular/core'; + +import { ThemedComponent } from '../../../../../../src/app/shared/theme-support/themed.component'; +import { TestThemeableComponent } from './test-themeable.component'; + +@Component({ + selector: 'ds-test-themeable', + template: '', + standalone: true, + imports: [TestThemeableComponent], +}) +export class ThemedTestThemeableComponent extends ThemedComponent { + protected getComponentName(): string { + return ''; + } + + protected importThemedComponent(themeName: string): Promise { + return Promise.resolve(undefined); + } + + protected importUnthemedComponent(): Promise { + return Promise.resolve(undefined); + } +} diff --git a/src/app/core/coar-notify/notify-info/notify-info.component.scss b/lint/test/fixture/src/test.ts similarity index 100% rename from src/app/core/coar-notify/notify-info/notify-info.component.scss rename to lint/test/fixture/src/test.ts diff --git a/lint/test/fixture/src/themes/test/app/test/other-themeable.component.ts b/lint/test/fixture/src/themes/test/app/test/other-themeable.component.ts new file mode 100644 index 0000000000..f72161b2bf --- /dev/null +++ b/lint/test/fixture/src/themes/test/app/test/other-themeable.component.ts @@ -0,0 +1,16 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +import { Component } from '@angular/core'; + +@Component({ + selector: 'ds-themed-test-themeable', + template: '', +}) +export class OtherThemeableComponent { + +} diff --git a/lint/test/fixture/src/themes/test/app/test/test-themeable.component.ts b/lint/test/fixture/src/themes/test/app/test/test-themeable.component.ts new file mode 100644 index 0000000000..d2b02ca9f1 --- /dev/null +++ b/lint/test/fixture/src/themes/test/app/test/test-themeable.component.ts @@ -0,0 +1,18 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +import { Component } from '@angular/core'; + +import { TestThemeableComponent as BaseComponent } from '../../../../app/test/test-themeable.component'; + +@Component({ + selector: 'ds-themed-test-themeable', + template: '', +}) +export class TestThemeableComponent extends BaseComponent { + +} diff --git a/lint/test/fixture/src/themes/test/test.module.ts b/lint/test/fixture/src/themes/test/test.module.ts new file mode 100644 index 0000000000..ff6ec3b2c0 --- /dev/null +++ b/lint/test/fixture/src/themes/test/test.module.ts @@ -0,0 +1,22 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ +// @ts-ignore +import { NgModule } from '@angular/core'; + +import { OtherThemeableComponent } from './app/test/other-themeable.component'; +import { TestThemeableComponent } from './app/test/test-themeable.component'; + +@NgModule({ + declarations: [ + TestThemeableComponent, + OtherThemeableComponent, + ], +}) +export class TestModule { + +} diff --git a/lint/test/fixture/tsconfig.json b/lint/test/fixture/tsconfig.json new file mode 100644 index 0000000000..0fd1141ae0 --- /dev/null +++ b/lint/test/fixture/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.json", + "include": [ + "src/**/*.ts" + ], + "exclude": [] +} diff --git a/lint/test/helpers.js b/lint/test/helpers.js new file mode 100644 index 0000000000..bd648d007f --- /dev/null +++ b/lint/test/helpers.js @@ -0,0 +1,13 @@ +const SpecReporter = require('jasmine-spec-reporter').SpecReporter; +const StacktraceOption = require('jasmine-spec-reporter').StacktraceOption; + +jasmine.getEnv().clearReporters(); // Clear default console reporter for those instead +jasmine.getEnv().addReporter(new SpecReporter({ + spec: { + displayErrorMessages: false, + }, + summary: { + displayFailed: true, + displayStacktrace: StacktraceOption.PRETTY, + }, +})); diff --git a/lint/test/rules.spec.ts b/lint/test/rules.spec.ts new file mode 100644 index 0000000000..11c9bec46c --- /dev/null +++ b/lint/test/rules.spec.ts @@ -0,0 +1,26 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ + +import { default as htmlPlugin } from '../src/rules/html'; +import { default as tsPlugin } from '../src/rules/ts'; +import { + htmlRuleTester, + tsRuleTester, +} from './testing'; + +describe('TypeScript rules', () => { + for (const { info, rule, tests } of tsPlugin.index) { + tsRuleTester.run(info.name, rule, tests as any); + } +}); + +describe('HTML rules', () => { + for (const { info, rule, tests } of htmlPlugin.index) { + htmlRuleTester.run(info.name, rule, tests); + } +}); diff --git a/lint/test/structure.spec.ts b/lint/test/structure.spec.ts new file mode 100644 index 0000000000..24e69e42d9 --- /dev/null +++ b/lint/test/structure.spec.ts @@ -0,0 +1,76 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ + +import { default as html } from '../src/rules/html'; +import { default as ts } from '../src/rules/ts'; + +describe('plugin structure', () => { + for (const pluginExports of [ts, html]) { + const pluginName = pluginExports.name ?? 'UNNAMED PLUGIN'; + + describe(pluginName, () => { + it('should have a name', () => { + expect(pluginExports.name).toBeTruthy(); + }); + + it('should have rules', () => { + expect(pluginExports.index).toBeTruthy(); + expect(pluginExports.rules).toBeTruthy(); + expect(pluginExports.index.length).toBeGreaterThan(0); + }); + + for (const ruleExports of pluginExports.index) { + const ruleName = ruleExports.info.name ?? 'UNNAMED RULE'; + + describe(ruleName, () => { + it('should have a name', () => { + expect(ruleExports.info.name).toBeTruthy(); + }); + + it('should be included under the right name in the plugin', () => { + expect(pluginExports.rules[ruleExports.info.name]).toBe(ruleExports.rule); + }); + + it('should contain metadata', () => { + expect(ruleExports.info).toBeTruthy(); + expect(ruleExports.info.name).toBeTruthy(); + expect(ruleExports.info.meta).toBeTruthy(); + expect(ruleExports.info.defaultOptions).toBeTruthy(); + }); + + it('should contain messages', () => { + expect(ruleExports.Message).toBeTruthy(); + expect(ruleExports.info.meta.messages).toBeTruthy(); + }); + + describe('messages', () => { + for (const member of Object.keys(ruleExports.Message)) { + describe(member, () => { + const id = (ruleExports.Message as any)[member]; + + it('should have a valid ID', () => { + expect(id).toBeTruthy(); + }); + + it('should have valid metadata', () => { + expect(ruleExports.info.meta.messages[id]).toBeTruthy(); + }); + }); + } + }); + + it('should contain tests', () => { + expect(ruleExports.tests).toBeTruthy(); + expect(ruleExports.tests.valid.length).toBeGreaterThan(0); + expect(ruleExports.tests.invalid.length).toBeGreaterThan(0); + }); + }); + } + }); + } +}); diff --git a/lint/test/testing.ts b/lint/test/testing.ts new file mode 100644 index 0000000000..53faf32069 --- /dev/null +++ b/lint/test/testing.ts @@ -0,0 +1,53 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ + +import { RuleTester as TypeScriptRuleTester } from '@typescript-eslint/rule-tester'; +import { RuleTester } from '@typescript-eslint/utils/ts-eslint'; + +import { themeableComponents } from '../src/util/theme-support'; +import { + FIXTURE, + fixture, +} from './fixture'; + + +// Register themed components from test fixture +themeableComponents.initialize(FIXTURE); + +TypeScriptRuleTester.itOnly = fit; +TypeScriptRuleTester.itSkip = xit; + +export const tsRuleTester = new TypeScriptRuleTester({ + parser: '@typescript-eslint/parser', + defaultFilenames: { + ts: fixture('src/test.ts'), + tsx: 'n/a', + }, + parserOptions: { + project: fixture('tsconfig.json'), + }, +}); + +class HtmlRuleTester extends RuleTester { + run(name: string, rule: any, tests: { valid: any[], invalid: any[] }) { + super.run(name, rule, { + valid: tests.valid.map((test) => ({ + filename: fixture('test.html'), + ...test, + })), + invalid: tests.invalid.map((test) => ({ + filename: fixture('test.html'), + ...test, + })), + }); + } +} + +export const htmlRuleTester = new HtmlRuleTester({ + parser: require.resolve('@angular-eslint/template-parser'), +}); diff --git a/lint/test/theme-support.spec.ts b/lint/test/theme-support.spec.ts new file mode 100644 index 0000000000..2edf9594b6 --- /dev/null +++ b/lint/test/theme-support.spec.ts @@ -0,0 +1,24 @@ +/** + * The contents of this file are subject to the license and copyright + * detailed in the LICENSE and NOTICE files at the root of the source + * tree and available online at + * + * http://www.dspace.org/license/ + */ + +import { themeableComponents } from '../src/util/theme-support'; + +describe('theme-support', () => { + describe('themeable component registry', () => { + it('should contain all themeable components from the fixture', () => { + expect(themeableComponents.entries.size).toBe(1); + expect(themeableComponents.byBasePath.size).toBe(1); + expect(themeableComponents.byWrapperPath.size).toBe(1); + expect(themeableComponents.byBaseClass.size).toBe(1); + + expect(themeableComponents.byBaseClass.get('TestThemeableComponent')).toBeTruthy(); + expect(themeableComponents.byBasePath.get('src/app/test/test-themeable.component.ts')).toBeTruthy(); + expect(themeableComponents.byWrapperPath.get('src/app/test/themed-test-themeable.component.ts')).toBeTruthy(); + }); + }); +}); diff --git a/lint/tsconfig.json b/lint/tsconfig.json new file mode 100644 index 0000000000..d3537a7376 --- /dev/null +++ b/lint/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "ES2021", + "lib": [ + "es2021" + ], + "module": "nodenext", + "moduleResolution": "nodenext", + "noImplicitReturns": true, + "skipLibCheck": true, + "strict": true, + "outDir": "./dist", + "sourceMap": true, + "allowSyntheticDefaultImports": true, + "types": [ + "jasmine", + "node" + ] + }, + "include": [ + "**/*.ts", + ], + "exclude": [ + "dist", + "test/fixture" + ] +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000000..b75ddc9ccf --- /dev/null +++ b/package-lock.json @@ -0,0 +1,23352 @@ +{ + "name": "dspace-angular", + "version": "9.0.0-next", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "dspace-angular", + "version": "9.0.0-next", + "hasInstallScript": true, + "dependencies": { + "@angular/animations": "^17.3.12", + "@angular/cdk": "^17.3.10", + "@angular/common": "^17.3.12", + "@angular/compiler": "^17.3.12", + "@angular/core": "^17.3.12", + "@angular/forms": "^17.3.12", + "@angular/localize": "^17.3.12", + "@angular/platform-browser": "^17.3.12", + "@angular/platform-browser-dynamic": "^17.3.12", + "@angular/platform-server": "^17.3.12", + "@angular/router": "^17.3.12", + "@angular/ssr": "^17.3.11", + "@babel/runtime": "7.26.0", + "@kolkov/ngx-gallery": "^2.0.1", + "@ng-bootstrap/ng-bootstrap": "^11.0.0", + "@ng-dynamic-forms/core": "^16.0.0", + "@ng-dynamic-forms/ui-ng-bootstrap": "^16.0.0", + "@ngrx/effects": "^17.1.1", + "@ngrx/router-store": "^17.1.1", + "@ngrx/store": "^17.1.1", + "@ngx-translate/core": "^14.0.0", + "@nicky-lenaers/ngx-scroll-to": "^14.0.0", + "angulartics2": "^12.2.0", + "axios": "^1.7.9", + "bootstrap": "^4.6.1", + "cerialize": "0.1.18", + "cli-progress": "^3.12.0", + "colors": "^1.4.0", + "compression": "^1.7.5", + "cookie-parser": "1.4.7", + "core-js": "^3.40.0", + "date-fns": "^2.29.3", + "date-fns-tz": "^1.3.7", + "deepmerge": "^4.3.1", + "ejs": "^3.1.10", + "express": "^4.21.2", + "express-rate-limit": "^5.1.3", + "fast-json-patch": "^3.1.1", + "filesize": "^6.1.0", + "http-proxy-middleware": "^2.0.7", + "http-terminator": "^3.2.0", + "isbot": "^5.1.21", + "js-cookie": "2.2.1", + "js-yaml": "^4.1.0", + "json5": "^2.2.3", + "jsonschema": "1.5.0", + "jwt-decode": "^3.1.2", + "lodash": "^4.17.21", + "lru-cache": "^7.14.1", + "markdown-it": "^13.0.1", + "mirador": "^3.4.3", + "mirador-dl-plugin": "^0.13.0", + "mirador-share-plugin": "^0.16.0", + "morgan": "^1.10.0", + "ng2-file-upload": "5.0.0", + "ng2-nouislider": "^2.0.0", + "ngx-infinite-scroll": "^16.0.0", + "ngx-pagination": "6.0.3", + "ngx-skeleton-loader": "^9.0.0", + "ngx-ui-switch": "^14.1.0", + "nouislider": "^15.7.1", + "orejime": "^2.3.1", + "pem": "1.14.8", + "reflect-metadata": "^0.2.2", + "rxjs": "^7.8.0", + "uuid": "^8.3.2", + "zone.js": "~0.14.10" + }, + "devDependencies": { + "@angular-builders/custom-webpack": "~17.0.2", + "@angular-devkit/build-angular": "^17.3.11", + "@angular-eslint/builder": "^17.5.3", + "@angular-eslint/bundled-angular-compiler": "^17.5.3", + "@angular-eslint/eslint-plugin": "^17.5.3", + "@angular-eslint/eslint-plugin-template": "^17.5.3", + "@angular-eslint/schematics": "^17.5.3", + "@angular-eslint/template-parser": "^17.5.3", + "@angular-eslint/utils": "^17.5.3", + "@angular/cli": "^17.3.11", + "@angular/compiler-cli": "^17.3.11", + "@angular/language-service": "^17.3.12", + "@cypress/schematic": "^1.5.0", + "@fortawesome/fontawesome-free": "^6.7.2", + "@ngrx/store-devtools": "^17.1.1", + "@ngtools/webpack": "^16.2.16", + "@types/deep-freeze": "0.1.5", + "@types/ejs": "^3.1.2", + "@types/express": "^4.17.17", + "@types/grecaptcha": "^3.0.9", + "@types/jasmine": "~3.6.0", + "@types/js-cookie": "2.2.6", + "@types/lodash": "^4.17.14", + "@types/node": "^14.14.9", + "@typescript-eslint/eslint-plugin": "^7.18.0", + "@typescript-eslint/parser": "^7.18.0", + "@typescript-eslint/rule-tester": "^7.18.0", + "@typescript-eslint/utils": "^7.18.0", + "axe-core": "^4.10.2", + "compression-webpack-plugin": "^9.2.0", + "copy-webpack-plugin": "^6.4.1", + "cross-env": "^7.0.3", + "cypress": "^13.17.0", + "cypress-axe": "^1.5.0", + "deep-freeze": "0.0.1", + "eslint": "^8.39.0", + "eslint-plugin-deprecation": "^1.4.1", + "eslint-plugin-dspace-angular-html": "file:./lint/dist/src/rules/html", + "eslint-plugin-dspace-angular-ts": "file:./lint/dist/src/rules/ts", + "eslint-plugin-import": "^2.31.0", + "eslint-plugin-import-newlines": "^1.3.1", + "eslint-plugin-jsdoc": "^45.0.0", + "eslint-plugin-jsonc": "^2.18.2", + "eslint-plugin-lodash": "^7.4.0", + "eslint-plugin-rxjs": "^5.0.3", + "eslint-plugin-simple-import-sort": "^10.0.0", + "eslint-plugin-unused-imports": "^3.2.0", + "express-static-gzip": "^2.2.0", + "jasmine": "^3.8.0", + "jasmine-core": "^3.8.0", + "jasmine-marbles": "0.9.2", + "karma": "^6.4.4", + "karma-chrome-launcher": "~3.2.0", + "karma-coverage-istanbul-reporter": "~3.0.3", + "karma-jasmine": "~4.0.0", + "karma-jasmine-html-reporter": "^1.5.0", + "karma-mocha-reporter": "2.2.5", + "ng-mocks": "^14.13.2", + "ngx-mask": "14.2.4", + "nodemon": "^2.0.22", + "postcss": "^8.5", + "postcss-import": "^14.0.0", + "postcss-loader": "^4.0.3", + "postcss-preset-env": "^7.4.2", + "rimraf": "^3.0.2", + "sass": "~1.83.4", + "sass-loader": "^12.6.0", + "sass-resources-loader": "^2.2.5", + "ts-node": "^8.10.2", + "typescript": "~5.4.5", + "webpack": "5.97.1", + "webpack-cli": "^5.1.4", + "webpack-dev-server": "^4.15.1" + } + }, + "lint/dist/src/rules/html": { + "name": "eslint-plugin-dspace-angular-html", + "version": "0.0.0", + "dev": true + }, + "lint/dist/src/rules/ts": { + "name": "eslint-plugin-dspace-angular-ts", + "version": "0.0.0", + "dev": true + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@angular-builders/common": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@angular-builders/common/-/common-1.0.2.tgz", + "integrity": "sha512-lUusRq6jN1It5LcUTLS6Q+AYAYGTo/EEN8hV0M6Ek9qXzweAouJaSEnwv7p04/pD7yJTl0YOCbN79u+wGm3x4g==", + "dev": true, + "dependencies": { + "@angular-devkit/core": "^17.1.0", + "ts-node": "^10.0.0", + "tsconfig-paths": "^4.1.0" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0" + } + }, + "node_modules/@angular-builders/common/node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/@angular-builders/custom-webpack": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/@angular-builders/custom-webpack/-/custom-webpack-17.0.2.tgz", + "integrity": "sha512-K0jqdW5UdVIeKiZXO4nLiiiVt0g6PKJELdxgjsBGMtyRk+RLEY+pIp1061oy/Yf09nGYseZ7Mdx3XASYHQjNwA==", + "dev": true, + "dependencies": { + "@angular-builders/common": "1.0.2", + "@angular-devkit/architect": ">=0.1700.0 < 0.1800.0", + "@angular-devkit/build-angular": "^17.0.0", + "@angular-devkit/core": "^17.0.0", + "lodash": "^4.17.15", + "webpack-merge": "^5.7.3" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0" + }, + "peerDependencies": { + "@angular/compiler-cli": "^17.0.0" + } + }, + "node_modules/@angular-devkit/architect": { + "version": "0.1703.11", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1703.11.tgz", + "integrity": "sha512-YNasVZk4rYdcM6M+KRH8PUBhVyJfqzUYLpO98GgRokW+taIDgifckSlmfDZzQRbw45qiwei1IKCLqcpC8nM5Tw==", + "dev": true, + "dependencies": { + "@angular-devkit/core": "17.3.11", + "rxjs": "7.8.1" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-devkit/build-angular": { + "version": "17.3.11", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-17.3.11.tgz", + "integrity": "sha512-lHX5V2dSts328yvo/9E2u9QMGcvJhbEKKDDp9dBecwvIG9s+4lTOJgi9DPUE7W+AtmPcmbbhwC2JRQ/SLQhAoA==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "2.3.0", + "@angular-devkit/architect": "0.1703.11", + "@angular-devkit/build-webpack": "0.1703.11", + "@angular-devkit/core": "17.3.11", + "@babel/core": "7.24.0", + "@babel/generator": "7.23.6", + "@babel/helper-annotate-as-pure": "7.22.5", + "@babel/helper-split-export-declaration": "7.22.6", + "@babel/plugin-transform-async-generator-functions": "7.23.9", + "@babel/plugin-transform-async-to-generator": "7.23.3", + "@babel/plugin-transform-runtime": "7.24.0", + "@babel/preset-env": "7.24.0", + "@babel/runtime": "7.24.0", + "@discoveryjs/json-ext": "0.5.7", + "@ngtools/webpack": "17.3.11", + "@vitejs/plugin-basic-ssl": "1.1.0", + "ansi-colors": "4.1.3", + "autoprefixer": "10.4.18", + "babel-loader": "9.1.3", + "babel-plugin-istanbul": "6.1.1", + "browserslist": "^4.21.5", + "copy-webpack-plugin": "11.0.0", + "critters": "0.0.22", + "css-loader": "6.10.0", + "esbuild-wasm": "0.20.1", + "fast-glob": "3.3.2", + "http-proxy-middleware": "2.0.7", + "https-proxy-agent": "7.0.4", + "inquirer": "9.2.15", + "jsonc-parser": "3.2.1", + "karma-source-map-support": "1.4.0", + "less": "4.2.0", + "less-loader": "11.1.0", + "license-webpack-plugin": "4.0.2", + "loader-utils": "3.2.1", + "magic-string": "0.30.8", + "mini-css-extract-plugin": "2.8.1", + "mrmime": "2.0.0", + "open": "8.4.2", + "ora": "5.4.1", + "parse5-html-rewriting-stream": "7.0.0", + "picomatch": "4.0.1", + "piscina": "4.4.0", + "postcss": "8.4.35", + "postcss-loader": "8.1.1", + "resolve-url-loader": "5.0.0", + "rxjs": "7.8.1", + "sass": "1.71.1", + "sass-loader": "14.1.1", + "semver": "7.6.0", + "source-map-loader": "5.0.0", + "source-map-support": "0.5.21", + "terser": "5.29.1", + "tree-kill": "1.2.2", + "tslib": "2.6.2", + "undici": "6.11.1", + "vite": "5.1.8", + "watchpack": "2.4.0", + "webpack": "5.94.0", + "webpack-dev-middleware": "6.1.2", + "webpack-dev-server": "4.15.1", + "webpack-merge": "5.10.0", + "webpack-subresource-integrity": "5.1.0" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "optionalDependencies": { + "esbuild": "0.20.1" + }, + "peerDependencies": { + "@angular/compiler-cli": "^17.0.0", + "@angular/localize": "^17.0.0", + "@angular/platform-server": "^17.0.0", + "@angular/service-worker": "^17.0.0", + "@web/test-runner": "^0.18.0", + "browser-sync": "^3.0.2", + "jest": "^29.5.0", + "jest-environment-jsdom": "^29.5.0", + "karma": "^6.3.0", + "ng-packagr": "^17.0.0", + "protractor": "^7.0.0", + "tailwindcss": "^2.0.0 || ^3.0.0", + "typescript": ">=5.2 <5.5" + }, + "peerDependenciesMeta": { + "@angular/localize": { + "optional": true + }, + "@angular/platform-server": { + "optional": true + }, + "@angular/service-worker": { + "optional": true + }, + "@web/test-runner": { + "optional": true + }, + "browser-sync": { + "optional": true + }, + "jest": { + "optional": true + }, + "jest-environment-jsdom": { + "optional": true + }, + "karma": { + "optional": true + }, + "ng-packagr": { + "optional": true + }, + "protractor": { + "optional": true + }, + "tailwindcss": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@babel/runtime": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.0.tgz", + "integrity": "sha512-Chk32uHMg6TnQdvw2e9IlqPpFX/6NLuK0Ys2PqLb7/gL5uFn9mXvK715FGLlOLQrcO4qIkNHkvPGktzzXexsFw==", + "dev": true, + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/aix-ppc64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz", + "integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/android-arm": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz", + "integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/android-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz", + "integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/android-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz", + "integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/darwin-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz", + "integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/darwin-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz", + "integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/freebsd-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz", + "integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/freebsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz", + "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/linux-arm": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz", + "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/linux-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz", + "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/linux-ia32": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz", + "integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/linux-loong64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz", + "integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/linux-mips64el": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz", + "integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/linux-ppc64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz", + "integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/linux-riscv64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz", + "integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/linux-s390x": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz", + "integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/linux-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz", + "integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/netbsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz", + "integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/openbsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz", + "integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/sunos-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz", + "integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/win32-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz", + "integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/win32-ia32": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz", + "integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@esbuild/win32-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz", + "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@ngtools/webpack": { + "version": "17.3.11", + "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-17.3.11.tgz", + "integrity": "sha512-SfTCbplt4y6ak5cf2IfqdoVOsnoNdh/j6Vu+wb8WWABKwZ5yfr2S/Gk6ithSKcdIZhAF8DNBOoyk1EJuf8Xkfg==", + "dev": true, + "engines": { + "node": "^18.13.0 || >=20.9.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "@angular/compiler-cli": "^17.0.0", + "typescript": ">=5.2 <5.5", + "webpack": "^5.54.0" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@types/node": { + "version": "22.7.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.7.tgz", + "integrity": "sha512-SRxCrrg9CL/y54aiMCG3edPKdprgMVGDXjA3gB8UmmBW5TcXzRUYAh8EWzTnSJFAd1rgImPELza+A3bJ+qxz8Q==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@vitejs/plugin-basic-ssl": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-1.1.0.tgz", + "integrity": "sha512-wO4Dk/rm8u7RNhOf95ZzcEmC9rYOncYgvq4z3duaJrCgjN8BxAnDVyndanfcJZ0O6XZzHz6Q0hTimxTg8Y9g/A==", + "dev": true, + "engines": { + "node": ">=14.6.0" + }, + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/connect-history-api-fallback": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz", + "integrity": "sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==", + "dev": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/copy-webpack-plugin": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-11.0.0.tgz", + "integrity": "sha512-fX2MWpamkW0hZxMEg0+mYnA40LTosOSa5TqZ9GYIBzyJa9C3QUaMPSE2xAi/buNr8u89SfD9wHSQVBzrRa/SOQ==", + "dev": true, + "dependencies": { + "fast-glob": "^3.2.11", + "glob-parent": "^6.0.1", + "globby": "^13.1.1", + "normalize-path": "^3.0.0", + "schema-utils": "^4.0.0", + "serialize-javascript": "^6.0.0" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/cosmiconfig": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", + "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", + "dev": true, + "dependencies": { + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/globby": { + "version": "13.2.2", + "resolved": "https://registry.npmjs.org/globby/-/globby-13.2.2.tgz", + "integrity": "sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w==", + "dev": true, + "dependencies": { + "dir-glob": "^3.0.1", + "fast-glob": "^3.3.0", + "ignore": "^5.2.4", + "merge2": "^1.4.1", + "slash": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/immutable": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.7.tgz", + "integrity": "sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==", + "dev": true + }, + "node_modules/@angular-devkit/build-angular/node_modules/ipaddr.js": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz", + "integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "node_modules/@angular-devkit/build-angular/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/@angular-devkit/build-angular/node_modules/postcss": { + "version": "8.4.35", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.35.tgz", + "integrity": "sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/postcss-loader": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-8.1.1.tgz", + "integrity": "sha512-0IeqyAsG6tYiDRCYKQJLAmgQr47DX6N7sFSWvQxt6AcupX8DIdmykuk/o/tx0Lze3ErGHJEp5OSRxrelC6+NdQ==", + "dev": true, + "dependencies": { + "cosmiconfig": "^9.0.0", + "jiti": "^1.20.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "postcss": "^7.0.0 || ^8.0.1", + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/sass": { + "version": "1.71.1", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.71.1.tgz", + "integrity": "sha512-wovtnV2PxzteLlfNzbgm1tFXPLoZILYAMJtvoXXkD7/+1uP41eKkIt1ypWq5/q2uT94qHjXehEYfmjKOvjL9sg==", + "dev": true, + "dependencies": { + "chokidar": ">=3.0.0 <4.0.0", + "immutable": "^4.0.0", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/sass-loader": { + "version": "14.1.1", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-14.1.1.tgz", + "integrity": "sha512-QX8AasDg75monlybel38BZ49JP5Z+uSKfKwF2rO7S74BywaRmGQMUBw9dtkS+ekyM/QnP+NOrRYq8ABMZ9G8jw==", + "dev": true, + "dependencies": { + "neo-async": "^2.6.2" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "node-sass": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0", + "sass": "^1.3.0", + "sass-embedded": "*", + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "node-sass": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/slash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", + "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/vite": { + "version": "5.1.8", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.1.8.tgz", + "integrity": "sha512-mB8ToUuSmzODSpENgvpFk2fTiU/YQ1tmcVJJ4WZbq4fPdGJkFNVcmVL5k7iDug6xzWjjuGDKAuSievIsD6H7Xw==", + "dev": true, + "dependencies": { + "esbuild": "^0.19.3", + "postcss": "^8.4.35", + "rollup": "^4.2.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/vite/node_modules/esbuild": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz", + "integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.19.12", + "@esbuild/android-arm": "0.19.12", + "@esbuild/android-arm64": "0.19.12", + "@esbuild/android-x64": "0.19.12", + "@esbuild/darwin-arm64": "0.19.12", + "@esbuild/darwin-x64": "0.19.12", + "@esbuild/freebsd-arm64": "0.19.12", + "@esbuild/freebsd-x64": "0.19.12", + "@esbuild/linux-arm": "0.19.12", + "@esbuild/linux-arm64": "0.19.12", + "@esbuild/linux-ia32": "0.19.12", + "@esbuild/linux-loong64": "0.19.12", + "@esbuild/linux-mips64el": "0.19.12", + "@esbuild/linux-ppc64": "0.19.12", + "@esbuild/linux-riscv64": "0.19.12", + "@esbuild/linux-s390x": "0.19.12", + "@esbuild/linux-x64": "0.19.12", + "@esbuild/netbsd-x64": "0.19.12", + "@esbuild/openbsd-x64": "0.19.12", + "@esbuild/sunos-x64": "0.19.12", + "@esbuild/win32-arm64": "0.19.12", + "@esbuild/win32-ia32": "0.19.12", + "@esbuild/win32-x64": "0.19.12" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/webpack": { + "version": "5.94.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.94.0.tgz", + "integrity": "sha512-KcsGn50VT+06JH/iunZJedYGUJS5FGjow8wb9c0v5n1Om8O1g4L6LjtfxwlXIATopoQu+vOXXa7gYisWxCoPyg==", + "dev": true, + "dependencies": { + "@types/estree": "^1.0.5", + "@webassemblyjs/ast": "^1.12.1", + "@webassemblyjs/wasm-edit": "^1.12.1", + "@webassemblyjs/wasm-parser": "^1.12.1", + "acorn": "^8.7.1", + "acorn-import-attributes": "^1.9.5", + "browserslist": "^4.21.10", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.17.1", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^3.2.0", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.3.10", + "watchpack": "^2.4.1", + "webpack-sources": "^3.2.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/webpack-dev-server": { + "version": "4.15.1", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.15.1.tgz", + "integrity": "sha512-5hbAst3h3C3L8w6W4P96L5vaV0PxSmJhxZvWKYIdgxOQm8pNZ5dEOmmSLBVpP85ReeyRt6AS1QJNyo/oFFPeVA==", + "dev": true, + "dependencies": { + "@types/bonjour": "^3.5.9", + "@types/connect-history-api-fallback": "^1.3.5", + "@types/express": "^4.17.13", + "@types/serve-index": "^1.9.1", + "@types/serve-static": "^1.13.10", + "@types/sockjs": "^0.3.33", + "@types/ws": "^8.5.5", + "ansi-html-community": "^0.0.8", + "bonjour-service": "^1.0.11", + "chokidar": "^3.5.3", + "colorette": "^2.0.10", + "compression": "^1.7.4", + "connect-history-api-fallback": "^2.0.0", + "default-gateway": "^6.0.3", + "express": "^4.17.3", + "graceful-fs": "^4.2.6", + "html-entities": "^2.3.2", + "http-proxy-middleware": "^2.0.3", + "ipaddr.js": "^2.0.1", + "launch-editor": "^2.6.0", + "open": "^8.0.9", + "p-retry": "^4.5.0", + "rimraf": "^3.0.2", + "schema-utils": "^4.0.0", + "selfsigned": "^2.1.1", + "serve-index": "^1.9.1", + "sockjs": "^0.3.24", + "spdy": "^4.0.2", + "webpack-dev-middleware": "^5.3.1", + "ws": "^8.13.0" + }, + "bin": { + "webpack-dev-server": "bin/webpack-dev-server.js" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.37.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + }, + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/webpack-dev-server/node_modules/webpack-dev-middleware": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.4.tgz", + "integrity": "sha512-BVdTqhhs+0IfoeAf7EoH5WE+exCmqGerHfDM0IL096Px60Tq2Mn9MAbnaGUe6HiMa41KMCYF19gyzZmBcq/o4Q==", + "dev": true, + "dependencies": { + "colorette": "^2.0.10", + "memfs": "^3.4.3", + "mime-types": "^2.1.31", + "range-parser": "^1.2.1", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/webpack-sources": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", + "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "dev": true, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/webpack/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/webpack/node_modules/watchpack": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz", + "integrity": "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==", + "dev": true, + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/@angular-devkit/build-webpack": { + "version": "0.1703.11", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1703.11.tgz", + "integrity": "sha512-qbCiiHuoVkD7CtLyWoRi/Vzz6nrEztpF5XIyWUcQu67An1VlxbMTE4yoSQiURjCQMnB/JvS1GPVed7wOq3SJ/w==", + "dev": true, + "dependencies": { + "@angular-devkit/architect": "0.1703.11", + "rxjs": "7.8.1" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "webpack": "^5.30.0", + "webpack-dev-server": "^4.0.0" + } + }, + "node_modules/@angular-devkit/core": { + "version": "17.3.11", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-17.3.11.tgz", + "integrity": "sha512-vTNDYNsLIWpYk2I969LMQFH29GTsLzxNk/0cLw5q56ARF0v5sIWfHYwGTS88jdDqIpuuettcSczbxeA7EuAmqQ==", + "dev": true, + "dependencies": { + "ajv": "8.12.0", + "ajv-formats": "2.1.1", + "jsonc-parser": "3.2.1", + "picomatch": "4.0.1", + "rxjs": "7.8.1", + "source-map": "0.7.4" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^3.5.2" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/schematics": { + "version": "17.3.11", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-17.3.11.tgz", + "integrity": "sha512-I5wviiIqiFwar9Pdk30Lujk8FczEEc18i22A5c6Z9lbmhPQdTroDnEQdsfXjy404wPe8H62s0I15o4pmMGfTYQ==", + "dev": true, + "dependencies": { + "@angular-devkit/core": "17.3.11", + "jsonc-parser": "3.2.1", + "magic-string": "0.30.8", + "ora": "5.4.1", + "rxjs": "7.8.1" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-eslint/builder": { + "version": "17.5.3", + "resolved": "https://registry.npmjs.org/@angular-eslint/builder/-/builder-17.5.3.tgz", + "integrity": "sha512-DoPCwt8qp5oMkfxY8V3wygf6/E7zzgXkPCwTRhIelklfpB3nYwLnbRSD8G5hueAU4eyASKiIuhR79E996AuUSw==", + "dev": true, + "peerDependencies": { + "eslint": "^7.20.0 || ^8.0.0", + "typescript": "*" + } + }, + "node_modules/@angular-eslint/bundled-angular-compiler": { + "version": "17.5.3", + "resolved": "https://registry.npmjs.org/@angular-eslint/bundled-angular-compiler/-/bundled-angular-compiler-17.5.3.tgz", + "integrity": "sha512-x9jZ6mME9wxumErPGonWERXX/9TJ7mzEkQhOKt3BxBFm0sy9XQqLMAenp1PBSg3RF3rH7EEVdB2+jb75RtHp0g==", + "dev": true + }, + "node_modules/@angular-eslint/eslint-plugin": { + "version": "17.5.3", + "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin/-/eslint-plugin-17.5.3.tgz", + "integrity": "sha512-2gMRZ+SkiygrPDtCJwMfjmwIFOcvxxC4NRX/MqRo6udsa0gtqPrc8acRbwrmAXlullmhzmaeUfkHpGDSzW8pFw==", + "dev": true, + "dependencies": { + "@angular-eslint/bundled-angular-compiler": "17.5.3", + "@angular-eslint/utils": "17.5.3", + "@typescript-eslint/utils": "7.11.0" + }, + "peerDependencies": { + "eslint": "^7.20.0 || ^8.0.0", + "typescript": "*" + } + }, + "node_modules/@angular-eslint/eslint-plugin-template": { + "version": "17.5.3", + "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin-template/-/eslint-plugin-template-17.5.3.tgz", + "integrity": "sha512-RkRFagxqBPV2xdNyeQQROUm6I1Izto1Z3Wy73lCk2zq1RhVgbznniH/epmOIE8PMkHmMKmZ765FV++J/90p4Ig==", + "dev": true, + "dependencies": { + "@angular-eslint/bundled-angular-compiler": "17.5.3", + "@angular-eslint/utils": "17.5.3", + "@typescript-eslint/type-utils": "7.11.0", + "@typescript-eslint/utils": "7.11.0", + "aria-query": "5.3.0", + "axobject-query": "4.0.0" + }, + "peerDependencies": { + "eslint": "^7.20.0 || ^8.0.0", + "typescript": "*" + } + }, + "node_modules/@angular-eslint/eslint-plugin-template/node_modules/@typescript-eslint/scope-manager": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.11.0.tgz", + "integrity": "sha512-27tGdVEiutD4POirLZX4YzT180vevUURJl4wJGmm6TrQoiYwuxTIY98PBp6L2oN+JQxzE0URvYlzJaBHIekXAw==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.11.0", + "@typescript-eslint/visitor-keys": "7.11.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@angular-eslint/eslint-plugin-template/node_modules/@typescript-eslint/types": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.11.0.tgz", + "integrity": "sha512-MPEsDRZTyCiXkD4vd3zywDCifi7tatc4K37KqTprCvaXptP7Xlpdw0NR2hRJTetG5TxbWDB79Ys4kLmHliEo/w==", + "dev": true, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@angular-eslint/eslint-plugin-template/node_modules/@typescript-eslint/typescript-estree": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.11.0.tgz", + "integrity": "sha512-cxkhZ2C/iyi3/6U9EPc5y+a6csqHItndvN/CzbNXTNrsC3/ASoYQZEt9uMaEp+xFNjasqQyszp5TumAVKKvJeQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.11.0", + "@typescript-eslint/visitor-keys": "7.11.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@angular-eslint/eslint-plugin-template/node_modules/@typescript-eslint/utils": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.11.0.tgz", + "integrity": "sha512-xlAWwPleNRHwF37AhrZurOxA1wyXowW4PqVXZVUNCLjB48CqdPJoJWkrpH2nij9Q3Lb7rtWindtoXwxjxlKKCA==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "7.11.0", + "@typescript-eslint/types": "7.11.0", + "@typescript-eslint/typescript-estree": "7.11.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + } + }, + "node_modules/@angular-eslint/eslint-plugin-template/node_modules/@typescript-eslint/visitor-keys": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.11.0.tgz", + "integrity": "sha512-7syYk4MzjxTEk0g/w3iqtgxnFQspDJfn6QKD36xMuuhTzjcxY7F8EmBLnALjVyaOF1/bVocu3bS/2/F7rXrveQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.11.0", + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@angular-eslint/eslint-plugin/node_modules/@typescript-eslint/scope-manager": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.11.0.tgz", + "integrity": "sha512-27tGdVEiutD4POirLZX4YzT180vevUURJl4wJGmm6TrQoiYwuxTIY98PBp6L2oN+JQxzE0URvYlzJaBHIekXAw==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.11.0", + "@typescript-eslint/visitor-keys": "7.11.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@angular-eslint/eslint-plugin/node_modules/@typescript-eslint/types": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.11.0.tgz", + "integrity": "sha512-MPEsDRZTyCiXkD4vd3zywDCifi7tatc4K37KqTprCvaXptP7Xlpdw0NR2hRJTetG5TxbWDB79Ys4kLmHliEo/w==", + "dev": true, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@angular-eslint/eslint-plugin/node_modules/@typescript-eslint/typescript-estree": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.11.0.tgz", + "integrity": "sha512-cxkhZ2C/iyi3/6U9EPc5y+a6csqHItndvN/CzbNXTNrsC3/ASoYQZEt9uMaEp+xFNjasqQyszp5TumAVKKvJeQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.11.0", + "@typescript-eslint/visitor-keys": "7.11.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@angular-eslint/eslint-plugin/node_modules/@typescript-eslint/utils": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.11.0.tgz", + "integrity": "sha512-xlAWwPleNRHwF37AhrZurOxA1wyXowW4PqVXZVUNCLjB48CqdPJoJWkrpH2nij9Q3Lb7rtWindtoXwxjxlKKCA==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "7.11.0", + "@typescript-eslint/types": "7.11.0", + "@typescript-eslint/typescript-estree": "7.11.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + } + }, + "node_modules/@angular-eslint/eslint-plugin/node_modules/@typescript-eslint/visitor-keys": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.11.0.tgz", + "integrity": "sha512-7syYk4MzjxTEk0g/w3iqtgxnFQspDJfn6QKD36xMuuhTzjcxY7F8EmBLnALjVyaOF1/bVocu3bS/2/F7rXrveQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.11.0", + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@angular-eslint/schematics": { + "version": "17.5.3", + "resolved": "https://registry.npmjs.org/@angular-eslint/schematics/-/schematics-17.5.3.tgz", + "integrity": "sha512-a0MlOjNLIM18l/66S+CzhANQR3QH3jDUa1MC50E4KBf1mwjQyfqd6RdfbOTMDjgFlPrfB+5JvoWOHHGj7FFM1A==", + "dev": true, + "dependencies": { + "@angular-eslint/eslint-plugin": "17.5.3", + "@angular-eslint/eslint-plugin-template": "17.5.3", + "ignore": "5.3.1", + "strip-json-comments": "3.1.1", + "tmp": "0.2.3" + }, + "peerDependencies": { + "@angular/cli": ">= 17.0.0 < 18.0.0" + } + }, + "node_modules/@angular-eslint/template-parser": { + "version": "17.5.3", + "resolved": "https://registry.npmjs.org/@angular-eslint/template-parser/-/template-parser-17.5.3.tgz", + "integrity": "sha512-NYybOsMkJUtFOW2JWALicipq0kK5+jGwA1MYyRoXjdbDlXltHUb9qkXj7p0fE6uRutBGXDl4288s8g/fZCnAIA==", + "dev": true, + "dependencies": { + "@angular-eslint/bundled-angular-compiler": "17.5.3", + "eslint-scope": "^8.0.0" + }, + "peerDependencies": { + "eslint": "^7.20.0 || ^8.0.0", + "typescript": "*" + } + }, + "node_modules/@angular-eslint/utils": { + "version": "17.5.3", + "resolved": "https://registry.npmjs.org/@angular-eslint/utils/-/utils-17.5.3.tgz", + "integrity": "sha512-0nNm1FUOLhVHrdK2PP5dZCYYVmTIkEJ4CmlwpuC4JtCLbD5XAHQpY/ZW5Ff5n1b7KfJt1Zy//jlhkkIaw3LaBQ==", + "dev": true, + "dependencies": { + "@angular-eslint/bundled-angular-compiler": "17.5.3", + "@typescript-eslint/utils": "7.11.0" + }, + "peerDependencies": { + "eslint": "^7.20.0 || ^8.0.0", + "typescript": "*" + } + }, + "node_modules/@angular-eslint/utils/node_modules/@typescript-eslint/scope-manager": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.11.0.tgz", + "integrity": "sha512-27tGdVEiutD4POirLZX4YzT180vevUURJl4wJGmm6TrQoiYwuxTIY98PBp6L2oN+JQxzE0URvYlzJaBHIekXAw==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.11.0", + "@typescript-eslint/visitor-keys": "7.11.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@angular-eslint/utils/node_modules/@typescript-eslint/types": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.11.0.tgz", + "integrity": "sha512-MPEsDRZTyCiXkD4vd3zywDCifi7tatc4K37KqTprCvaXptP7Xlpdw0NR2hRJTetG5TxbWDB79Ys4kLmHliEo/w==", + "dev": true, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@angular-eslint/utils/node_modules/@typescript-eslint/typescript-estree": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.11.0.tgz", + "integrity": "sha512-cxkhZ2C/iyi3/6U9EPc5y+a6csqHItndvN/CzbNXTNrsC3/ASoYQZEt9uMaEp+xFNjasqQyszp5TumAVKKvJeQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.11.0", + "@typescript-eslint/visitor-keys": "7.11.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@angular-eslint/utils/node_modules/@typescript-eslint/utils": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.11.0.tgz", + "integrity": "sha512-xlAWwPleNRHwF37AhrZurOxA1wyXowW4PqVXZVUNCLjB48CqdPJoJWkrpH2nij9Q3Lb7rtWindtoXwxjxlKKCA==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "7.11.0", + "@typescript-eslint/types": "7.11.0", + "@typescript-eslint/typescript-estree": "7.11.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + } + }, + "node_modules/@angular-eslint/utils/node_modules/@typescript-eslint/visitor-keys": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.11.0.tgz", + "integrity": "sha512-7syYk4MzjxTEk0g/w3iqtgxnFQspDJfn6QKD36xMuuhTzjcxY7F8EmBLnALjVyaOF1/bVocu3bS/2/F7rXrveQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.11.0", + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@angular/animations": { + "version": "17.3.12", + "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-17.3.12.tgz", + "integrity": "sha512-9hsdWF4gRRcVJtPcCcYLaX1CIyM9wUu6r+xRl6zU5hq8qhl35hig6ounz7CXFAzLf0WDBdM16bPHouVGaG76lg==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0" + }, + "peerDependencies": { + "@angular/core": "17.3.12" + } + }, + "node_modules/@angular/cdk": { + "version": "17.3.10", + "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-17.3.10.tgz", + "integrity": "sha512-b1qktT2c1TTTe5nTji/kFAVW92fULK0YhYAvJ+BjZTPKu2FniZNe8o4qqQ0pUuvtMu+ZQxp/QqFYoidIVCjScg==", + "dependencies": { + "tslib": "^2.3.0" + }, + "optionalDependencies": { + "parse5": "^7.1.2" + }, + "peerDependencies": { + "@angular/common": "^17.0.0 || ^18.0.0", + "@angular/core": "^17.0.0 || ^18.0.0", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/cli": { + "version": "17.3.11", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-17.3.11.tgz", + "integrity": "sha512-8R9LwAGL8hGAWJ4mNG9ZPUrBUzIdmst0Ldua6RJJ+PrqgjX+8IbO+lNnfrOY/XY+Z3LXbCEJflL26f9czCvTPQ==", + "dev": true, + "dependencies": { + "@angular-devkit/architect": "0.1703.11", + "@angular-devkit/core": "17.3.11", + "@angular-devkit/schematics": "17.3.11", + "@schematics/angular": "17.3.11", + "@yarnpkg/lockfile": "1.1.0", + "ansi-colors": "4.1.3", + "ini": "4.1.2", + "inquirer": "9.2.15", + "jsonc-parser": "3.2.1", + "npm-package-arg": "11.0.1", + "npm-pick-manifest": "9.0.0", + "open": "8.4.2", + "ora": "5.4.1", + "pacote": "17.0.6", + "resolve": "1.22.8", + "semver": "7.6.0", + "symbol-observable": "4.0.0", + "yargs": "17.7.2" + }, + "bin": { + "ng": "bin/ng.js" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular/common": { + "version": "17.3.12", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-17.3.12.tgz", + "integrity": "sha512-vabJzvrx76XXFrm1RJZ6o/CyG32piTB/1sfFfKHdlH1QrmArb8It4gyk9oEjZ1IkAD0HvBWlfWmn+T6Vx3pdUw==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0" + }, + "peerDependencies": { + "@angular/core": "17.3.12", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/compiler": { + "version": "17.3.12", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-17.3.12.tgz", + "integrity": "sha512-vwI8oOL/gM+wPnptOVeBbMfZYwzRxQsovojZf+Zol9szl0k3SZ3FycWlxxXZGFu3VIEfrP6pXplTmyODS/Lt1w==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0" + }, + "peerDependencies": { + "@angular/core": "17.3.12" + }, + "peerDependenciesMeta": { + "@angular/core": { + "optional": true + } + } + }, + "node_modules/@angular/compiler-cli": { + "version": "17.3.12", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-17.3.12.tgz", + "integrity": "sha512-1F8M7nWfChzurb7obbvuE7mJXlHtY1UG58pcwcomVtpPb+kPavgAO8OEvJHYBMV+bzSxkXt5UIwL9lt9jHUxZA==", + "dependencies": { + "@babel/core": "7.23.9", + "@jridgewell/sourcemap-codec": "^1.4.14", + "chokidar": "^3.0.0", + "convert-source-map": "^1.5.1", + "reflect-metadata": "^0.2.0", + "semver": "^7.0.0", + "tslib": "^2.3.0", + "yargs": "^17.2.1" + }, + "bin": { + "ng-xi18n": "bundles/src/bin/ng_xi18n.js", + "ngc": "bundles/src/bin/ngc.js", + "ngcc": "bundles/ngcc/index.js" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0" + }, + "peerDependencies": { + "@angular/compiler": "17.3.12", + "typescript": ">=5.2 <5.5" + } + }, + "node_modules/@angular/compiler-cli/node_modules/@babel/core": { + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.9.tgz", + "integrity": "sha512-5q0175NOjddqpvvzU+kDiSOAk4PfdO6FvwCWoQ6RO7rTzEe8vlo+4HVfcnAREhD4npMs0e9uZypjTwzZPCf/cw==", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.23.5", + "@babel/generator": "^7.23.6", + "@babel/helper-compilation-targets": "^7.23.6", + "@babel/helper-module-transforms": "^7.23.3", + "@babel/helpers": "^7.23.9", + "@babel/parser": "^7.23.9", + "@babel/template": "^7.23.9", + "@babel/traverse": "^7.23.9", + "@babel/types": "^7.23.9", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@angular/compiler-cli/node_modules/@babel/core/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==" + }, + "node_modules/@angular/compiler-cli/node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@angular/core": { + "version": "17.3.12", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-17.3.12.tgz", + "integrity": "sha512-MuFt5yKi161JmauUta4Dh0m8ofwoq6Ino+KoOtkYMBGsSx+A7dSm+DUxxNwdj7+DNyg3LjVGCFgBFnq4g8z06A==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0" + }, + "peerDependencies": { + "rxjs": "^6.5.3 || ^7.4.0", + "zone.js": "~0.14.0" + } + }, + "node_modules/@angular/forms": { + "version": "17.3.12", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-17.3.12.tgz", + "integrity": "sha512-tV6r12Q3yEUlXwpVko4E+XscunTIpPkLbaiDn/MTL3Vxi2LZnsLgHyd/i38HaHN+e/H3B0a1ToSOhV5wf3ay4Q==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0" + }, + "peerDependencies": { + "@angular/common": "17.3.12", + "@angular/core": "17.3.12", + "@angular/platform-browser": "17.3.12", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/language-service": { + "version": "17.3.12", + "resolved": "https://registry.npmjs.org/@angular/language-service/-/language-service-17.3.12.tgz", + "integrity": "sha512-MVmEXonXwdhFtIpU4q8qbXHsrAsdTjZcPPuWCU0zXVQ+VaB/y6oF7BVpmBtfyBcBCums1guEncPP+AZVvulXmQ==", + "dev": true, + "engines": { + "node": "^18.13.0 || >=20.9.0" + } + }, + "node_modules/@angular/localize": { + "version": "17.3.12", + "resolved": "https://registry.npmjs.org/@angular/localize/-/localize-17.3.12.tgz", + "integrity": "sha512-b7J7zY/CgJhFVPtmu/pEjefU5SHuTy7lQgX6kTrJPaUSJ5i578R17xr4SwrWe7G4jzQwO6GXZZd17a62uNRyOA==", + "dependencies": { + "@babel/core": "7.23.9", + "@types/babel__core": "7.20.5", + "fast-glob": "3.3.2", + "yargs": "^17.2.1" + }, + "bin": { + "localize-extract": "tools/bundles/src/extract/cli.js", + "localize-migrate": "tools/bundles/src/migrate/cli.js", + "localize-translate": "tools/bundles/src/translate/cli.js" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0" + }, + "peerDependencies": { + "@angular/compiler": "17.3.12", + "@angular/compiler-cli": "17.3.12" + } + }, + "node_modules/@angular/localize/node_modules/@babel/core": { + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.9.tgz", + "integrity": "sha512-5q0175NOjddqpvvzU+kDiSOAk4PfdO6FvwCWoQ6RO7rTzEe8vlo+4HVfcnAREhD4npMs0e9uZypjTwzZPCf/cw==", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.23.5", + "@babel/generator": "^7.23.6", + "@babel/helper-compilation-targets": "^7.23.6", + "@babel/helper-module-transforms": "^7.23.3", + "@babel/helpers": "^7.23.9", + "@babel/parser": "^7.23.9", + "@babel/template": "^7.23.9", + "@babel/traverse": "^7.23.9", + "@babel/types": "^7.23.9", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@angular/localize/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==" + }, + "node_modules/@angular/localize/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@angular/platform-browser": { + "version": "17.3.12", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-17.3.12.tgz", + "integrity": "sha512-DYY04ptWh/ulMHzd+y52WCE8QnEYGeIiW3hEIFjCN8z0kbIdFdUtEB0IK5vjNL3ejyhUmphcpeT5PYf3YXtqWQ==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0" + }, + "peerDependencies": { + "@angular/animations": "17.3.12", + "@angular/common": "17.3.12", + "@angular/core": "17.3.12" + }, + "peerDependenciesMeta": { + "@angular/animations": { + "optional": true + } + } + }, + "node_modules/@angular/platform-browser-dynamic": { + "version": "17.3.12", + "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-17.3.12.tgz", + "integrity": "sha512-DQwV7B2x/DRLRDSisngZRdLqHdYbbrqZv2Hmu4ZbnNYaWPC8qvzgE/0CvY+UkDat3nCcsfwsMnlDeB6TL7/IaA==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0" + }, + "peerDependencies": { + "@angular/common": "17.3.12", + "@angular/compiler": "17.3.12", + "@angular/core": "17.3.12", + "@angular/platform-browser": "17.3.12" + } + }, + "node_modules/@angular/platform-server": { + "version": "17.3.12", + "resolved": "https://registry.npmjs.org/@angular/platform-server/-/platform-server-17.3.12.tgz", + "integrity": "sha512-P3xBzyeT2w/iiGsqGUNuLRYdqs2e+5nRnVYU9tc/TjhYDAgwEgq946U7Nie1xq5Ts/8b7bhxcK9maPKWG237Kw==", + "dependencies": { + "tslib": "^2.3.0", + "xhr2": "^0.2.0" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0" + }, + "peerDependencies": { + "@angular/animations": "17.3.12", + "@angular/common": "17.3.12", + "@angular/compiler": "17.3.12", + "@angular/core": "17.3.12", + "@angular/platform-browser": "17.3.12" + } + }, + "node_modules/@angular/router": { + "version": "17.3.12", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-17.3.12.tgz", + "integrity": "sha512-dg7PHBSW9fmPKTVzwvHEeHZPZdpnUqW/U7kj8D29HTP9ur8zZnx9QcnbplwPeYb8yYa62JMnZSEel2X4PxdYBg==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0" + }, + "peerDependencies": { + "@angular/common": "17.3.12", + "@angular/core": "17.3.12", + "@angular/platform-browser": "17.3.12", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/ssr": { + "version": "17.3.11", + "resolved": "https://registry.npmjs.org/@angular/ssr/-/ssr-17.3.11.tgz", + "integrity": "sha512-8AslXZnj5bu0fJrSSoZf202HXptc+vS8hSvEIobK1+UpEVmtrk3StiBxYTdbN4Pe76r7RGRmBt40fHe+88AZoA==", + "dependencies": { + "critters": "0.0.22", + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/common": "^17.0.0", + "@angular/core": "^17.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.25.7.tgz", + "integrity": "sha512-0xZJFNE5XMpENsgfHYTw8FbX4kv53mFLn2i3XPoq69LyhYSCBJtitaHx9QnsVTrsogI4Z3+HtEfZ2/GFPOtf5g==", + "dependencies": { + "@babel/highlight": "^7.25.7", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.8.tgz", + "integrity": "sha512-ZsysZyXY4Tlx+Q53XdnOFmqwfB9QDTHYxaZYajWRoBLuLEAwI2UIbtxOjWh/cFaa9IKUlcB+DDuoskLuKu56JA==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.0.tgz", + "integrity": "sha512-fQfkg0Gjkza3nf0c7/w6Xf34BW4YvzNfACRLmmb7XRLa6XHdR+K9AlJlxneFfWYf6uhOzuzZVTjF/8KfndZANw==", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.23.5", + "@babel/generator": "^7.23.6", + "@babel/helper-compilation-targets": "^7.23.6", + "@babel/helper-module-transforms": "^7.23.3", + "@babel/helpers": "^7.24.0", + "@babel/parser": "^7.24.0", + "@babel/template": "^7.24.0", + "@babel/traverse": "^7.24.0", + "@babel/types": "^7.24.0", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==" + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz", + "integrity": "sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==", + "dependencies": { + "@babel/types": "^7.23.6", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz", + "integrity": "sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-builder-binary-assignment-operator-visitor": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.25.7.tgz", + "integrity": "sha512-12xfNeKNH7jubQNm7PAkzlLwEmCs1tfuX3UjIw6vP6QXi+leKh6+LyC/+Ed4EIQermwd58wsyh070yjDHFlNGg==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.25.7", + "@babel/types": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.7.tgz", + "integrity": "sha512-DniTEax0sv6isaw6qSQSfV4gVRNtw2rte8HHM45t9ZR0xILaufBRNkpMifCRiAPyvL4ACD6v0gfCwCmtOQaV4A==", + "dependencies": { + "@babel/compat-data": "^7.25.7", + "@babel/helper-validator-option": "^7.25.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.25.7.tgz", + "integrity": "sha512-bD4WQhbkx80mAyj/WCm4ZHcF4rDxkoLFO6ph8/5/mQ3z4vAzltQXAmbc7GvVJx5H+lk5Mi5EmbTeox5nMGCsbw==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.25.7", + "@babel/helper-member-expression-to-functions": "^7.25.7", + "@babel/helper-optimise-call-expression": "^7.25.7", + "@babel/helper-replace-supers": "^7.25.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.7", + "@babel/traverse": "^7.25.7", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin/node_modules/@babel/helper-annotate-as-pure": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.7.tgz", + "integrity": "sha512-4xwU8StnqnlIhhioZf1tqnVWeQ9pvH/ujS8hRfw/WOza+/a+1qv69BWNy+oY231maTCWgKWhfBU7kDpsds6zAA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.25.7.tgz", + "integrity": "sha512-byHhumTj/X47wJ6C6eLpK7wW/WBEcnUeb7D0FNc/jFQnQVw7DOso3Zz5u9x/zLrFVkHa89ZGDbkAa1D54NdrCQ==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.25.7", + "regexpu-core": "^6.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/@babel/helper-annotate-as-pure": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.7.tgz", + "integrity": "sha512-4xwU8StnqnlIhhioZf1tqnVWeQ9pvH/ujS8hRfw/WOza+/a+1qv69BWNy+oY231maTCWgKWhfBU7kDpsds6zAA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.2.tgz", + "integrity": "sha512-LV76g+C502biUK6AyZ3LK10vDpDyCzZnhZFXkH1L75zHPj68+qc8Zfpx2th+gzwA2MzyK+1g/3EPl62yFnVttQ==", + "dev": true, + "dependencies": { + "@babel/helper-compilation-targets": "^7.22.6", + "@babel/helper-plugin-utils": "^7.22.5", + "debug": "^4.1.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.14.2" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/helper-environment-visitor": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.7.tgz", + "integrity": "sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.25.7.tgz", + "integrity": "sha512-O31Ssjd5K6lPbTX9AAYpSKrZmLeagt9uwschJd+Ixo6QiRyfpvgtVQp8qrDR9UNFjZ8+DO34ZkdrN+BnPXemeA==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.25.7", + "@babel/types": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.7.tgz", + "integrity": "sha512-o0xCgpNmRohmnoWKQ0Ij8IdddjyBFE4T2kagL/x6M3+4zUgc+4qTOUBoNe4XxDskt1HPKO007ZPiMgLDq2s7Kw==", + "dependencies": { + "@babel/traverse": "^7.25.7", + "@babel/types": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.25.7.tgz", + "integrity": "sha512-k/6f8dKG3yDz/qCwSM+RKovjMix563SLxQFo0UhRNo239SP6n9u5/eLtKD6EAjwta2JHJ49CsD8pms2HdNiMMQ==", + "dependencies": { + "@babel/helper-module-imports": "^7.25.7", + "@babel/helper-simple-access": "^7.25.7", + "@babel/helper-validator-identifier": "^7.25.7", + "@babel/traverse": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.25.7.tgz", + "integrity": "sha512-VAwcwuYhv/AT+Vfr28c9y6SHzTan1ryqrydSTFGjU0uDJHw3uZ+PduI8plCLkRsDnqK2DMEDmwrOQRsK/Ykjng==", + "dev": true, + "dependencies": { + "@babel/types": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.25.7.tgz", + "integrity": "sha512-eaPZai0PiqCi09pPs3pAFfl/zYgGaE6IdXtYvmf0qlcDTd3WCtO7JWCcRd64e0EQrcYgiHibEZnOGsSY4QSgaw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-remap-async-to-generator": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.25.7.tgz", + "integrity": "sha512-kRGE89hLnPfcz6fTrlNU+uhgcwv0mBE4Gv3P9Ke9kLVJYpi4AMVVEElXvB5CabrPZW4nCM8P8UyyjrzCM0O2sw==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.25.7", + "@babel/helper-wrap-function": "^7.25.7", + "@babel/traverse": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-remap-async-to-generator/node_modules/@babel/helper-annotate-as-pure": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.7.tgz", + "integrity": "sha512-4xwU8StnqnlIhhioZf1tqnVWeQ9pvH/ujS8hRfw/WOza+/a+1qv69BWNy+oY231maTCWgKWhfBU7kDpsds6zAA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.25.7.tgz", + "integrity": "sha512-iy8JhqlUW9PtZkd4pHM96v6BdJ66Ba9yWSE4z0W4TvSZwLBPkyDsiIU3ENe4SmrzRBs76F7rQXTy1lYC49n6Lw==", + "dev": true, + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.25.7", + "@babel/helper-optimise-call-expression": "^7.25.7", + "@babel/traverse": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.25.7.tgz", + "integrity": "sha512-FPGAkJmyoChQeM+ruBGIDyrT2tKfZJO8NcxdC+CWNJi7N8/rZpSxK7yvBJ5O/nF1gfu5KzN7VKG3YVSLFfRSxQ==", + "dependencies": { + "@babel/traverse": "^7.25.7", + "@babel/types": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.25.7.tgz", + "integrity": "sha512-pPbNbchZBkPMD50K0p3JGcFMNLVUCuU/ABybm/PGNj4JiHrpmNyqqCphBk4i19xXtNV0JhldQJJtbSW5aUvbyA==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.25.7", + "@babel/types": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.7.tgz", + "integrity": "sha512-CbkjYdsJNHFk8uqpEkpCvRs3YRp9tY6FmFY7wLMSYuGYkrdUi7r2lc4/wqsvlHoMznX3WJ9IP8giGPq68T/Y6g==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.7.tgz", + "integrity": "sha512-AM6TzwYqGChO45oiuPqwL2t20/HdMC1rTPAesnBCgPCSF1x3oN9MVUwQV2iyz4xqWrctwK5RNC8LV22kaQCNYg==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.7.tgz", + "integrity": "sha512-ytbPLsm+GjArDYXJ8Ydr1c/KJuutjF2besPNbIZnZ6MKUxi/uTA22t2ymmA4WFjZFpjiAMO0xuuJPqK2nvDVfQ==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-wrap-function": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.25.7.tgz", + "integrity": "sha512-MA0roW3JF2bD1ptAaJnvcabsVlNQShUaThyJbCDD4bCp8NEgiFvpoqRI2YS22hHlc2thjO/fTg2ShLMC3jygAg==", + "dev": true, + "dependencies": { + "@babel/template": "^7.25.7", + "@babel/traverse": "^7.25.7", + "@babel/types": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.7.tgz", + "integrity": "sha512-NlmJJtvcw72yRJRcnCmGvSi+3jDEg8qFu3z0AFoymmzLx5ERVWyzd9kVXr7Th9/8yIJi2Zc6av4Tqz3wFs8QWg==", + "dependencies": { + "@babel/template": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.25.7.tgz", + "integrity": "sha512-iYyACpW3iW8Fw+ZybQK+drQre+ns/tKpXbNESfrhNnPLIklLbXr7MYJ6gPEd0iETGLOK+SxMjVvKb/ffmk+FEw==", + "dependencies": { + "@babel/helper-validator-identifier": "^7.25.7", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.8.tgz", + "integrity": "sha512-HcttkxzdPucv3nNFmfOOMfFf64KgdJVqm1KaCm25dPGMLElo9nsLvXeJECQg8UzPuBGLyTSA0ZzqCtDSzKTEoQ==", + "dependencies": { + "@babel/types": "^7.25.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.25.7.tgz", + "integrity": "sha512-wxyWg2RYaSUYgmd9MR0FyRGyeOMQE/Uzr1wzd/g5cf5bwi9A4v6HFdDm7y1MgDtod/fLOSTZY6jDgV0xU9d5bA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.25.7.tgz", + "integrity": "sha512-Xwg6tZpLxc4iQjorYsyGMyfJE7nP5MV8t/Ka58BgiA7Jw0fRqQNcANlLfdJ/yvBt9z9LD2We+BEkT7vLqZRWng==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.7", + "@babel/plugin-transform-optional-chaining": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.13.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.25.7.tgz", + "integrity": "sha512-UVATLMidXrnH+GMUIuxq55nejlj02HP7F5ETyBONzP6G87fPBogG4CH6kxrSrdIuAjdwNO9VzyaYsrZPscWUrw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/traverse": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.0-placeholder-for-preset-env.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", + "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", + "dev": true, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-dynamic-import": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", + "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-export-namespace-from": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", + "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-assertions": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.25.7.tgz", + "integrity": "sha512-ZvZQRmME0zfJnDQnVBKYzHxXT7lYBB3Revz1GuS7oLXWMgqUPX4G+DDbT30ICClht9WKV34QVrZhSw6WdklwZQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.25.7.tgz", + "integrity": "sha512-AqVo+dguCgmpi/3mYBdu9lkngOBlQ2w2vnNpa6gfiCxQZLzV4ZbhsXitJ2Yblkoe1VQwtHSaNmIaGll/26YWRw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-unicode-sets-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", + "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.25.7.tgz", + "integrity": "sha512-EJN2mKxDwfOUCPxMO6MUI58RN3ganiRAG/MS/S3HfB6QFNjroAMelQo/gybyYq97WerCBAZoyrAoW8Tzdq2jWg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-generator-functions": { + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.23.9.tgz", + "integrity": "sha512-8Q3veQEDGe14dTYuwagbRtwxQDnytyg1JFu4/HwEMETeofocrB0U0ejBJIXoeG/t2oXZ8kzCyI0ZZfbT80VFNQ==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-remap-async-to-generator": "^7.22.20", + "@babel/plugin-syntax-async-generators": "^7.8.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-to-generator": { + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.23.3.tgz", + "integrity": "sha512-A7LFsKi4U4fomjqXJlZg/u0ft/n8/7n7lpffUP/ZULx/DtV9SGlNKZolHH6PE8Xl1ngCc0M11OaeZptXVkfKSw==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-remap-async-to-generator": "^7.22.20" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoped-functions": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.25.7.tgz", + "integrity": "sha512-xHttvIM9fvqW+0a3tZlYcZYSBpSWzGBFIt/sYG3tcdSzBB8ZeVgz2gBP7Df+sM0N1850jrviYSSeUuc+135dmQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.25.7.tgz", + "integrity": "sha512-ZEPJSkVZaeTFG/m2PARwLZQ+OG0vFIhPlKHK/JdIMy8DbRJ/htz6LRrTFtdzxi9EHmcwbNPAKDnadpNSIW+Aow==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-properties": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.25.7.tgz", + "integrity": "sha512-mhyfEW4gufjIqYFo9krXHJ3ElbFLIze5IDp+wQTxoPd+mwFb1NxatNAwmv8Q8Iuxv7Zc+q8EkiMQwc9IhyGf4g==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-static-block": { + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.25.8.tgz", + "integrity": "sha512-e82gl3TCorath6YLf9xUwFehVvjvfqFhdOo4+0iVIVju+6XOi5XHkqB3P2AXnSwoeTX0HBoXq5gJFtvotJzFnQ==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0" + } + }, + "node_modules/@babel/plugin-transform-classes": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.25.7.tgz", + "integrity": "sha512-9j9rnl+YCQY0IGoeipXvnk3niWicIB6kCsWRGLwX241qSXpbA4MKxtp/EdvFxsc4zI5vqfLxzOd0twIJ7I99zg==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.25.7", + "@babel/helper-compilation-targets": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/helper-replace-supers": "^7.25.7", + "@babel/traverse": "^7.25.7", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-classes/node_modules/@babel/helper-annotate-as-pure": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.7.tgz", + "integrity": "sha512-4xwU8StnqnlIhhioZf1tqnVWeQ9pvH/ujS8hRfw/WOza+/a+1qv69BWNy+oY231maTCWgKWhfBU7kDpsds6zAA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.25.7.tgz", + "integrity": "sha512-QIv+imtM+EtNxg/XBKL3hiWjgdLjMOmZ+XzQwSgmBfKbfxUjBzGgVPklUuE55eq5/uVoh8gg3dqlrwR/jw3ZeA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/template": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.25.7.tgz", + "integrity": "sha512-xKcfLTlJYUczdaM1+epcdh1UGewJqr9zATgrNHcLBcV2QmfvPPEixo/sK/syql9cEmbr7ulu5HMFG5vbbt/sEA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dotall-regex": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.25.7.tgz", + "integrity": "sha512-kXzXMMRzAtJdDEgQBLF4oaiT6ZCU3oWHgpARnTKDAqPkDJ+bs3NrZb310YYevR5QlRo3Kn7dzzIdHbZm1VzJdQ==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-keys": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.25.7.tgz", + "integrity": "sha512-by+v2CjoL3aMnWDOyCIg+yxU9KXSRa9tN6MbqggH5xvymmr9p4AMjYkNlQy4brMceBnUyHZ9G8RnpvT8wP7Cfg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dynamic-import": { + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.25.8.tgz", + "integrity": "sha512-gznWY+mr4ZQL/EWPcbBQUP3BXS5FwZp8RUOw06BaRn8tQLzN4XLIxXejpHN9Qo8x8jjBmAAKp6FoS51AgkSA/A==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-exponentiation-operator": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.25.7.tgz", + "integrity": "sha512-yjqtpstPfZ0h/y40fAXRv2snciYr0OAoMXY/0ClC7tm4C/nG5NJKmIItlaYlLbIVAWNfrYuy9dq1bE0SbX0PEg==", + "dev": true, + "dependencies": { + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-export-namespace-from": { + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.25.8.tgz", + "integrity": "sha512-sPtYrduWINTQTW7FtOy99VCTWp4H23UX7vYcut7S4CIMEXU+54zKX9uCoGkLsWXteyaMXzVHgzWbLfQ1w4GZgw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-for-of": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.25.7.tgz", + "integrity": "sha512-n/TaiBGJxYFWvpJDfsxSj9lEEE44BFM1EPGz4KEiTipTgkoFVVcCmzAL3qA7fdQU96dpo4gGf5HBx/KnDvqiHw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-function-name": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.25.7.tgz", + "integrity": "sha512-5MCTNcjCMxQ63Tdu9rxyN6cAWurqfrDZ76qvVPrGYdBxIj+EawuuxTu/+dgJlhK5eRz3v1gLwp6XwS8XaX2NiQ==", + "dev": true, + "dependencies": { + "@babel/helper-compilation-targets": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/traverse": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-json-strings": { + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.25.8.tgz", + "integrity": "sha512-4OMNv7eHTmJ2YXs3tvxAfa/I43di+VcF+M4Wt66c88EAED1RoGaf1D64cL5FkRpNL+Vx9Hds84lksWvd/wMIdA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-literals": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.25.7.tgz", + "integrity": "sha512-fwzkLrSu2fESR/cm4t6vqd7ebNIopz2QHGtjoU+dswQo/P6lwAG04Q98lliE3jkz/XqnbGFLnUcE0q0CVUf92w==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-logical-assignment-operators": { + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.25.8.tgz", + "integrity": "sha512-f5W0AhSbbI+yY6VakT04jmxdxz+WsID0neG7+kQZbCOjuyJNdL5Nn4WIBm4hRpKnUcO9lP0eipUhFN12JpoH8g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-member-expression-literals": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.25.7.tgz", + "integrity": "sha512-Std3kXwpXfRV0QtQy5JJcRpkqP8/wG4XL7hSKZmGlxPlDqmpXtEPRmhF7ztnlTCtUN3eXRUJp+sBEZjaIBVYaw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-amd": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.25.7.tgz", + "integrity": "sha512-CgselSGCGzjQvKzghCvDTxKHP3iooenLpJDO842ehn5D2G5fJB222ptnDwQho0WjEvg7zyoxb9P+wiYxiJX5yA==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.25.7.tgz", + "integrity": "sha512-L9Gcahi0kKFYXvweO6n0wc3ZG1ChpSFdgG+eV1WYZ3/dGbJK7vvk91FgGgak8YwRgrCuihF8tE/Xg07EkL5COg==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/helper-simple-access": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-systemjs": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.25.7.tgz", + "integrity": "sha512-t9jZIvBmOXJsiuyOwhrIGs8dVcD6jDyg2icw1VL4A/g+FnWyJKwUfSSU2nwJuMV2Zqui856El9u+ElB+j9fV1g==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/helper-validator-identifier": "^7.25.7", + "@babel/traverse": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-umd": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.25.7.tgz", + "integrity": "sha512-p88Jg6QqsaPh+EB7I9GJrIqi1Zt4ZBHUQtjw3z1bzEXcLh6GfPqzZJ6G+G1HBGKUNukT58MnKG7EN7zXQBCODw==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.25.7.tgz", + "integrity": "sha512-BtAT9LzCISKG3Dsdw5uso4oV1+v2NlVXIIomKJgQybotJY3OwCwJmkongjHgwGKoZXd0qG5UZ12JUlDQ07W6Ow==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-new-target": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.25.7.tgz", + "integrity": "sha512-CfCS2jDsbcZaVYxRFo2qtavW8SpdzmBXC2LOI4oO0rP+JSRDxxF3inF4GcPsLgfb5FjkhXG5/yR/lxuRs2pySA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.25.8.tgz", + "integrity": "sha512-Z7WJJWdQc8yCWgAmjI3hyC+5PXIubH9yRKzkl9ZEG647O9szl9zvmKLzpbItlijBnVhTUf1cpyWBsZ3+2wjWPQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-numeric-separator": { + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.25.8.tgz", + "integrity": "sha512-rm9a5iEFPS4iMIy+/A/PiS0QN0UyjPIeVvbU5EMZFKJZHt8vQnasbpo3T3EFcxzCeYO0BHfc4RqooCZc51J86Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-rest-spread": { + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.25.8.tgz", + "integrity": "sha512-LkUu0O2hnUKHKE7/zYOIjByMa4VRaV2CD/cdGz0AxU9we+VA3kDDggKEzI0Oz1IroG+6gUP6UmWEHBMWZU316g==", + "dev": true, + "dependencies": { + "@babel/helper-compilation-targets": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/plugin-transform-parameters": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-super": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.25.7.tgz", + "integrity": "sha512-pWT6UXCEW3u1t2tcAGtE15ornCBvopHj9Bps9D2DsH15APgNVOTwwczGckX+WkAvBmuoYKRCFa4DK+jM8vh5AA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/helper-replace-supers": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-catch-binding": { + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.25.8.tgz", + "integrity": "sha512-EbQYweoMAHOn7iJ9GgZo14ghhb9tTjgOc88xFgYngifx7Z9u580cENCV159M4xDh3q/irbhSjZVpuhpC2gKBbg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-chaining": { + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.25.8.tgz", + "integrity": "sha512-q05Bk7gXOxpTHoQ8RSzGSh/LHVB9JEIkKnk3myAWwZHnYiTGYtbdrYkIsS8Xyh4ltKf7GNUSgzs/6P2bJtBAQg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-parameters": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.25.7.tgz", + "integrity": "sha512-FYiTvku63me9+1Nz7TOx4YMtW3tWXzfANZtrzHhUZrz4d47EEtMQhzFoZWESfXuAMMT5mwzD4+y1N8ONAX6lMQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-methods": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.25.7.tgz", + "integrity": "sha512-KY0hh2FluNxMLwOCHbxVOKfdB5sjWG4M183885FmaqWWiGMhRZq4DQRKH6mHdEucbJnyDyYiZNwNG424RymJjA==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-property-in-object": { + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.25.8.tgz", + "integrity": "sha512-8Uh966svuB4V8RHHg0QJOB32QK287NBksJOByoKmHMp1TAobNniNalIkI2i5IPj5+S9NYCG4VIjbEuiSN8r+ow==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.25.7", + "@babel/helper-create-class-features-plugin": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-property-in-object/node_modules/@babel/helper-annotate-as-pure": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.7.tgz", + "integrity": "sha512-4xwU8StnqnlIhhioZf1tqnVWeQ9pvH/ujS8hRfw/WOza+/a+1qv69BWNy+oY231maTCWgKWhfBU7kDpsds6zAA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/plugin-transform-property-literals": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.25.7.tgz", + "integrity": "sha512-lQEeetGKfFi0wHbt8ClQrUSUMfEeI3MMm74Z73T9/kuz990yYVtfofjf3NuA42Jy3auFOpbjDyCSiIkTs1VIYw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.25.7.tgz", + "integrity": "sha512-mgDoQCRjrY3XK95UuV60tZlFCQGXEtMg8H+IsW72ldw1ih1jZhzYXbJvghmAEpg5UVhhnCeia1CkGttUvCkiMQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7", + "regenerator-transform": "^0.15.2" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-reserved-words": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.25.7.tgz", + "integrity": "sha512-3OfyfRRqiGeOvIWSagcwUTVk2hXBsr/ww7bLn6TRTuXnexA+Udov2icFOxFX9abaj4l96ooYkcNN1qi2Zvqwng==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-runtime": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.24.0.tgz", + "integrity": "sha512-zc0GA5IitLKJrSfXlXmp8KDqLrnGECK7YRfQBmEKg1NmBOQ7e+KuclBEKJgzifQeUYLdNiAw4B4bjyvzWVLiSA==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.22.15", + "@babel/helper-plugin-utils": "^7.24.0", + "babel-plugin-polyfill-corejs2": "^0.4.8", + "babel-plugin-polyfill-corejs3": "^0.9.0", + "babel-plugin-polyfill-regenerator": "^0.5.5", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-runtime/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/plugin-transform-shorthand-properties": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.25.7.tgz", + "integrity": "sha512-uBbxNwimHi5Bv3hUccmOFlUy3ATO6WagTApenHz9KzoIdn0XeACdB12ZJ4cjhuB2WSi80Ez2FWzJnarccriJeA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-spread": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.25.7.tgz", + "integrity": "sha512-Mm6aeymI0PBh44xNIv/qvo8nmbkpZze1KvR8MkEqbIREDxoiWTi18Zr2jryfRMwDfVZF9foKh060fWgni44luw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-sticky-regex": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.25.7.tgz", + "integrity": "sha512-ZFAeNkpGuLnAQ/NCsXJ6xik7Id+tHuS+NT+ue/2+rn/31zcdnupCdmunOizEaP0JsUmTFSTOPoQY7PkK2pttXw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-template-literals": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.25.7.tgz", + "integrity": "sha512-SI274k0nUsFFmyQupiO7+wKATAmMFf8iFgq2O+vVFXZ0SV9lNfT1NGzBEhjquFmD8I9sqHLguH+gZVN3vww2AA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typeof-symbol": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.25.7.tgz", + "integrity": "sha512-OmWmQtTHnO8RSUbL0NTdtpbZHeNTnm68Gj5pA4Y2blFNh+V4iZR68V1qL9cI37J21ZN7AaCnkfdHtLExQPf2uA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-escapes": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.25.7.tgz", + "integrity": "sha512-BN87D7KpbdiABA+t3HbVqHzKWUDN3dymLaTnPFAMyc8lV+KN3+YzNhVRNdinaCPA4AUqx7ubXbQ9shRjYBl3SQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-property-regex": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.25.7.tgz", + "integrity": "sha512-IWfR89zcEPQGB/iB408uGtSPlQd3Jpq11Im86vUgcmSTcoWAiQMCTOa2K2yNNqFJEBVICKhayctee65Ka8OB0w==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-regex": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.25.7.tgz", + "integrity": "sha512-8JKfg/hiuA3qXnlLx8qtv5HWRbgyFx2hMMtpDDuU2rTckpKkGu4ycK5yYHwuEa16/quXfoxHBIApEsNyMWnt0g==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-sets-regex": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.25.7.tgz", + "integrity": "sha512-YRW8o9vzImwmh4Q3Rffd09bH5/hvY0pxg+1H1i0f7APoUeg12G7+HhLj9ZFNIrYkgBXhIijPJ+IXypN0hLTIbw==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/preset-env": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.24.0.tgz", + "integrity": "sha512-ZxPEzV9IgvGn73iK0E6VB9/95Nd7aMFpbE0l8KQFDG70cOV9IxRP7Y2FUPmlK0v6ImlLqYX50iuZ3ZTVhOF2lA==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.23.5", + "@babel/helper-compilation-targets": "^7.23.6", + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/helper-validator-option": "^7.23.5", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.23.3", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.23.3", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.23.7", + "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3", + "@babel/plugin-syntax-import-assertions": "^7.23.3", + "@babel/plugin-syntax-import-attributes": "^7.23.3", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5", + "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", + "@babel/plugin-transform-arrow-functions": "^7.23.3", + "@babel/plugin-transform-async-generator-functions": "^7.23.9", + "@babel/plugin-transform-async-to-generator": "^7.23.3", + "@babel/plugin-transform-block-scoped-functions": "^7.23.3", + "@babel/plugin-transform-block-scoping": "^7.23.4", + "@babel/plugin-transform-class-properties": "^7.23.3", + "@babel/plugin-transform-class-static-block": "^7.23.4", + "@babel/plugin-transform-classes": "^7.23.8", + "@babel/plugin-transform-computed-properties": "^7.23.3", + "@babel/plugin-transform-destructuring": "^7.23.3", + "@babel/plugin-transform-dotall-regex": "^7.23.3", + "@babel/plugin-transform-duplicate-keys": "^7.23.3", + "@babel/plugin-transform-dynamic-import": "^7.23.4", + "@babel/plugin-transform-exponentiation-operator": "^7.23.3", + "@babel/plugin-transform-export-namespace-from": "^7.23.4", + "@babel/plugin-transform-for-of": "^7.23.6", + "@babel/plugin-transform-function-name": "^7.23.3", + "@babel/plugin-transform-json-strings": "^7.23.4", + "@babel/plugin-transform-literals": "^7.23.3", + "@babel/plugin-transform-logical-assignment-operators": "^7.23.4", + "@babel/plugin-transform-member-expression-literals": "^7.23.3", + "@babel/plugin-transform-modules-amd": "^7.23.3", + "@babel/plugin-transform-modules-commonjs": "^7.23.3", + "@babel/plugin-transform-modules-systemjs": "^7.23.9", + "@babel/plugin-transform-modules-umd": "^7.23.3", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.22.5", + "@babel/plugin-transform-new-target": "^7.23.3", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.23.4", + "@babel/plugin-transform-numeric-separator": "^7.23.4", + "@babel/plugin-transform-object-rest-spread": "^7.24.0", + "@babel/plugin-transform-object-super": "^7.23.3", + "@babel/plugin-transform-optional-catch-binding": "^7.23.4", + "@babel/plugin-transform-optional-chaining": "^7.23.4", + "@babel/plugin-transform-parameters": "^7.23.3", + "@babel/plugin-transform-private-methods": "^7.23.3", + "@babel/plugin-transform-private-property-in-object": "^7.23.4", + "@babel/plugin-transform-property-literals": "^7.23.3", + "@babel/plugin-transform-regenerator": "^7.23.3", + "@babel/plugin-transform-reserved-words": "^7.23.3", + "@babel/plugin-transform-shorthand-properties": "^7.23.3", + "@babel/plugin-transform-spread": "^7.23.3", + "@babel/plugin-transform-sticky-regex": "^7.23.3", + "@babel/plugin-transform-template-literals": "^7.23.3", + "@babel/plugin-transform-typeof-symbol": "^7.23.3", + "@babel/plugin-transform-unicode-escapes": "^7.23.3", + "@babel/plugin-transform-unicode-property-regex": "^7.23.3", + "@babel/plugin-transform-unicode-regex": "^7.23.3", + "@babel/plugin-transform-unicode-sets-regex": "^7.23.3", + "@babel/preset-modules": "0.1.6-no-external-plugins", + "babel-plugin-polyfill-corejs2": "^0.4.8", + "babel-plugin-polyfill-corejs3": "^0.9.0", + "babel-plugin-polyfill-regenerator": "^0.5.5", + "core-js-compat": "^3.31.0", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-env/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/preset-modules": { + "version": "0.1.6-no-external-plugins", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", + "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.0.tgz", + "integrity": "sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.7.tgz", + "integrity": "sha512-wRwtAgI3bAS+JGU2upWNL9lSlDcRCqD05BZ1n3X2ONLH1WilFP6O1otQjeMK/1g0pvYcXC7b/qVUB1keofjtZA==", + "dependencies": { + "@babel/code-frame": "^7.25.7", + "@babel/parser": "^7.25.7", + "@babel/types": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.7.tgz", + "integrity": "sha512-jatJPT1Zjqvh/1FyJs6qAHL+Dzb7sTb+xr7Q+gM1b+1oBsMsQQ4FkVKb6dFlJvLlVssqkRzV05Jzervt9yhnzg==", + "dependencies": { + "@babel/code-frame": "^7.25.7", + "@babel/generator": "^7.25.7", + "@babel/parser": "^7.25.7", + "@babel/template": "^7.25.7", + "@babel/types": "^7.25.7", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/@babel/generator": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.7.tgz", + "integrity": "sha512-5Dqpl5fyV9pIAD62yK9P7fcA768uVPUyrQmqpqstHWgMma4feF1x/oFysBCVZLY5wJ2GkMUCdsNDnGZrPoR6rA==", + "dependencies": { + "@babel/types": "^7.25.7", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/jsesc": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", + "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@babel/types": { + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.8.tgz", + "integrity": "sha512-JWtuCu8VQsMladxVz/P4HzHUGCAwpuqacmowgXFs5XjxIgKuNjnLokQzuVjlTvIzODaDmpjT3oxcC48vyk9EWg==", + "dependencies": { + "@babel/helper-string-parser": "^7.25.7", + "@babel/helper-validator-identifier": "^7.25.7", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "dev": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@csstools/postcss-cascade-layers": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-cascade-layers/-/postcss-cascade-layers-1.1.1.tgz", + "integrity": "sha512-+KdYrpKC5TgomQr2DlZF4lDEpHcoxnj5IGddYYfBWJAKfj1JtuHUIqMa+E1pJJ+z3kvDViWMqyqPlG4Ja7amQA==", + "dev": true, + "dependencies": { + "@csstools/selector-specificity": "^2.0.2", + "postcss-selector-parser": "^6.0.10" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/@csstools/postcss-color-function": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-color-function/-/postcss-color-function-1.1.1.tgz", + "integrity": "sha512-Bc0f62WmHdtRDjf5f3e2STwRAl89N2CLb+9iAwzrv4L2hncrbDwnQD9PCq0gtAt7pOI2leIV08HIBUd4jxD8cw==", + "dev": true, + "dependencies": { + "@csstools/postcss-progressive-custom-properties": "^1.1.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/@csstools/postcss-font-format-keywords": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-font-format-keywords/-/postcss-font-format-keywords-1.0.1.tgz", + "integrity": "sha512-ZgrlzuUAjXIOc2JueK0X5sZDjCtgimVp/O5CEqTcs5ShWBa6smhWYbS0x5cVc/+rycTDbjjzoP0KTDnUneZGOg==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/@csstools/postcss-hwb-function": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@csstools/postcss-hwb-function/-/postcss-hwb-function-1.0.2.tgz", + "integrity": "sha512-YHdEru4o3Rsbjmu6vHy4UKOXZD+Rn2zmkAmLRfPet6+Jz4Ojw8cbWxe1n42VaXQhD3CQUXXTooIy8OkVbUcL+w==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/@csstools/postcss-ic-unit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-ic-unit/-/postcss-ic-unit-1.0.1.tgz", + "integrity": "sha512-Ot1rcwRAaRHNKC9tAqoqNZhjdYBzKk1POgWfhN4uCOE47ebGcLRqXjKkApVDpjifL6u2/55ekkpnFcp+s/OZUw==", + "dev": true, + "dependencies": { + "@csstools/postcss-progressive-custom-properties": "^1.1.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/@csstools/postcss-is-pseudo-class": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@csstools/postcss-is-pseudo-class/-/postcss-is-pseudo-class-2.0.7.tgz", + "integrity": "sha512-7JPeVVZHd+jxYdULl87lvjgvWldYu+Bc62s9vD/ED6/QTGjy0jy0US/f6BG53sVMTBJ1lzKZFpYmofBN9eaRiA==", + "dev": true, + "dependencies": { + "@csstools/selector-specificity": "^2.0.0", + "postcss-selector-parser": "^6.0.10" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/@csstools/postcss-nested-calc": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-nested-calc/-/postcss-nested-calc-1.0.0.tgz", + "integrity": "sha512-JCsQsw1wjYwv1bJmgjKSoZNvf7R6+wuHDAbi5f/7MbFhl2d/+v+TvBTU4BJH3G1X1H87dHl0mh6TfYogbT/dJQ==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/@csstools/postcss-normalize-display-values": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-normalize-display-values/-/postcss-normalize-display-values-1.0.1.tgz", + "integrity": "sha512-jcOanIbv55OFKQ3sYeFD/T0Ti7AMXc9nM1hZWu8m/2722gOTxFg7xYu4RDLJLeZmPUVQlGzo4jhzvTUq3x4ZUw==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/@csstools/postcss-oklab-function": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-oklab-function/-/postcss-oklab-function-1.1.1.tgz", + "integrity": "sha512-nJpJgsdA3dA9y5pgyb/UfEzE7W5Ka7u0CX0/HIMVBNWzWemdcTH3XwANECU6anWv/ao4vVNLTMxhiPNZsTK6iA==", + "dev": true, + "dependencies": { + "@csstools/postcss-progressive-custom-properties": "^1.1.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/@csstools/postcss-progressive-custom-properties": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-progressive-custom-properties/-/postcss-progressive-custom-properties-1.3.0.tgz", + "integrity": "sha512-ASA9W1aIy5ygskZYuWams4BzafD12ULvSypmaLJT2jvQ8G0M3I8PRQhC0h7mG0Z3LI05+agZjqSR9+K9yaQQjA==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.3" + } + }, + "node_modules/@csstools/postcss-stepped-value-functions": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-stepped-value-functions/-/postcss-stepped-value-functions-1.0.1.tgz", + "integrity": "sha512-dz0LNoo3ijpTOQqEJLY8nyaapl6umbmDcgj4AD0lgVQ572b2eqA1iGZYTTWhrcrHztWDDRAX2DGYyw2VBjvCvQ==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/@csstools/postcss-text-decoration-shorthand": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-text-decoration-shorthand/-/postcss-text-decoration-shorthand-1.0.0.tgz", + "integrity": "sha512-c1XwKJ2eMIWrzQenN0XbcfzckOLLJiczqy+YvfGmzoVXd7pT9FfObiSEfzs84bpE/VqfpEuAZ9tCRbZkZxxbdw==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/@csstools/postcss-trigonometric-functions": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@csstools/postcss-trigonometric-functions/-/postcss-trigonometric-functions-1.0.2.tgz", + "integrity": "sha512-woKaLO///4bb+zZC2s80l+7cm07M7268MsyG3M0ActXXEFi6SuhvriQYcb58iiKGbjwwIU7n45iRLEHypB47Og==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/@csstools/postcss-unset-value": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@csstools/postcss-unset-value/-/postcss-unset-value-1.0.2.tgz", + "integrity": "sha512-c8J4roPBILnelAsdLr4XOAR/GsTm0GJi4XpcfvoWk3U6KiTCqiFYc63KhRMQQX35jYMp4Ao8Ij9+IZRgMfJp1g==", + "dev": true, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/@csstools/selector-specificity": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-2.2.0.tgz", + "integrity": "sha512-+OJ9konv95ClSTOJCmMZqpd5+YGsB2S+x6w3E1oaM8UuR5j8nTNHYSz8c9BEPGDOCMQYIEEGlVPj/VY64iTbGw==", + "dev": true, + "engines": { + "node": "^14 || ^16 || >=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss-selector-parser": "^6.0.10" + } + }, + "node_modules/@cypress/request": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.6.tgz", + "integrity": "sha512-fi0eVdCOtKu5Ed6+E8mYxUF6ZTFJDZvHogCBelM0xVXmrDEkyM22gRArQzq1YcHPm1V47Vf/iAD+WgVdUlJCGg==", + "dev": true, + "dependencies": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~4.0.0", + "http-signature": "~1.4.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "performance-now": "^2.1.0", + "qs": "6.13.0", + "safe-buffer": "^5.1.2", + "tough-cookie": "^5.0.0", + "tunnel-agent": "^0.6.0", + "uuid": "^8.3.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@cypress/schematic": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@cypress/schematic/-/schematic-1.7.0.tgz", + "integrity": "sha512-CouQrVlZ+uHVVBQtmNoMYU9LyoSAmQTOLDpVjrdTdMPpJH1mWnHCL5OCMt+FZLR+43KRiWEvDUjNqSza11oGsQ==", + "dev": true, + "dependencies": { + "@angular-devkit/architect": "^0.1202.10", + "@angular-devkit/core": "^12.2.17", + "@angular-devkit/schematics": "^12.2.17", + "@schematics/angular": "^12.2.17", + "jsonc-parser": "^3.0.0", + "rxjs": "~6.6.0" + }, + "peerDependencies": { + "@angular/cli": ">=12", + "@angular/core": ">=12" + } + }, + "node_modules/@cypress/schematic/node_modules/@angular-devkit/architect": { + "version": "0.1202.18", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1202.18.tgz", + "integrity": "sha512-C4ASKe+xBjl91MJyHDLt3z7ICPF9FU6B0CeJ1phwrlSHK9lmFG99WGxEj/Tc82+vHyPhajqS5XJ38KyVAPBGzA==", + "dev": true, + "dependencies": { + "@angular-devkit/core": "12.2.18", + "rxjs": "6.6.7" + }, + "engines": { + "node": "^12.14.1 || >=14.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@cypress/schematic/node_modules/@angular-devkit/core": { + "version": "12.2.18", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-12.2.18.tgz", + "integrity": "sha512-GDLHGe9HEY5SRS+NrKr14C8aHsRCiBFkBFSSbeohgLgcgSXzZHFoU84nDWrl3KZNP8oqcUSv5lHu6dLcf2fnww==", + "dev": true, + "dependencies": { + "ajv": "8.6.2", + "ajv-formats": "2.1.0", + "fast-json-stable-stringify": "2.1.0", + "magic-string": "0.25.7", + "rxjs": "6.6.7", + "source-map": "0.7.3" + }, + "engines": { + "node": "^12.14.1 || >=14.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@cypress/schematic/node_modules/@angular-devkit/schematics": { + "version": "12.2.18", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-12.2.18.tgz", + "integrity": "sha512-bZ9NS5PgoVfetRC6WeQBHCY5FqPZ9y2TKHUo12sOB2YSL3tgWgh1oXyP8PtX34gasqsLjNULxEQsAQYEsiX/qQ==", + "dev": true, + "dependencies": { + "@angular-devkit/core": "12.2.18", + "ora": "5.4.1", + "rxjs": "6.6.7" + }, + "engines": { + "node": "^12.14.1 || >=14.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@cypress/schematic/node_modules/@schematics/angular": { + "version": "12.2.18", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-12.2.18.tgz", + "integrity": "sha512-niRS9Ly9y8uI0YmTSbo8KpdqCCiZ/ATMZWeS2id5M8JZvfXbngwiqJvojdSol0SWU+n1W4iA+lJBdt4gSKlD5w==", + "dev": true, + "dependencies": { + "@angular-devkit/core": "12.2.18", + "@angular-devkit/schematics": "12.2.18", + "jsonc-parser": "3.0.0" + }, + "engines": { + "node": "^12.14.1 || >=14.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@cypress/schematic/node_modules/ajv": { + "version": "8.6.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.6.2.tgz", + "integrity": "sha512-9807RlWAgT564wT+DjeyU5OFMPjmzxVobvDFmNAhY+5zD6A2ly3jDp6sgnfyDtlIQ+7H97oc/DGCzzfu9rjw9w==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@cypress/schematic/node_modules/ajv-formats": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.0.tgz", + "integrity": "sha512-USH2jBb+C/hIpwD2iRjp0pe0k+MvzG0mlSn/FIdCgQhUb9ALPRjt2KIQdfZDS9r0ZIeUAg7gOu9KL0PFqGqr5Q==", + "dev": true, + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/@cypress/schematic/node_modules/jsonc-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.0.0.tgz", + "integrity": "sha512-fQzRfAbIBnR0IQvftw9FJveWiHp72Fg20giDrHz6TdfB12UH/uue0D3hm57UB5KgAVuniLMCaS8P1IMj9NR7cA==", + "dev": true + }, + "node_modules/@cypress/schematic/node_modules/magic-string": { + "version": "0.25.7", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz", + "integrity": "sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA==", + "dev": true, + "dependencies": { + "sourcemap-codec": "^1.4.4" + } + }, + "node_modules/@cypress/schematic/node_modules/rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "dev": true, + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" + } + }, + "node_modules/@cypress/schematic/node_modules/source-map": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", + "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@cypress/schematic/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "node_modules/@cypress/xvfb": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@cypress/xvfb/-/xvfb-1.2.4.tgz", + "integrity": "sha512-skbBzPggOVYCbnGgV+0dmBdW/s77ZkAOXIC1knS8NagwDjBrNC1LuXtQJeiN6l+m7lzmHtaoUw/ctJKdqkG57Q==", + "dev": true, + "dependencies": { + "debug": "^3.1.0", + "lodash.once": "^4.1.1" + } + }, + "node_modules/@cypress/xvfb/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/@discoveryjs/json-ext": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", + "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", + "dev": true, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@edsilv/http-status-codes": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@edsilv/http-status-codes/-/http-status-codes-1.0.3.tgz", + "integrity": "sha512-HLK2FS5sZqxPqD53D6hhZxC6C8THTVwlyZDZ7J0iWsrB8JmMA69m/CQuNKZc1kki9WSVeck2fXna26NL0SE7cg==" + }, + "node_modules/@emotion/hash": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz", + "integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==" + }, + "node_modules/@es-joy/jsdoccomment": { + "version": "0.39.4", + "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.39.4.tgz", + "integrity": "sha512-Jvw915fjqQct445+yron7Dufix9A+m9j1fCJYlCo1FWlRvTxa3pjJelxdSTdaLWcTwRU6vbL+NYjO4YuNIS5Qg==", + "dev": true, + "dependencies": { + "comment-parser": "1.3.1", + "esquery": "^1.5.0", + "jsdoc-type-pratt-parser": "~4.0.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.1.tgz", + "integrity": "sha512-m55cpeupQ2DbuRGQMMZDzbv9J9PgVelPjlcmM5kxHnrBdBx6REaEd7LamYV7Dm8N7rCyR/XwU6rVP8ploKtIkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.1.tgz", + "integrity": "sha512-4j0+G27/2ZXGWR5okcJi7pQYhmkVgb4D7UKwxcqrjhvp5TKWx3cUjgB1CGj1mfdmJBQ9VnUGgUhign+FPF2Zgw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.1.tgz", + "integrity": "sha512-hCnXNF0HM6AjowP+Zou0ZJMWWa1VkD77BXe959zERgGJBBxB+sV+J9f/rcjeg2c5bsukD/n17RKWXGFCO5dD5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.1.tgz", + "integrity": "sha512-MSfZMBoAsnhpS+2yMFYIQUPs8Z19ajwfuaSZx+tSl09xrHZCjbeXXMsUF/0oq7ojxYEpsSo4c0SfjxOYXRbpaA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.1.tgz", + "integrity": "sha512-Ylk6rzgMD8klUklGPzS414UQLa5NPXZD5tf8JmQU8GQrj6BrFA/Ic9tb2zRe1kOZyCbGl+e8VMbDRazCEBqPvA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.1.tgz", + "integrity": "sha512-pFIfj7U2w5sMp52wTY1XVOdoxw+GDwy9FsK3OFz4BpMAjvZVs0dT1VXs8aQm22nhwoIWUmIRaE+4xow8xfIDZA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.1.tgz", + "integrity": "sha512-UyW1WZvHDuM4xDz0jWun4qtQFauNdXjXOtIy7SYdf7pbxSWWVlqhnR/T2TpX6LX5NI62spt0a3ldIIEkPM6RHw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.1.tgz", + "integrity": "sha512-itPwCw5C+Jh/c624vcDd9kRCCZVpzpQn8dtwoYIt2TJF3S9xJLiRohnnNrKwREvcZYx0n8sCSbvGH349XkcQeg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.1.tgz", + "integrity": "sha512-LojC28v3+IhIbfQ+Vu4Ut5n3wKcgTu6POKIHN9Wpt0HnfgUGlBuyDDQR4jWZUZFyYLiz4RBBBmfU6sNfn6RhLw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.1.tgz", + "integrity": "sha512-cX8WdlF6Cnvw/DO9/X7XLH2J6CkBnz7Twjpk56cshk9sjYVcuh4sXQBy5bmTwzBjNVZze2yaV1vtcJS04LbN8w==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.1.tgz", + "integrity": "sha512-4H/sQCy1mnnGkUt/xszaLlYJVTz3W9ep52xEefGtd6yXDQbz/5fZE5dFLUgsPdbUOQANcVUa5iO6g3nyy5BJiw==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.1.tgz", + "integrity": "sha512-c0jgtB+sRHCciVXlyjDcWb2FUuzlGVRwGXgI+3WqKOIuoo8AmZAddzeOHeYLtD+dmtHw3B4Xo9wAUdjlfW5yYA==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.1.tgz", + "integrity": "sha512-TgFyCfIxSujyuqdZKDZ3yTwWiGv+KnlOeXXitCQ+trDODJ+ZtGOzLkSWngynP0HZnTsDyBbPy7GWVXWaEl6lhA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.1.tgz", + "integrity": "sha512-b+yuD1IUeL+Y93PmFZDZFIElwbmFfIKLKlYI8M6tRyzE6u7oEP7onGk0vZRh8wfVGC2dZoy0EqX1V8qok4qHaw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.1.tgz", + "integrity": "sha512-wpDlpE0oRKZwX+GfomcALcouqjjV8MIX8DyTrxfyCfXxoKQSDm45CZr9fanJ4F6ckD4yDEPT98SrjvLwIqUCgg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.1.tgz", + "integrity": "sha512-5BepC2Au80EohQ2dBpyTquqGCES7++p7G+7lXe1bAIvMdXm4YYcEfZtQrP4gaoZ96Wv1Ute61CEHFU7h4FMueQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.1.tgz", + "integrity": "sha512-5gRPk7pKuaIB+tmH+yKd2aQTRpqlf1E4f/mC+tawIm/CGJemZcHZpp2ic8oD83nKgUPMEd0fNanrnFljiruuyA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.1.tgz", + "integrity": "sha512-4fL68JdrLV2nVW2AaWZBv3XEm3Ae3NZn/7qy2KGAt3dexAgSVT+Hc97JKSZnqezgMlv9x6KV0ZkZY7UO5cNLCg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.1.tgz", + "integrity": "sha512-GhRuXlvRE+twf2ES+8REbeCb/zeikNqwD3+6S5y5/x+DYbAQUNl0HNBs4RQJqrechS4v4MruEr8ZtAin/hK5iw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.1.tgz", + "integrity": "sha512-ZnWEyCM0G1Ex6JtsygvC3KUUrlDXqOihw8RicRuQAzw+c4f1D66YlPNNV3rkjVW90zXVsHwZYWbJh3v+oQFM9Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.1.tgz", + "integrity": "sha512-QZ6gXue0vVQY2Oon9WyLFCdSuYbXSoxaZrPuJ4c20j6ICedfsDilNPYfHLlMH7vGfU5DQR0czHLmJvH4Nzis/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.1.tgz", + "integrity": "sha512-HzcJa1NcSWTAU0MJIxOho8JftNp9YALui3o+Ny7hCh0v5f90nprly1U3Sj1Ldj/CvKKdvvFsCRvDkpsEMp4DNw==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.1.tgz", + "integrity": "sha512-0MBh53o6XtI6ctDnRMeQ+xoCN8kD2qI1rY1KgF/xdWQwoFeKou7puvDfV8/Wv4Ctx2rRpET/gGdz3YlNtNACSA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.0.tgz", + "integrity": "sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/eslintrc/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", + "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@fortawesome/fontawesome-free": { + "version": "6.7.2", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.7.2.tgz", + "integrity": "sha512-JUOtgFW6k9u4Y+xeIaEiLr3+cjoUPiAuLXoyKOJSia6Duzb7pq+A76P9ZdPDoAoxHdHzq6gE9/jKBGXlZT8FbA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/@gar/promisify": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", + "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", + "dev": true + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.14", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", + "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.2", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true + }, + "node_modules/@iiif/vocabulary": { + "version": "1.0.26", + "resolved": "https://registry.npmjs.org/@iiif/vocabulary/-/vocabulary-1.0.26.tgz", + "integrity": "sha512-yOsMDg5C90iMfD5HSydoTDzmOM/ki5zGiu4DbHpzRueM7D+12IcDHeai2A8QvEroS8HCJl5M1Edbju5rOlPIpg==" + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", + "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@kolkov/ngx-gallery": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@kolkov/ngx-gallery/-/ngx-gallery-2.0.1.tgz", + "integrity": "sha512-mTigRy9Ha7bqCF/+GNKeW2Oe8ZILuM5GGMw+ZbvTQWq3X5hngeFFgv8GFG49Py3biX67kb0NhqCP+msLe4wbXQ==", + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/animations": ">=13.0.0 <14", + "@angular/common": ">=13.0.0 <14", + "@angular/core": ">=13.0.0 <14" + } + }, + "node_modules/@leichtgewicht/ip-codec": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", + "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==", + "dev": true + }, + "node_modules/@ljharb/through": { + "version": "2.3.13", + "resolved": "https://registry.npmjs.org/@ljharb/through/-/through-2.3.13.tgz", + "integrity": "sha512-/gKJun8NNiWGZJkGzI/Ragc53cOdcLNdzjLaIa+GEjguQs0ulsurx8WN0jijdK9yPqDvziX995sMRLyLt1uZMQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/@material-ui/core": { + "version": "4.12.4", + "resolved": "https://registry.npmjs.org/@material-ui/core/-/core-4.12.4.tgz", + "integrity": "sha512-tr7xekNlM9LjA6pagJmL8QCgZXaubWUwkJnoYcMKd4gw/t4XiyvnTkjdGrUVicyB2BsdaAv1tvow45bPM4sSwQ==", + "deprecated": "Material UI v4 doesn't receive active development since September 2021. See the guide https://mui.com/material-ui/migration/migration-v4/ to upgrade to v5.", + "dependencies": { + "@babel/runtime": "^7.4.4", + "@material-ui/styles": "^4.11.5", + "@material-ui/system": "^4.12.2", + "@material-ui/types": "5.1.0", + "@material-ui/utils": "^4.11.3", + "@types/react-transition-group": "^4.2.0", + "clsx": "^1.0.4", + "hoist-non-react-statics": "^3.3.2", + "popper.js": "1.16.1-lts", + "prop-types": "^15.7.2", + "react-is": "^16.8.0 || ^17.0.0", + "react-transition-group": "^4.4.0" + }, + "engines": { + "node": ">=8.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/material-ui" + }, + "peerDependencies": { + "@types/react": "^16.8.6 || ^17.0.0", + "react": "^16.8.0 || ^17.0.0", + "react-dom": "^16.8.0 || ^17.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@material-ui/core/node_modules/popper.js": { + "version": "1.16.1-lts", + "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1-lts.tgz", + "integrity": "sha512-Kjw8nKRl1m+VrSFCoVGPph93W/qrSO7ZkqPpTf7F4bk/sqcfWK019dWBUpE/fBOsOQY1dks/Bmcbfn1heM/IsA==", + "license": "MIT" + }, + "node_modules/@material-ui/icons": { + "version": "4.11.3", + "resolved": "https://registry.npmjs.org/@material-ui/icons/-/icons-4.11.3.tgz", + "integrity": "sha512-IKHlyx6LDh8n19vzwH5RtHIOHl9Tu90aAAxcbWME6kp4dmvODM3UvOHJeMIDzUbd4muuJKHmlNoBN+mDY4XkBA==", + "dependencies": { + "@babel/runtime": "^7.4.4" + }, + "engines": { + "node": ">=8.0.0" + }, + "peerDependencies": { + "@material-ui/core": "^4.0.0", + "@types/react": "^16.8.6 || ^17.0.0", + "react": "^16.8.0 || ^17.0.0", + "react-dom": "^16.8.0 || ^17.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@material-ui/lab": { + "version": "4.0.0-alpha.61", + "resolved": "https://registry.npmjs.org/@material-ui/lab/-/lab-4.0.0-alpha.61.tgz", + "integrity": "sha512-rSzm+XKiNUjKegj8bzt5+pygZeckNLOr+IjykH8sYdVk7dE9y2ZuUSofiMV2bJk3qU+JHwexmw+q0RyNZB9ugg==", + "deprecated": "Material UI v4 doesn't receive active development since September 2021. See the guide https://mui.com/material-ui/migration/migration-v4/ to upgrade to v5.", + "dependencies": { + "@babel/runtime": "^7.4.4", + "@material-ui/utils": "^4.11.3", + "clsx": "^1.0.4", + "prop-types": "^15.7.2", + "react-is": "^16.8.0 || ^17.0.0" + }, + "engines": { + "node": ">=8.0.0" + }, + "peerDependencies": { + "@material-ui/core": "^4.12.1", + "@types/react": "^16.8.6 || ^17.0.0", + "react": "^16.8.0 || ^17.0.0", + "react-dom": "^16.8.0 || ^17.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@material-ui/styles": { + "version": "4.11.5", + "resolved": "https://registry.npmjs.org/@material-ui/styles/-/styles-4.11.5.tgz", + "integrity": "sha512-o/41ot5JJiUsIETME9wVLAJrmIWL3j0R0Bj2kCOLbSfqEkKf0fmaPt+5vtblUh5eXr2S+J/8J3DaCb10+CzPGA==", + "deprecated": "Material UI v4 doesn't receive active development since September 2021. See the guide https://mui.com/material-ui/migration/migration-v4/ to upgrade to v5.", + "dependencies": { + "@babel/runtime": "^7.4.4", + "@emotion/hash": "^0.8.0", + "@material-ui/types": "5.1.0", + "@material-ui/utils": "^4.11.3", + "clsx": "^1.0.4", + "csstype": "^2.5.2", + "hoist-non-react-statics": "^3.3.2", + "jss": "^10.5.1", + "jss-plugin-camel-case": "^10.5.1", + "jss-plugin-default-unit": "^10.5.1", + "jss-plugin-global": "^10.5.1", + "jss-plugin-nested": "^10.5.1", + "jss-plugin-props-sort": "^10.5.1", + "jss-plugin-rule-value-function": "^10.5.1", + "jss-plugin-vendor-prefixer": "^10.5.1", + "prop-types": "^15.7.2" + }, + "engines": { + "node": ">=8.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/material-ui" + }, + "peerDependencies": { + "@types/react": "^16.8.6 || ^17.0.0", + "react": "^16.8.0 || ^17.0.0", + "react-dom": "^16.8.0 || ^17.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@material-ui/system": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@material-ui/system/-/system-4.12.2.tgz", + "integrity": "sha512-6CSKu2MtmiJgcCGf6nBQpM8fLkuB9F55EKfbdTC80NND5wpTmKzwdhLYLH3zL4cLlK0gVaaltW7/wMuyTnN0Lw==", + "dependencies": { + "@babel/runtime": "^7.4.4", + "@material-ui/utils": "^4.11.3", + "csstype": "^2.5.2", + "prop-types": "^15.7.2" + }, + "engines": { + "node": ">=8.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/material-ui" + }, + "peerDependencies": { + "@types/react": "^16.8.6 || ^17.0.0", + "react": "^16.8.0 || ^17.0.0", + "react-dom": "^16.8.0 || ^17.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@material-ui/types": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@material-ui/types/-/types-5.1.0.tgz", + "integrity": "sha512-7cqRjrY50b8QzRSYyhSpx4WRw2YuO0KKIGQEVk5J8uoz2BanawykgZGoWEqKm7pVIbzFDN0SpPcVV4IhOFkl8A==", + "peerDependencies": { + "@types/react": "*" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@material-ui/utils": { + "version": "4.11.3", + "resolved": "https://registry.npmjs.org/@material-ui/utils/-/utils-4.11.3.tgz", + "integrity": "sha512-ZuQPV4rBK/V1j2dIkSSEcH5uT6AaHuKWFfotADHsC0wVL1NLd2WkFCm4ZZbX33iO4ydl6V0GPngKm8HZQ2oujg==", + "dependencies": { + "@babel/runtime": "^7.4.4", + "prop-types": "^15.7.2", + "react-is": "^16.8.0 || ^17.0.0" + }, + "engines": { + "node": ">=8.0.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0", + "react-dom": "^16.8.0 || ^17.0.0" + } + }, + "node_modules/@ng-bootstrap/ng-bootstrap": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/@ng-bootstrap/ng-bootstrap/-/ng-bootstrap-11.0.1.tgz", + "integrity": "sha512-xpXpW2x2S9ZQhEu5kCmEAFf8WvkVD+rcKb1NLQiLuiZgAQR7GXVexXy5Y+RIvTjAQmPEVyxaSgYiJA6sWNJLNw==", + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/common": "^13.0.0", + "@angular/core": "^13.0.0", + "@angular/forms": "^13.0.0", + "@angular/localize": "^13.0.0", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@ng-dynamic-forms/core": { + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/@ng-dynamic-forms/core/-/core-16.0.0.tgz", + "integrity": "sha512-fH0OIgFs/bWkVnnOtDoAAXHXb3K2UOQXm7qEgO9hg86keE2x3Tu/G/Ma6tRVi5+RfKRgvI2Q6JlMLHIQHYFAgA==", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@angular/common": "^15.0.0", + "@angular/core": "^15.0.0", + "@angular/forms": "^15.0.0", + "core-js": "^3.8.1", + "rxjs": "^7.5.5" + } + }, + "node_modules/@ng-dynamic-forms/ui-ng-bootstrap": { + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/@ng-dynamic-forms/ui-ng-bootstrap/-/ui-ng-bootstrap-16.0.0.tgz", + "integrity": "sha512-qopjC+j1wfOaB6a/xSUKEpl5vohEospvhLqPl33BgNH557DQg3x3oI/oPo3caLRhIo2vpKGIeuEN5Itok0B3vg==", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@ng-bootstrap/ng-bootstrap": "^11.0.0", + "@ng-dynamic-forms/core": "^16.0.0", + "bootstrap": "^4.0.0", + "ngx-mask": "^13.0.0" + } + }, + "node_modules/@ngrx/effects": { + "version": "17.2.0", + "resolved": "https://registry.npmjs.org/@ngrx/effects/-/effects-17.2.0.tgz", + "integrity": "sha512-tXDJNsuBtbvI/7+vYnkDKKpUvLbopw1U5G6LoPnKNrbTPsPcUGmCqF5Su/ZoRN3BhXjt2j+eoeVdpBkxdxMRgg==", + "dependencies": { + "@ngrx/operators": "17.0.0-beta.0", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@angular/core": "^17.0.0", + "@ngrx/store": "17.2.0", + "rxjs": "^6.5.3 || ^7.5.0" + } + }, + "node_modules/@ngrx/operators": { + "version": "17.0.0-beta.0", + "resolved": "https://registry.npmjs.org/@ngrx/operators/-/operators-17.0.0-beta.0.tgz", + "integrity": "sha512-EbO8AONuQ6zo2v/mPyBOi4y0CTAp1x4Z+bx7ZF+Pd8BL5ma53BTCL1TmzaeK5zPUe0yApudLk9/ZbHXPnVox5A==", + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@ngrx/router-store": { + "version": "17.2.0", + "resolved": "https://registry.npmjs.org/@ngrx/router-store/-/router-store-17.2.0.tgz", + "integrity": "sha512-Vynfg2xsB57Oedf0Bb6mjC4MIeaF2OtAewsSnppGIM2b8pwL5W89r2+q2SGc2D6Mp3/pZF3HRI6NxhnHWJdYmg==", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@angular/common": "^17.0.0", + "@angular/core": "^17.0.0", + "@angular/router": "^17.0.0", + "@ngrx/store": "17.2.0", + "rxjs": "^6.5.3 || ^7.5.0" + } + }, + "node_modules/@ngrx/store": { + "version": "17.2.0", + "resolved": "https://registry.npmjs.org/@ngrx/store/-/store-17.2.0.tgz", + "integrity": "sha512-7wKgZ59B/6yQSvvsU0DQXipDqpkAXv7LwcXLD5Ww7nvqN0fQoRPThMh4+Wv55DCJhE0bQc1NEMciLA47uRt7Wg==", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@angular/core": "^17.0.0", + "rxjs": "^6.5.3 || ^7.5.0" + } + }, + "node_modules/@ngrx/store-devtools": { + "version": "17.2.0", + "resolved": "https://registry.npmjs.org/@ngrx/store-devtools/-/store-devtools-17.2.0.tgz", + "integrity": "sha512-ig0qr6hMexZGnrlxfHvZmu5CanRjH7hhx60XUbB5BdBvWJIIRaWKPLcsniiDUhljAD87gvzrrilbCTiML38+CA==", + "dev": true, + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@ngrx/store": "17.2.0", + "rxjs": "^6.5.3 || ^7.5.0" + } + }, + "node_modules/@ngtools/webpack": { + "version": "16.2.16", + "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-16.2.16.tgz", + "integrity": "sha512-4gm2allK0Pjy/Lxb9IGRnhEZNEOJSOTWwy09VOdHouV2ODRK7Tto2LgteaFJUUSLkuvWRsI7pfuA6yrz8KDfHw==", + "dev": true, + "engines": { + "node": "^16.14.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "@angular/compiler-cli": "^16.0.0", + "typescript": ">=4.9.3 <5.2", + "webpack": "^5.54.0" + } + }, + "node_modules/@ngx-translate/core": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/@ngx-translate/core/-/core-14.0.0.tgz", + "integrity": "sha512-UevdwNCXMRCdJv//0kC8h2eSfmi02r29xeE8E9gJ1Al4D4jEJ7eiLPdjslTMc21oJNGguqqWeEVjf64SFtvw2w==", + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/core": ">=13.0.0", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@nicky-lenaers/ngx-scroll-to": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/@nicky-lenaers/ngx-scroll-to/-/ngx-scroll-to-14.0.0.tgz", + "integrity": "sha512-nORagPmAQYDjB5jyQ3awGhbnKTKVARZUCK8FJIe46XjPQX2CI9+WBYvsdqq0CsNWVzCjhnC0Om6krzjOTrTHYQ==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": ">=16.18.0", + "yarn": ">=1.22.18" + }, + "peerDependencies": { + "@angular/common": "^14.0.0", + "@angular/core": "^14.0.0" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@npmcli/agent": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-2.2.2.tgz", + "integrity": "sha512-OrcNPXdpSl9UX7qPVRWbmWMCSXrcDa2M9DvrbOTj7ao1S4PlqVFYv9/yLKMkrJKZ/V5A/kDBC690or307i26Og==", + "dev": true, + "dependencies": { + "agent-base": "^7.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.1", + "lru-cache": "^10.0.1", + "socks-proxy-agent": "^8.0.3" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/agent/node_modules/lru-cache": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.0.tgz", + "integrity": "sha512-bfJaPTuEiTYBu+ulDaeQ0F+uLmlfFkMgXj4cbwfuMSjgObGMzb55FMMbDvbRU0fAHZ4sLGkz2mKwcMg8Dvm8Ww==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@npmcli/fs": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.1.tgz", + "integrity": "sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==", + "dev": true, + "dependencies": { + "@gar/promisify": "^1.0.1", + "semver": "^7.3.5" + } + }, + "node_modules/@npmcli/git": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-5.0.7.tgz", + "integrity": "sha512-WaOVvto604d5IpdCRV2KjQu8PzkfE96d50CQGKgywXh2GxXmDeUO5EWcBC4V57uFyrNqx83+MewuJh3WTR3xPA==", + "dev": true, + "dependencies": { + "@npmcli/promise-spawn": "^7.0.0", + "lru-cache": "^10.0.1", + "npm-pick-manifest": "^9.0.0", + "proc-log": "^4.0.0", + "promise-inflight": "^1.0.1", + "promise-retry": "^2.0.1", + "semver": "^7.3.5", + "which": "^4.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/git/node_modules/isexe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", + "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "dev": true, + "engines": { + "node": ">=16" + } + }, + "node_modules/@npmcli/git/node_modules/lru-cache": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.0.tgz", + "integrity": "sha512-bfJaPTuEiTYBu+ulDaeQ0F+uLmlfFkMgXj4cbwfuMSjgObGMzb55FMMbDvbRU0fAHZ4sLGkz2mKwcMg8Dvm8Ww==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@npmcli/git/node_modules/proc-log": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-4.2.0.tgz", + "integrity": "sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/git/node_modules/which": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", + "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "dev": true, + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/installed-package-contents": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@npmcli/installed-package-contents/-/installed-package-contents-2.1.0.tgz", + "integrity": "sha512-c8UuGLeZpm69BryRykLuKRyKFZYJsZSCT4aVY5ds4omyZqJ172ApzgfKJ5eV/r3HgLdUYgFVe54KSFVjKoe27w==", + "dev": true, + "dependencies": { + "npm-bundled": "^3.0.0", + "npm-normalize-package-bin": "^3.0.0" + }, + "bin": { + "installed-package-contents": "bin/index.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/move-file": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.1.2.tgz", + "integrity": "sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==", + "deprecated": "This functionality has been moved to @npmcli/fs", + "dev": true, + "dependencies": { + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@npmcli/node-gyp": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/node-gyp/-/node-gyp-3.0.0.tgz", + "integrity": "sha512-gp8pRXC2oOxu0DUE1/M3bYtb1b3/DbJ5aM113+XJBgfXdussRAsX0YOrOhdd8WvnAR6auDBvJomGAkLKA5ydxA==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/package-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@npmcli/package-json/-/package-json-5.2.0.tgz", + "integrity": "sha512-qe/kiqqkW0AGtvBjL8TJKZk/eBBSpnJkUWvHdQ9jM2lKHXRYYJuyNpJPlJw3c8QjC2ow6NZYiLExhUaeJelbxQ==", + "dev": true, + "dependencies": { + "@npmcli/git": "^5.0.0", + "glob": "^10.2.2", + "hosted-git-info": "^7.0.0", + "json-parse-even-better-errors": "^3.0.0", + "normalize-package-data": "^6.0.0", + "proc-log": "^4.0.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/package-json/node_modules/glob": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.3.tgz", + "integrity": "sha512-Q38SGlYRpVtDBPSWEylRyctn7uDeTp4NQERTLiCT1FqA9JXPYWqAVmQU6qh4r/zMM5ehxTcbaO8EjhWnvEhmyg==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@npmcli/package-json/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/@npmcli/package-json/node_modules/proc-log": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-4.2.0.tgz", + "integrity": "sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/promise-spawn": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-7.0.2.tgz", + "integrity": "sha512-xhfYPXoV5Dy4UkY0D+v2KkwvnDfiA/8Mt3sWCGI/hM03NsYIH8ZaG6QzS9x7pje5vHZBZJ2v6VRFVTWACnqcmQ==", + "dev": true, + "dependencies": { + "which": "^4.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/promise-spawn/node_modules/isexe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", + "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "dev": true, + "engines": { + "node": ">=16" + } + }, + "node_modules/@npmcli/promise-spawn/node_modules/which": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", + "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "dev": true, + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/redact": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@npmcli/redact/-/redact-1.1.0.tgz", + "integrity": "sha512-PfnWuOkQgu7gCbnSsAisaX7hKOdZ4wSAhAzH3/ph5dSGau52kCRrMMGbiSQLwyTZpgldkZ49b0brkOr1AzGBHQ==", + "dev": true, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/run-script": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-7.0.4.tgz", + "integrity": "sha512-9ApYM/3+rBt9V80aYg6tZfzj3UWdiYyCt7gJUD1VJKvWF5nwKDSICXbYIQbspFTq6TOpbsEtIC0LArB8d9PFmg==", + "dev": true, + "dependencies": { + "@npmcli/node-gyp": "^3.0.0", + "@npmcli/package-json": "^5.0.0", + "@npmcli/promise-spawn": "^7.0.0", + "node-gyp": "^10.0.0", + "which": "^4.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/run-script/node_modules/isexe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", + "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "dev": true, + "engines": { + "node": ">=16" + } + }, + "node_modules/@npmcli/run-script/node_modules/which": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", + "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "dev": true, + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^16.13.0 || >=18.0.0" + } + }, + "node_modules/@parcel/watcher": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.4.1.tgz", + "integrity": "sha512-HNjmfLQEVRZmHRET336f20H/8kOozUGwk7yajvsonjNxbj2wBTK1WsQuHkD5yYh9RxFGL2EyDHryOihOwUoKDA==", + "dev": true, + "optional": true, + "dependencies": { + "detect-libc": "^1.0.3", + "is-glob": "^4.0.3", + "micromatch": "^4.0.5", + "node-addon-api": "^7.0.0" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.4.1", + "@parcel/watcher-darwin-arm64": "2.4.1", + "@parcel/watcher-darwin-x64": "2.4.1", + "@parcel/watcher-freebsd-x64": "2.4.1", + "@parcel/watcher-linux-arm-glibc": "2.4.1", + "@parcel/watcher-linux-arm64-glibc": "2.4.1", + "@parcel/watcher-linux-arm64-musl": "2.4.1", + "@parcel/watcher-linux-x64-glibc": "2.4.1", + "@parcel/watcher-linux-x64-musl": "2.4.1", + "@parcel/watcher-win32-arm64": "2.4.1", + "@parcel/watcher-win32-ia32": "2.4.1", + "@parcel/watcher-win32-x64": "2.4.1" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.4.1.tgz", + "integrity": "sha512-LOi/WTbbh3aTn2RYddrO8pnapixAziFl6SMxHM69r3tvdSm94JtCenaKgk1GRg5FJ5wpMCpHeW+7yqPlvZv7kg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.4.1.tgz", + "integrity": "sha512-ln41eihm5YXIY043vBrrHfn94SIBlqOWmoROhsMVTSXGh0QahKGy77tfEywQ7v3NywyxBBkGIfrWRHm0hsKtzA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.4.1.tgz", + "integrity": "sha512-yrw81BRLjjtHyDu7J61oPuSoeYWR3lDElcPGJyOvIXmor6DEo7/G2u1o7I38cwlcoBHQFULqF6nesIX3tsEXMg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.4.1.tgz", + "integrity": "sha512-TJa3Pex/gX3CWIx/Co8k+ykNdDCLx+TuZj3f3h7eOjgpdKM+Mnix37RYsYU4LHhiYJz3DK5nFCCra81p6g050w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.4.1.tgz", + "integrity": "sha512-4rVYDlsMEYfa537BRXxJ5UF4ddNwnr2/1O4MHM5PjI9cvV2qymvhwZSFgXqbS8YoTk5i/JR0L0JDs69BUn45YA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.4.1.tgz", + "integrity": "sha512-BJ7mH985OADVLpbrzCLgrJ3TOpiZggE9FMblfO65PlOCdG++xJpKUJ0Aol74ZUIYfb8WsRlUdgrZxKkz3zXWYA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.4.1.tgz", + "integrity": "sha512-p4Xb7JGq3MLgAfYhslU2SjoV9G0kI0Xry0kuxeG/41UfpjHGOhv7UoUDAz/jb1u2elbhazy4rRBL8PegPJFBhA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.4.1.tgz", + "integrity": "sha512-s9O3fByZ/2pyYDPoLM6zt92yu6P4E39a03zvO0qCHOTjxmt3GHRMLuRZEWhWLASTMSrrnVNWdVI/+pUElJBBBg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.4.1.tgz", + "integrity": "sha512-L2nZTYR1myLNST0O632g0Dx9LyMNHrn6TOt76sYxWLdff3cB22/GZX2UPtJnaqQPdCRoszoY5rcOj4oMTtp5fQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.4.1.tgz", + "integrity": "sha512-Uq2BPp5GWhrq/lcuItCHoqxjULU1QYEcyjSO5jqqOK8RNFDBQnenMMx4gAl3v8GiWa59E9+uDM7yZ6LxwUIfRg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.4.1.tgz", + "integrity": "sha512-maNRit5QQV2kgHFSYwftmPBxiuK5u4DXjbXx7q6eKjq5dsLXZ4FJiVvlcw35QXzk0KrUecJmuVFbj4uV9oYrcw==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.4.1.tgz", + "integrity": "sha512-+DvS92F9ezicfswqrvIRM2njcYJbd5mb9CUgtrHCHmvn7pPPa+nMDRu1o1bYYz/l5IB2NVGNJWiH7h1E58IF2A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher/node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "dev": true, + "optional": true + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@react-dnd/asap": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@react-dnd/asap/-/asap-4.0.1.tgz", + "integrity": "sha512-kLy0PJDDwvwwTXxqTFNAAllPHD73AycE9ypWeln/IguoGBEbvFcPDbCV03G52bEcC5E+YgupBE0VzHGdC8SIXg==" + }, + "node_modules/@react-dnd/invariant": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@react-dnd/invariant/-/invariant-2.0.0.tgz", + "integrity": "sha512-xL4RCQBCBDJ+GRwKTFhGUW8GXa4yoDfJrPbLblc3U09ciS+9ZJXJ3Qrcs/x2IODOdIE5kQxvMmE2UKyqUictUw==" + }, + "node_modules/@react-dnd/shallowequal": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@react-dnd/shallowequal/-/shallowequal-2.0.0.tgz", + "integrity": "sha512-Pc/AFTdwZwEKJxFJvlxrSmGe/di+aAOBn60sremrpLo6VI/6cmiUYNNwlI5KNYttg7uypzA3ILPMPgxB2GYZEg==" + }, + "node_modules/@redux-saga/core": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@redux-saga/core/-/core-1.3.0.tgz", + "integrity": "sha512-L+i+qIGuyWn7CIg7k1MteHGfttKPmxwZR5E7OsGikCL2LzYA0RERlaUY00Y3P3ZV2EYgrsYlBrGs6cJP5OKKqA==", + "dependencies": { + "@babel/runtime": "^7.6.3", + "@redux-saga/deferred": "^1.2.1", + "@redux-saga/delay-p": "^1.2.1", + "@redux-saga/is": "^1.1.3", + "@redux-saga/symbols": "^1.1.3", + "@redux-saga/types": "^1.2.1", + "typescript-tuple": "^2.2.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/redux-saga" + } + }, + "node_modules/@redux-saga/deferred": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@redux-saga/deferred/-/deferred-1.2.1.tgz", + "integrity": "sha512-cmin3IuuzMdfQjA0lG4B+jX+9HdTgHZZ+6u3jRAOwGUxy77GSlTi4Qp2d6PM1PUoTmQUR5aijlA39scWWPF31g==" + }, + "node_modules/@redux-saga/delay-p": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@redux-saga/delay-p/-/delay-p-1.2.1.tgz", + "integrity": "sha512-MdiDxZdvb1m+Y0s4/hgdcAXntpUytr9g0hpcOO1XFVyyzkrDu3SKPgBFOtHn7lhu7n24ZKIAT1qtKyQjHqRd+w==", + "dependencies": { + "@redux-saga/symbols": "^1.1.3" + } + }, + "node_modules/@redux-saga/is": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@redux-saga/is/-/is-1.1.3.tgz", + "integrity": "sha512-naXrkETG1jLRfVfhOx/ZdLj0EyAzHYbgJWkXbB3qFliPcHKiWbv/ULQryOAEKyjrhiclmr6AMdgsXFyx7/yE6Q==", + "dependencies": { + "@redux-saga/symbols": "^1.1.3", + "@redux-saga/types": "^1.2.1" + } + }, + "node_modules/@redux-saga/symbols": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@redux-saga/symbols/-/symbols-1.1.3.tgz", + "integrity": "sha512-hCx6ZvU4QAEUojETnX8EVg4ubNLBFl1Lps4j2tX7o45x/2qg37m3c6v+kSp8xjDJY+2tJw4QB3j8o8dsl1FDXg==" + }, + "node_modules/@redux-saga/types": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@redux-saga/types/-/types-1.2.1.tgz", + "integrity": "sha512-1dgmkh+3so0+LlBWRhGA33ua4MYr7tUOj+a9Si28vUi0IUFNbff1T3sgpeDJI/LaC75bBYnQ0A3wXjn0OrRNBA==" + }, + "node_modules/@researchgate/react-intersection-observer": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@researchgate/react-intersection-observer/-/react-intersection-observer-1.3.5.tgz", + "integrity": "sha512-aYlsex5Dd6BAHMJvJrUoFp8gzgMSL27xFvrxkVYW0bV1RMAapVsO+QeYLtTaSF/QCflktODodvv+wJm49oMnnQ==", + "engines": { + "node": ">=10.18.1" + }, + "peerDependencies": { + "react": "^16.3.2", + "react-dom": "^16.3.2" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.24.0.tgz", + "integrity": "sha512-Q6HJd7Y6xdB48x8ZNVDOqsbh2uByBhgK8PiQgPhwkIw/HC/YX5Ghq2mQY5sRMZWHb3VsFkWooUVOZHKr7DmDIA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.24.0.tgz", + "integrity": "sha512-ijLnS1qFId8xhKjT81uBHuuJp2lU4x2yxa4ctFPtG+MqEE6+C5f/+X/bStmxapgmwLwiL3ih122xv8kVARNAZA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.24.0.tgz", + "integrity": "sha512-bIv+X9xeSs1XCk6DVvkO+S/z8/2AMt/2lMqdQbMrmVpgFvXlmde9mLcbQpztXm1tajC3raFDqegsH18HQPMYtA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.24.0.tgz", + "integrity": "sha512-X6/nOwoFN7RT2svEQWUsW/5C/fYMBe4fnLK9DQk4SX4mgVBiTA9h64kjUYPvGQ0F/9xwJ5U5UfTbl6BEjaQdBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.24.0.tgz", + "integrity": "sha512-0KXvIJQMOImLCVCz9uvvdPgfyWo93aHHp8ui3FrtOP57svqrF/roSSR5pjqL2hcMp0ljeGlU4q9o/rQaAQ3AYA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.24.0.tgz", + "integrity": "sha512-it2BW6kKFVh8xk/BnHfakEeoLPv8STIISekpoF+nBgWM4d55CZKc7T4Dx1pEbTnYm/xEKMgy1MNtYuoA8RFIWw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.24.0.tgz", + "integrity": "sha512-i0xTLXjqap2eRfulFVlSnM5dEbTVque/3Pi4g2y7cxrs7+a9De42z4XxKLYJ7+OhE3IgxvfQM7vQc43bwTgPwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.24.0.tgz", + "integrity": "sha512-9E6MKUJhDuDh604Qco5yP/3qn3y7SLXYuiC0Rpr89aMScS2UAmK1wHP2b7KAa1nSjWJc/f/Lc0Wl1L47qjiyQw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.24.0.tgz", + "integrity": "sha512-2XFFPJ2XMEiF5Zi2EBf4h73oR1V/lycirxZxHZNc93SqDN/IWhYYSYj8I9381ikUFXZrz2v7r2tOVk2NBwxrWw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.24.0.tgz", + "integrity": "sha512-M3Dg4hlwuntUCdzU7KjYqbbd+BLq3JMAOhCKdBE3TcMGMZbKkDdJ5ivNdehOssMCIokNHFOsv7DO4rlEOfyKpg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.24.0.tgz", + "integrity": "sha512-mjBaoo4ocxJppTorZVKWFpy1bfFj9FeCMJqzlMQGjpNPY9JwQi7OuS1axzNIk0nMX6jSgy6ZURDZ2w0QW6D56g==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.24.0.tgz", + "integrity": "sha512-ZXFk7M72R0YYFN5q13niV0B7G8/5dcQ9JDp8keJSfr3GoZeXEoMHP/HlvqROA3OMbMdfr19IjCeNAnPUG93b6A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.24.0.tgz", + "integrity": "sha512-w1i+L7kAXZNdYl+vFvzSZy8Y1arS7vMgIy8wusXJzRrPyof5LAb02KGr1PD2EkRcl73kHulIID0M501lN+vobQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.24.0.tgz", + "integrity": "sha512-VXBrnPWgBpVDCVY6XF3LEW0pOU51KbaHhccHw6AS6vBWIC60eqsH19DAeeObl+g8nKAz04QFdl/Cefta0xQtUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.24.0.tgz", + "integrity": "sha512-xrNcGDU0OxVcPTH/8n/ShH4UevZxKIO6HJFK0e15XItZP2UcaiLFd5kiX7hJnqCbSztUF8Qot+JWBC/QXRPYWQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.24.0.tgz", + "integrity": "sha512-fbMkAF7fufku0N2dE5TBXcNlg0pt0cJue4xBRE2Qc5Vqikxr4VCgKj/ht6SMdFcOacVA9rqF70APJ8RN/4vMJw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "dev": true + }, + "node_modules/@schematics/angular": { + "version": "17.3.11", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-17.3.11.tgz", + "integrity": "sha512-tvJpTgYC+hCnTyLszYRUZVyNTpPd+C44gh5CPTcG3qkqStzXQwynQAf6X/DjtwXbUiPQF0XfF0+0R489GpdZPA==", + "dev": true, + "dependencies": { + "@angular-devkit/core": "17.3.11", + "@angular-devkit/schematics": "17.3.11", + "jsonc-parser": "3.2.1" + }, + "engines": { + "node": "^18.13.0 || >=20.9.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@sigstore/bundle": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@sigstore/bundle/-/bundle-2.3.2.tgz", + "integrity": "sha512-wueKWDk70QixNLB363yHc2D2ItTgYiMTdPwK8D9dKQMR3ZQ0c35IxP5xnwQ8cNLoCgCRcHf14kE+CLIvNX1zmA==", + "dev": true, + "dependencies": { + "@sigstore/protobuf-specs": "^0.3.2" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@sigstore/core": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@sigstore/core/-/core-1.1.0.tgz", + "integrity": "sha512-JzBqdVIyqm2FRQCulY6nbQzMpJJpSiJ8XXWMhtOX9eKgaXXpfNOF53lzQEjIydlStnd/eFtuC1dW4VYdD93oRg==", + "dev": true, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@sigstore/protobuf-specs": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@sigstore/protobuf-specs/-/protobuf-specs-0.3.2.tgz", + "integrity": "sha512-c6B0ehIWxMI8wiS/bj6rHMPqeFvngFV7cDU/MY+B16P9Z3Mp9k8L93eYZ7BYzSickzuqAQqAq0V956b3Ju6mLw==", + "dev": true, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@sigstore/sign": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@sigstore/sign/-/sign-2.3.2.tgz", + "integrity": "sha512-5Vz5dPVuunIIvC5vBb0APwo7qKA4G9yM48kPWJT+OEERs40md5GoUR1yedwpekWZ4m0Hhw44m6zU+ObsON+iDA==", + "dev": true, + "dependencies": { + "@sigstore/bundle": "^2.3.2", + "@sigstore/core": "^1.0.0", + "@sigstore/protobuf-specs": "^0.3.2", + "make-fetch-happen": "^13.0.1", + "proc-log": "^4.2.0", + "promise-retry": "^2.0.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@sigstore/sign/node_modules/proc-log": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-4.2.0.tgz", + "integrity": "sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@sigstore/tuf": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/@sigstore/tuf/-/tuf-2.3.4.tgz", + "integrity": "sha512-44vtsveTPUpqhm9NCrbU8CWLe3Vck2HO1PNLw7RIajbB7xhtn5RBPm1VNSCMwqGYHhDsBJG8gDF0q4lgydsJvw==", + "dev": true, + "dependencies": { + "@sigstore/protobuf-specs": "^0.3.2", + "tuf-js": "^2.2.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@sigstore/verify": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@sigstore/verify/-/verify-1.2.1.tgz", + "integrity": "sha512-8iKx79/F73DKbGfRf7+t4dqrc0bRr0thdPrxAtCKWRm/F0tG71i6O1rvlnScncJLLBZHn3h8M3c1BSUAb9yu8g==", + "dev": true, + "dependencies": { + "@sigstore/bundle": "^2.3.2", + "@sigstore/core": "^1.1.0", + "@sigstore/protobuf-specs": "^0.3.2" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "dev": true + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "dev": true + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true + }, + "node_modules/@tufjs/canonical-json": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tufjs/canonical-json/-/canonical-json-2.0.0.tgz", + "integrity": "sha512-yVtV8zsdo8qFHe+/3kw81dSLyF7D576A5cCFCi4X7B39tWT7SekaEFUnvnWJHz+9qO7qJTah1JbrDjWKqFtdWA==", + "dev": true, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@tufjs/models": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@tufjs/models/-/models-2.0.1.tgz", + "integrity": "sha512-92F7/SFyufn4DXsha9+QfKnN03JGqtMFMXgSHbZOo8JG59WkTni7UzAouNQDf7AuP9OAMxVOPQcqG3sB7w+kkg==", + "dev": true, + "dependencies": { + "@tufjs/canonical-json": "2.0.0", + "minimatch": "^9.0.4" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.6.8", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", + "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.6", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz", + "integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==", + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.5", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", + "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "devOptional": true, + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/bonjour": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.13.tgz", + "integrity": "sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "devOptional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/connect-history-api-fallback": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.4.tgz", + "integrity": "sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw==", + "dev": true, + "dependencies": { + "@types/express-serve-static-core": "*", + "@types/node": "*" + } + }, + "node_modules/@types/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==", + "dev": true + }, + "node_modules/@types/cors": { + "version": "2.8.17", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", + "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/deep-freeze": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/@types/deep-freeze/-/deep-freeze-0.1.5.tgz", + "integrity": "sha512-KZtR+jtmgkCpgE0f+We/QEI2Fi0towBV/tTkvHVhMzx+qhUVGXMx7pWvAtDp6vEWIjdKLTKpqbI/sORRCo8TKg==", + "dev": true + }, + "node_modules/@types/ejs": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/@types/ejs/-/ejs-3.1.5.tgz", + "integrity": "sha512-nv+GSx77ZtXiJzwKdsASqi+YQ5Z7vwHsTP0JY2SiQgjGckkBRKZnk8nIM+7oUZ1VCtuTz0+By4qVR7fqzp/Dfg==", + "dev": true + }, + "node_modules/@types/eslint": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "dev": true, + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "dev": true, + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "dev": true + }, + "node_modules/@types/express": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", + "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", + "devOptional": true, + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.19.5", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.5.tgz", + "integrity": "sha512-y6W03tvrACO72aijJ5uF02FRq5cgDR9lUxddQ8vyF+GvmjJQqbzDcJngEjURc+ZsG31VI3hODNZJ2URj86pzmg==", + "devOptional": true, + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/grecaptcha": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/grecaptcha/-/grecaptcha-3.0.9.tgz", + "integrity": "sha512-fFxMtjAvXXMYTzDFK5NpcVB7WHnrHVLl00QzEGpuFxSAC789io6M+vjcn+g5FTEamIJtJr/IHkCDsqvJxeWDyw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/hoist-non-react-statics": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.5.tgz", + "integrity": "sha512-SbcrWzkKBw2cdwRTwQAswfpB9g9LJWfjtUeW/jvNwbhC8cpmmNYVePa+ncbUe0rGTQ7G3Ff6mYUN2VMfLVr+Sg==", + "dependencies": { + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", + "devOptional": true + }, + "node_modules/@types/http-proxy": { + "version": "1.17.14", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.14.tgz", + "integrity": "sha512-SSrD0c1OQzlFX7pGu1eXxSEjemej64aaNPRhhVYUGqXh0BtldAAx37MG8btcumvpgKyZp1F5Gn3JkktdxiFv6w==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/jasmine": { + "version": "3.6.11", + "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-3.6.11.tgz", + "integrity": "sha512-S6pvzQDvMZHrkBz2Mcn/8Du7cpr76PlRJBAoHnSDNbulULsH5dp0Gns+WRyNX5LHejz/ljxK4/vIHK/caHt6SQ==", + "dev": true + }, + "node_modules/@types/js-cookie": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-2.2.6.tgz", + "integrity": "sha512-+oY0FDTO2GYKEV0YPvSshGq9t7YozVkgvXLty7zogQNuCxBhT9/3INX9Q7H1aRZ4SUDRXAKlJuA4EA5nTt7SNw==", + "dev": true + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true + }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true + }, + "node_modules/@types/lodash": { + "version": "4.17.14", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.14.tgz", + "integrity": "sha512-jsxagdikDiDBeIRaPYtArcT8my4tN1og7MtMRquFT3XNA6axxyHDRUemqDz/taRDdOUn0GnGHRCuff4q48sW9A==", + "dev": true + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "devOptional": true + }, + "node_modules/@types/node": { + "version": "14.18.63", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.63.tgz", + "integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==" + }, + "node_modules/@types/node-forge": { + "version": "1.3.11", + "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.11.tgz", + "integrity": "sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/parse-json": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", + "dev": true + }, + "node_modules/@types/prop-types": { + "version": "15.7.12", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz", + "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==" + }, + "node_modules/@types/qs": { + "version": "6.9.15", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.15.tgz", + "integrity": "sha512-uXHQKES6DQKKCLh441Xv/dwxOq1TVS3JPUMlEqoEglvlhR6Mxnlew/Xq/LRVHpLyk7iK3zODe1qYHIMltO7XGg==", + "devOptional": true + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "devOptional": true + }, + "node_modules/@types/react": { + "version": "17.0.80", + "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.80.tgz", + "integrity": "sha512-LrgHIu2lEtIo8M7d1FcI3BdwXWoRQwMoXOZ7+dPTW0lYREjmlHl3P0U1VD0i/9tppOuv8/sam7sOjx34TxSFbA==", + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "@types/scheduler": "^0.16", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-redux": { + "version": "7.1.33", + "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.33.tgz", + "integrity": "sha512-NF8m5AjWCkert+fosDsN3hAlHzpjSiXlVy9EgQEmLoBhaNXbmyeGs/aj5dQzKuF+/q+S7JQagorGDW8pJ28Hmg==", + "dependencies": { + "@types/hoist-non-react-statics": "^3.3.0", + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0", + "redux": "^4.0.0" + } + }, + "node_modules/@types/react-transition-group": { + "version": "4.4.10", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.10.tgz", + "integrity": "sha512-hT/+s0VQs2ojCX823m60m5f0sL5idt9SO6Tj6Dg+rdphGPIeJbJ6CxvBYkgkGKrYeDjvIpKTR38UzmtHJOGW3Q==", + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/react/node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" + }, + "node_modules/@types/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", + "dev": true + }, + "node_modules/@types/scheduler": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz", + "integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==", + "license": "MIT" + }, + "node_modules/@types/semver": { + "version": "7.5.8", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", + "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==", + "dev": true + }, + "node_modules/@types/send": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "devOptional": true, + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-index": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.4.tgz", + "integrity": "sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug==", + "dev": true, + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.7", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", + "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", + "devOptional": true, + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } + }, + "node_modules/@types/sinonjs__fake-timers": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.1.tgz", + "integrity": "sha512-0kSuKjAS0TrGLJ0M/+8MaFkGsQhZpB6pxOmvS3K8FYI72K//YmdfoW9X2qPsAKh1mkwxGD5zib9s1FIFed6E8g==", + "dev": true + }, + "node_modules/@types/sizzle": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.8.tgz", + "integrity": "sha512-0vWLNK2D5MT9dg0iOo8GlKguPAU02QjmZitPEsXRuJXU/OGIOt9vT9Fc26wtYuavLxtO45v9PGleoL9Z0k1LHg==", + "dev": true + }, + "node_modules/@types/sockjs": { + "version": "0.3.36", + "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.36.tgz", + "integrity": "sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/ws": { + "version": "8.5.10", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz", + "integrity": "sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/yargs": { + "version": "17.0.32", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", + "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true + }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "dev": true, + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.18.0.tgz", + "integrity": "sha512-94EQTWZ40mzBc42ATNIBimBEDltSJ9RQHCC8vc/PDbxi4k8dVwUAv4o98dk50M1zB+JGFxp43FP7f8+FP8R6Sw==", + "dev": true, + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/type-utils": "7.18.0", + "@typescript-eslint/utils": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^7.0.0", + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/type-utils": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.18.0.tgz", + "integrity": "sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA==", + "dev": true, + "dependencies": { + "@typescript-eslint/typescript-estree": "7.18.0", + "@typescript-eslint/utils": "7.18.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/experimental-utils": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-5.62.0.tgz", + "integrity": "sha512-RTXpeB3eMkpoclG3ZHft6vG/Z30azNHuqY6wKPBHlVMZFuEvrtlEDe8gMqDb+SO+9hjC/pLekeSCryf9vMZlCw==", + "dev": true, + "dependencies": { + "@typescript-eslint/utils": "5.62.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/experimental-utils/node_modules/@typescript-eslint/scope-manager": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz", + "integrity": "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/experimental-utils/node_modules/@typescript-eslint/types": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz", + "integrity": "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/experimental-utils/node_modules/@typescript-eslint/typescript-estree": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz", + "integrity": "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/experimental-utils/node_modules/@typescript-eslint/utils": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz", + "integrity": "sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@types/json-schema": "^7.0.9", + "@types/semver": "^7.3.12", + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/typescript-estree": "5.62.0", + "eslint-scope": "^5.1.1", + "semver": "^7.3.7" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/experimental-utils/node_modules/@typescript-eslint/visitor-keys": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz", + "integrity": "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/experimental-utils/node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@typescript-eslint/experimental-utils/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.18.0.tgz", + "integrity": "sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/typescript-estree": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/rule-tester": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/rule-tester/-/rule-tester-7.18.0.tgz", + "integrity": "sha512-ClrFQlwen9pJcYPIBLuarzBpONQAwjmJ0+YUjAo1TGzoZFJPyUK/A7bb4Mps0u+SMJJnFXbfMN8I9feQDf0O5A==", + "dev": true, + "dependencies": { + "@typescript-eslint/typescript-estree": "7.18.0", + "@typescript-eslint/utils": "7.18.0", + "ajv": "^6.12.6", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "4.6.2", + "semver": "^7.6.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@eslint/eslintrc": ">=2", + "eslint": "^8.56.0" + } + }, + "node_modules/@typescript-eslint/rule-tester/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@typescript-eslint/rule-tester/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.18.0.tgz", + "integrity": "sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.11.0.tgz", + "integrity": "sha512-WmppUEgYy+y1NTseNMJ6mCFxt03/7jTOy08bcg7bxJJdsM4nuhnchyBbE8vryveaJUf62noH7LodPSo5Z0WUCg==", + "dev": true, + "dependencies": { + "@typescript-eslint/typescript-estree": "7.11.0", + "@typescript-eslint/utils": "7.11.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/scope-manager": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.11.0.tgz", + "integrity": "sha512-27tGdVEiutD4POirLZX4YzT180vevUURJl4wJGmm6TrQoiYwuxTIY98PBp6L2oN+JQxzE0URvYlzJaBHIekXAw==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.11.0", + "@typescript-eslint/visitor-keys": "7.11.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/types": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.11.0.tgz", + "integrity": "sha512-MPEsDRZTyCiXkD4vd3zywDCifi7tatc4K37KqTprCvaXptP7Xlpdw0NR2hRJTetG5TxbWDB79Ys4kLmHliEo/w==", + "dev": true, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.11.0.tgz", + "integrity": "sha512-cxkhZ2C/iyi3/6U9EPc5y+a6csqHItndvN/CzbNXTNrsC3/ASoYQZEt9uMaEp+xFNjasqQyszp5TumAVKKvJeQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.11.0", + "@typescript-eslint/visitor-keys": "7.11.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/utils": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.11.0.tgz", + "integrity": "sha512-xlAWwPleNRHwF37AhrZurOxA1wyXowW4PqVXZVUNCLjB48CqdPJoJWkrpH2nij9Q3Lb7rtWindtoXwxjxlKKCA==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "7.11.0", + "@typescript-eslint/types": "7.11.0", + "@typescript-eslint/typescript-estree": "7.11.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/visitor-keys": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.11.0.tgz", + "integrity": "sha512-7syYk4MzjxTEk0g/w3iqtgxnFQspDJfn6QKD36xMuuhTzjcxY7F8EmBLnALjVyaOF1/bVocu3bS/2/F7rXrveQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.11.0", + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.18.0.tgz", + "integrity": "sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==", + "dev": true, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.18.0.tgz", + "integrity": "sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/visitor-keys": "7.18.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.18.0.tgz", + "integrity": "sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "7.18.0", + "@typescript-eslint/types": "7.18.0", + "@typescript-eslint/typescript-estree": "7.18.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "7.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.18.0.tgz", + "integrity": "sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.18.0", + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "dev": true + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "dev": true, + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "dev": true, + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "dev": true, + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "dev": true, + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "dev": true + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webpack-cli/configtest": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-2.1.1.tgz", + "integrity": "sha512-wy0mglZpDSiSS0XHrVR+BAdId2+yxPSoJW8fsna3ZpYSlufjvxnP4YbKTCBZnNIcGN4r6ZPXV55X4mYExOfLmw==", + "dev": true, + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + } + }, + "node_modules/@webpack-cli/info": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-2.0.2.tgz", + "integrity": "sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A==", + "dev": true, + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + } + }, + "node_modules/@webpack-cli/serve": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-2.0.5.tgz", + "integrity": "sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ==", + "dev": true, + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + }, + "peerDependenciesMeta": { + "webpack-dev-server": { + "optional": true + } + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true + }, + "node_modules/@yarnpkg/lockfile": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", + "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==", + "dev": true + }, + "node_modules/abbrev": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz", + "integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-attributes": { + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", + "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^8" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.3", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.3.tgz", + "integrity": "sha512-MxXdReSRhGO7VlFe1bRG/oI7/mdLV9B9JJT0N8vZOhF7gFRR5l3M8W9G8JxmKV+JC5mGqJ0QvqfSOLsCPa4nUw==", + "dev": true, + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/adjust-sourcemap-loader": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/adjust-sourcemap-loader/-/adjust-sourcemap-loader-4.0.0.tgz", + "integrity": "sha512-OXwN5b9pCUXNQHJpwwD2qP40byEmSgzj8B4ydSN0uMNYWiFmJ6x6KwUllMmfk8Rwu/HJDFR7U8ubsWBoN0Xp0A==", + "dev": true, + "dependencies": { + "loader-utils": "^2.0.0", + "regex-parser": "^2.2.11" + }, + "engines": { + "node": ">=8.9" + } + }, + "node_modules/adjust-sourcemap-loader/node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dev": true, + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "dev": true, + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/angulartics2": { + "version": "12.2.1", + "resolved": "https://registry.npmjs.org/angulartics2/-/angulartics2-12.2.1.tgz", + "integrity": "sha512-+uDXkGGJJzzIITE59z1s3rL5okNyGXZN5mP7m2Ro9gouJF88COeRkhYlXZwIXkdQjBzIyOvUv6UamzgL9NiXLg==", + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/common": ">=13.0.0-0", + "@angular/core": ">=13.0.0-0", + "rxjs": "^7.0.0" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-escapes/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-html-community": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz", + "integrity": "sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==", + "dev": true, + "engines": [ + "node >= 0.8.0" + ], + "bin": { + "ansi-html": "bin/ansi-html" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/arch": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz", + "integrity": "sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/are-docs-informative": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/are-docs-informative/-/are-docs-informative-0.0.2.tgz", + "integrity": "sha512-ixiS0nLNNG5jNQzgZJNoUpBKdo9yTYZMGJ+QgT2jmjR7G7+QHRCc4v6LQ3NgE7EBJq+o0ams3waJwkrlBom8Ig==", + "dev": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz", + "integrity": "sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.5", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + }, + "node_modules/array-includes": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", + "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.4", + "is-string": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/array.prototype.findlastindex": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.5.tgz", + "integrity": "sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz", + "integrity": "sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz", + "integrity": "sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz", + "integrity": "sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "es-abstract": "^1.22.3", + "es-errors": "^1.2.1", + "get-intrinsic": "^1.2.3", + "is-array-buffer": "^3.0.4", + "is-shared-array-buffer": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "dev": true, + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "dev": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/async": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", + "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==" + }, + "node_modules/async-each-series": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/async-each-series/-/async-each-series-0.1.1.tgz", + "integrity": "sha512-p4jj6Fws4Iy2m0iCmI2am2ZNZCgbdgE+P8F/8csmn2vx7ixXrO2zGcuNsD46X5uZSVecmkEy/M06X2vG8KD6dQ==", + "dev": true, + "optional": true, + "peer": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/autoprefixer": { + "version": "10.4.18", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.18.tgz", + "integrity": "sha512-1DKbDfsr6KUElM6wg+0zRNkB/Q7WcKYAaK+pzXn+Xqmszm/5Xa9coeNdtP88Vi+dPzZnMjhge8GIV49ZQkDa+g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "browserslist": "^4.23.0", + "caniuse-lite": "^1.0.30001591", + "fraction.js": "^4.3.7", + "normalize-range": "^0.1.2", + "picocolors": "^1.0.0", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/aws4": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.2.tgz", + "integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==", + "dev": true + }, + "node_modules/axe-core": { + "version": "4.10.2", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.10.2.tgz", + "integrity": "sha512-RE3mdQ7P3FRSe7eqCWoeQ/Z9QXrtniSjp1wUjt5nRC3WIpz5rSCve6o3fsZ2aCpJtrZjSZgjwXAoTO5k4tEI0w==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/axios": { + "version": "1.7.9", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz", + "integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/axobject-query": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.0.0.tgz", + "integrity": "sha512-+60uv1hiVFhHZeO+Lz0RYzsVHy5Wr1ayX0mwda9KPDVLNJgZ1T9Ny7VmFbLDzxsH0D87I86vgj3gFrjTJUYznw==", + "dev": true, + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/babel-loader": { + "version": "9.1.3", + "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-9.1.3.tgz", + "integrity": "sha512-xG3ST4DglodGf8qSwv0MdeWLhrDsw/32QMdTO5T1ZIp9gQur0HkCyFs7Awskr10JKXFXwpAhiCuYX5oGXnRGbw==", + "dev": true, + "dependencies": { + "find-cache-dir": "^4.0.0", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 14.15.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0", + "webpack": ">=5" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-polyfill-corejs2": { + "version": "0.4.11", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.11.tgz", + "integrity": "sha512-sMEJ27L0gRHShOh5G54uAAPaiCOygY/5ratXuiyb2G46FmlSpc9eFCzYVyDiPxfNbwzA7mYahmjQc5q+CZQ09Q==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.22.6", + "@babel/helper-define-polyfill-provider": "^0.6.2", + "semver": "^6.3.1" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs2/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.9.0.tgz", + "integrity": "sha512-7nZPG1uzK2Ymhy/NbaOWTg3uibM2BmGASS4vHS4szRZAIR8R6GwA/xAujpdrXU5iyklrimWnLWU+BLF9suPTqg==", + "dev": true, + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.5.0", + "core-js-compat": "^3.34.0" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs3/node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.5.0.tgz", + "integrity": "sha512-NovQquuQLAQ5HuyjCz7WQP9MjRj7dx++yspwiyUiGl9ZyadHRSql1HZh5ogRd8W8w6YM6EQ/NTB8rgjLt5W65Q==", + "dev": true, + "dependencies": { + "@babel/helper-compilation-targets": "^7.22.6", + "@babel/helper-plugin-utils": "^7.22.5", + "debug": "^4.1.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.14.2" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-regenerator": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.5.tgz", + "integrity": "sha512-OJGYZlhLqBh2DDHeqAxWB1XIvr49CxiJ2gIt61/PU55CQK4Z58OzMqjDe1zwQdQk+rBYsRc+1rJmdajM3gimHg==", + "dev": true, + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.5.0" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-regenerator/node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.5.0.tgz", + "integrity": "sha512-NovQquuQLAQ5HuyjCz7WQP9MjRj7dx++yspwiyUiGl9ZyadHRSql1HZh5ogRd8W8w6YM6EQ/NTB8rgjLt5W65Q==", + "dev": true, + "dependencies": { + "@babel/helper-compilation-targets": "^7.22.6", + "@babel/helper-plugin-utils": "^7.22.5", + "debug": "^4.1.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.14.2" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "dev": true, + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, + "node_modules/basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/batch": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", + "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==", + "dev": true + }, + "node_modules/batch-processor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/batch-processor/-/batch-processor-1.0.0.tgz", + "integrity": "sha512-xoLQD8gmmR32MeuBHgH0Tzd5PuSZx71ZsbhVxOCRbgktZEPe4SQy7s9Z50uPp0F/f7iw2XmkHN2xkgbMfckMDA==" + }, + "node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "dev": true, + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, + "node_modules/bent": { + "version": "7.3.12", + "resolved": "https://registry.npmjs.org/bent/-/bent-7.3.12.tgz", + "integrity": "sha512-T3yrKnVGB63zRuoco/7Ybl7BwwGZR0lceoVG5XmQyMIH9s19SV5m+a8qam4if0zQuAmOQTyPTPmsQBdAorGK3w==", + "dev": true, + "dependencies": { + "bytesish": "^0.4.1", + "caseless": "~0.12.0", + "is-stream": "^2.0.0" + } + }, + "node_modules/big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/blob-util": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/blob-util/-/blob-util-2.0.2.tgz", + "integrity": "sha512-T7JQa+zsXXEa6/8ZhHcQEW1UFfVM49Ts65uBkFL6fz2QmrElqmbajIDJvuA0tEhRe5eIjpV9ZF+0RfZR9voJFQ==", + "dev": true + }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/bonjour-service": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.2.1.tgz", + "integrity": "sha512-oSzCS2zV14bh2kji6vNe7vrpJYCHGvcZnlffFQ1MEoX/WOeQ/teD8SYWKR942OI3INjq8OMNJlbPK5LLLUxFDw==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "multicast-dns": "^7.2.5" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==" + }, + "node_modules/boolean": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/boolean/-/boolean-3.2.0.tgz", + "integrity": "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==" + }, + "node_modules/bootstrap": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.6.2.tgz", + "integrity": "sha512-51Bbp/Uxr9aTuy6ca/8FbFloBUJZLHwnhTcnjIeRn2suQWsWzcuJhGjKDB5eppVte/8oCdOL3VuwxvZDUggwGQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/twbs" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/bootstrap" + } + ], + "peerDependencies": { + "jquery": "1.9.1 - 3", + "popper.js": "^1.16.1" + } + }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browser-sync": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/browser-sync/-/browser-sync-3.0.3.tgz", + "integrity": "sha512-91hoBHKk1C4pGeD+oE9Ld222k2GNQEAsI5AElqR8iLLWNrmZR2LPP8B0h8dpld9u7kro5IEUB3pUb0DJ3n1cRQ==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "browser-sync-client": "^3.0.3", + "browser-sync-ui": "^3.0.3", + "bs-recipes": "1.3.4", + "chalk": "4.1.2", + "chokidar": "^3.5.1", + "connect": "3.6.6", + "connect-history-api-fallback": "^1", + "dev-ip": "^1.0.1", + "easy-extender": "^2.3.4", + "eazy-logger": "^4.0.1", + "etag": "^1.8.1", + "fresh": "^0.5.2", + "fs-extra": "3.0.1", + "http-proxy": "^1.18.1", + "immutable": "^3", + "micromatch": "^4.0.8", + "opn": "5.3.0", + "portscanner": "2.2.0", + "raw-body": "^2.3.2", + "resp-modifier": "6.0.2", + "rx": "4.1.0", + "send": "^0.19.0", + "serve-index": "^1.9.1", + "serve-static": "^1.16.2", + "server-destroy": "1.0.1", + "socket.io": "^4.4.1", + "ua-parser-js": "^1.0.33", + "yargs": "^17.3.1" + }, + "bin": { + "browser-sync": "dist/bin.js" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/browser-sync-client": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/browser-sync-client/-/browser-sync-client-3.0.3.tgz", + "integrity": "sha512-TOEXaMgYNjBYIcmX5zDlOdjEqCeCN/d7opf/fuyUD/hhGVCfP54iQIDhENCi012AqzYZm3BvuFl57vbwSTwkSQ==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "etag": "1.8.1", + "fresh": "0.5.2", + "mitt": "^1.1.3" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/browser-sync-ui": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/browser-sync-ui/-/browser-sync-ui-3.0.3.tgz", + "integrity": "sha512-FcGWo5lP5VodPY6O/f4pXQy5FFh4JK0f2/fTBsp0Lx1NtyBWs/IfPPJbW8m1ujTW/2r07oUXKTF2LYZlCZktjw==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "async-each-series": "0.1.1", + "chalk": "4.1.2", + "connect-history-api-fallback": "^1", + "immutable": "^3", + "server-destroy": "1.0.1", + "socket.io-client": "^4.4.1", + "stream-throttle": "^0.1.3" + } + }, + "node_modules/browser-sync-ui/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/browser-sync-ui/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/browser-sync-ui/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/browser-sync-ui/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "optional": true, + "peer": true + }, + "node_modules/browser-sync-ui/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "optional": true, + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/browser-sync-ui/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browser-sync/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/browser-sync/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/browser-sync/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/browser-sync/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "optional": true, + "peer": true + }, + "node_modules/browser-sync/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "optional": true, + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/browser-sync/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.0.tgz", + "integrity": "sha512-Rmb62sR1Zpjql25eSanFGEhAxcFwfA1K0GuQcLoaJBAcENegrQut3hYdhXFF1obQfiDyqIW/cLM5HSJ/9k884A==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001663", + "electron-to-chromium": "^1.5.28", + "node-releases": "^2.0.18", + "update-browserslist-db": "^1.1.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bs-recipes": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/bs-recipes/-/bs-recipes-1.3.4.tgz", + "integrity": "sha512-BXvDkqhDNxXEjeGM8LFkSbR+jzmP/CYpCiVKYn+soB1dDldeU15EBNDkwVXndKuX35wnNUaPd0qSoQEAkmQtMw==", + "dev": true, + "optional": true, + "peer": true + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/bytesish": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/bytesish/-/bytesish-0.4.4.tgz", + "integrity": "sha512-i4uu6M4zuMUiyfZN4RU2+i9+peJh//pXhd9x1oSe1LBkZ3LEbCoygu8W0bXTukU1Jme2txKuotpCZRaC3FLxcQ==", + "dev": true + }, + "node_modules/cacache": { + "version": "15.3.0", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-15.3.0.tgz", + "integrity": "sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==", + "dev": true, + "dependencies": { + "@npmcli/fs": "^1.0.0", + "@npmcli/move-file": "^1.0.1", + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "glob": "^7.1.4", + "infer-owner": "^1.0.4", + "lru-cache": "^6.0.0", + "minipass": "^3.1.1", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.2", + "mkdirp": "^1.0.3", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^3.0.2", + "ssri": "^8.0.1", + "tar": "^6.0.2", + "unique-filename": "^1.1.1" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/cacache/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cachedir": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/cachedir/-/cachedir-2.4.0.tgz", + "integrity": "sha512-9EtFOZR8g22CL7BWjJ9BUx1+A/djkofnyW3aOXZORNW2kxoUpx2h+uN2cOqwPmFhnpVmxg+KW2OjOSgChTEvsQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001668", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001668.tgz", + "integrity": "sha512-nWLrdxqCdblixUO+27JtGJJE/txpJlyUy5YN1u53wLZkP0emYCo5zgS6QYft7VUYR42LGgi/S5hdLZTrnyIddw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", + "dev": true + }, + "node_modules/cerialize": { + "version": "0.1.18", + "resolved": "https://registry.npmjs.org/cerialize/-/cerialize-0.1.18.tgz", + "integrity": "sha512-C/hp4UoPrMK060251Pt/21axF9aL4ceJlg3+pThB68VghhRjOzBzy4f8R9AirXdNB4gpqgaV2deU3UaexInL5w==", + "dependencies": { + "typescript": "^2.5.0" + } + }, + "node_modules/cerialize/node_modules/typescript": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.9.2.tgz", + "integrity": "sha512-Gr4p6nFNaoufRIY4NMdpQRNmgxVIGMs4Fcu/ujdYk3nAZqk7supzBE9idmvfZIlH/Cuj//dvi+019qEue9lV0w==", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "dev": true + }, + "node_modules/charenc": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", + "integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==", + "engines": { + "node": "*" + } + }, + "node_modules/check-more-types": { + "version": "2.24.0", + "resolved": "https://registry.npmjs.org/check-more-types/-/check-more-types-2.24.0.tgz", + "integrity": "sha512-Pj779qHxV2tuapviy1bSZNEL1maXr13bPYpsvSDB68HlYcYuhlDrmGd63i0JHMCLKzc7rUSNIrpdJlhVlNwrxA==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "dev": true, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/ci-info": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.1.0.tgz", + "integrity": "sha512-HutrvTNsF48wnxkzERIXOe5/mlcfFcbfCmwcg6CJnizbSue78AbDt+1cgl26zwn61WFxhcPykPfZrbqjGmBb4A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "engines": { + "node": ">=8" + } + }, + "node_modules/classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==" + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-progress": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/cli-progress/-/cli-progress-3.12.0.tgz", + "integrity": "sha512-tRkV3HJ1ASwm19THiiLIXLO7Im7wlTuKnvkYaTkyoAPefqjNg7W7DHKUlGRxy9vxDvbyCYQkQozvptuMkGCg8A==", + "dependencies": { + "string-width": "^4.2.3" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cli-spinners": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.6.1.tgz", + "integrity": "sha512-x/5fWmGMnbKQAaNwN+UZlV79qBLM9JFnJuJ03gIi5whrob0xV0ofNVHy9DhwGdsMJQc2OKv0oGmLzvaqvAVv+g==", + "dev": true, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-table3": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.5.tgz", + "integrity": "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0" + }, + "engines": { + "node": "10.* || >= 12.*" + }, + "optionalDependencies": { + "@colors/colors": "1.5.0" + } + }, + "node_modules/cli-truncate": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", + "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", + "dev": true, + "dependencies": { + "slice-ansi": "^3.0.0", + "string-width": "^4.2.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "dev": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cliui/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/cliui/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "dev": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dev": true, + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/clone-deep/node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true + }, + "node_modules/colors": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", + "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", + "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/comment-parser": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.3.1.tgz", + "integrity": "sha512-B52sN2VNghyq5ofvUsqZjmk6YkihBX5vMSChmSK9v4ShjKf3Vk5Xcmgpw4o+iIgtrnM/u5FiMpz9VKb8lpBveA==", + "dev": true, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/common-path-prefix": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/common-path-prefix/-/common-path-prefix-3.0.0.tgz", + "integrity": "sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==", + "dev": true + }, + "node_modules/common-tags": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz", + "integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==", + "dev": true, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "dev": true + }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.5.tgz", + "integrity": "sha512-bQJ0YRck5ak3LgtnpKkiabX5pNF7tMUh1BSy2ZBOTh0Dim0BUu6aPPwByIns6/A5Prh8PufSPerMDUklpzes2Q==", + "dependencies": { + "bytes": "3.1.2", + "compressible": "~2.0.18", + "debug": "2.6.9", + "negotiator": "~0.6.4", + "on-headers": "~1.0.2", + "safe-buffer": "5.2.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/compression-webpack-plugin": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/compression-webpack-plugin/-/compression-webpack-plugin-9.2.0.tgz", + "integrity": "sha512-R/Oi+2+UHotGfu72fJiRoVpuRifZT0tTC6UqFD/DUo+mv8dbOow9rVOuTvDv5nPPm3GZhHL/fKkwxwIHnJ8Nyw==", + "dev": true, + "dependencies": { + "schema-utils": "^4.0.0", + "serialize-javascript": "^6.0.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + } + }, + "node_modules/compression/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/compression/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/compression/node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, + "node_modules/connect": { + "version": "3.6.6", + "resolved": "https://registry.npmjs.org/connect/-/connect-3.6.6.tgz", + "integrity": "sha512-OO7axMmPpu/2XuX1+2Yrg0ddju31B6xLZMWkJ5rYBu4YRmRVlOjvlY6kw2FJKiAzyxGwnrDUAG4s1Pf0sbBMCQ==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "debug": "2.6.9", + "finalhandler": "1.1.0", + "parseurl": "~1.3.2", + "utils-merge": "1.0.1" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/connect-history-api-fallback": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz", + "integrity": "sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg==", + "dev": true, + "optional": true, + "peer": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/connect/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/connect/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "optional": true, + "peer": true + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-disposition/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==" + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-parser": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz", + "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==", + "dependencies": { + "cookie": "0.7.2", + "cookie-signature": "1.0.6" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + }, + "node_modules/copy-anything": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-2.0.6.tgz", + "integrity": "sha512-1j20GZTsvKNkc4BY3NpMOM8tt///wY3FpIzozTOFO2ffuZcV61nojHXVKIy3WM+7ADCy5FVhdZYHYDdgTU0yJw==", + "dev": true, + "dependencies": { + "is-what": "^3.14.1" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/copy-to-clipboard": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz", + "integrity": "sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==", + "dependencies": { + "toggle-selection": "^1.0.6" + } + }, + "node_modules/copy-webpack-plugin": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-6.4.1.tgz", + "integrity": "sha512-MXyPCjdPVx5iiWyl40Va3JGh27bKzOTNY3NjUTrosD2q7dR/cLD0013uqJ3BpFbUjyONINjb6qI7nDIJujrMbA==", + "dev": true, + "dependencies": { + "cacache": "^15.0.5", + "fast-glob": "^3.2.4", + "find-cache-dir": "^3.3.1", + "glob-parent": "^5.1.1", + "globby": "^11.0.1", + "loader-utils": "^2.0.0", + "normalize-path": "^3.0.0", + "p-limit": "^3.0.2", + "schema-utils": "^3.0.0", + "serialize-javascript": "^5.0.1", + "webpack-sources": "^1.4.3" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.37.0 || ^5.0.0" + } + }, + "node_modules/copy-webpack-plugin/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/copy-webpack-plugin/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/copy-webpack-plugin/node_modules/find-cache-dir": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", + "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", + "dev": true, + "dependencies": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/avajs/find-cache-dir?sponsor=1" + } + }, + "node_modules/copy-webpack-plugin/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/copy-webpack-plugin/node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dev": true, + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/copy-webpack-plugin/node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/copy-webpack-plugin/node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/copy-webpack-plugin/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/copy-webpack-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/copy-webpack-plugin/node_modules/serialize-javascript": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-5.0.1.tgz", + "integrity": "sha512-SaaNal9imEO737H2c05Og0/8LUXG7EnsZyMa8MzkmuHoELfT6txuj0cMqRj6zfPKnmQ1yasR4PCJc8x+M4JSPA==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/core-js": { + "version": "3.40.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.40.0.tgz", + "integrity": "sha512-7vsMc/Lty6AGnn7uFpYT56QesI5D2Y/UkgKounk87OP9Z2H9Z8kj6jzcSGAxFmUtDOS0ntK6lbQz+Nsa0Jj6mQ==", + "hasInstallScript": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-js-compat": { + "version": "3.38.1", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.38.1.tgz", + "integrity": "sha512-JRH6gfXxGmrzF3tZ57lFx97YARxCXPaMzPo6jELZhv88pBH5VXpQ+y0znKGlFnzuaihqhLbefxSJxWJMPtfDzw==", + "dev": true, + "dependencies": { + "browserslist": "^4.23.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", + "dev": true + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dev": true, + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "dev": true, + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, + "node_modules/critters": { + "version": "0.0.22", + "resolved": "https://registry.npmjs.org/critters/-/critters-0.0.22.tgz", + "integrity": "sha512-NU7DEcQZM2Dy8XTKFHxtdnIM/drE312j2T4PCVaSUcS0oBeyT/NImpRw/Ap0zOr/1SE7SgPK9tGPg1WK/sVakw==", + "dependencies": { + "chalk": "^4.1.0", + "css-select": "^5.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.2", + "htmlparser2": "^8.0.2", + "postcss": "^8.4.23", + "postcss-media-query-parser": "^0.2.3" + } + }, + "node_modules/critters/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/critters/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/critters/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/critters/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/critters/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/critters/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cross-env": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", + "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "bin": { + "cross-env": "src/bin/cross-env.js", + "cross-env-shell": "src/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=10.14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/crypt": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", + "integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==", + "engines": { + "node": "*" + } + }, + "node_modules/css-blank-pseudo": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/css-blank-pseudo/-/css-blank-pseudo-3.0.3.tgz", + "integrity": "sha512-VS90XWtsHGqoM0t4KpH053c4ehxZ2E6HtGI7x68YFV0pTo/QmkV/YFA+NnlvK8guxZVNWGQhVNJGC39Q8XF4OQ==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.9" + }, + "bin": { + "css-blank-pseudo": "dist/cli.cjs" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/css-box-model": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/css-box-model/-/css-box-model-1.2.1.tgz", + "integrity": "sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==", + "dependencies": { + "tiny-invariant": "^1.0.6" + } + }, + "node_modules/css-has-pseudo": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/css-has-pseudo/-/css-has-pseudo-3.0.4.tgz", + "integrity": "sha512-Vse0xpR1K9MNlp2j5w1pgWIJtm1a8qS0JwS9goFYcImjlHEmywP9VUF05aGBXzGpDJF86QXk4L0ypBmwPhGArw==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.9" + }, + "bin": { + "css-has-pseudo": "dist/cli.cjs" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/css-loader": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.10.0.tgz", + "integrity": "sha512-LTSA/jWbwdMlk+rhmElbDR2vbtQoTBPr7fkJE+mxrHj+7ru0hUmHafDRzWIjIHTwpitWVaqY2/UWGRca3yUgRw==", + "dev": true, + "dependencies": { + "icss-utils": "^5.1.0", + "postcss": "^8.4.33", + "postcss-modules-extract-imports": "^3.0.0", + "postcss-modules-local-by-default": "^4.0.4", + "postcss-modules-scope": "^3.1.1", + "postcss-modules-values": "^4.0.0", + "postcss-value-parser": "^4.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/css-prefers-color-scheme": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/css-prefers-color-scheme/-/css-prefers-color-scheme-6.0.3.tgz", + "integrity": "sha512-4BqMbZksRkJQx2zAjrokiGMd07RqOa2IxIrrN10lyBe9xhn9DEvjUK79J6jkeiv9D9hQFXKb6g1jwU62jziJZA==", + "dev": true, + "bin": { + "css-prefers-color-scheme": "dist/cli.cjs" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/css-select": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", + "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-vendor": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/css-vendor/-/css-vendor-2.0.8.tgz", + "integrity": "sha512-x9Aq0XTInxrkuFeHKbYC7zWY8ai7qJ04Kxd9MnvbC1uO5DagxoHQjm4JvG+vCdXOoFtCjbL2XSZfxmoYa9uQVQ==", + "dependencies": { + "@babel/runtime": "^7.8.3", + "is-in-browser": "^1.0.2" + } + }, + "node_modules/css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cssdb": { + "version": "7.11.2", + "resolved": "https://registry.npmjs.org/cssdb/-/cssdb-7.11.2.tgz", + "integrity": "sha512-lhQ32TFkc1X4eTefGfYPvgovRSzIMofHkigfH8nWtyRL4XJLsRhJFreRvEgKzept7x1rjBuy3J/MurXLaFxW/A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + } + ] + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "2.6.21", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.21.tgz", + "integrity": "sha512-Z1PhmomIfypOpoMjRQB70jfvy/wxT50qW08YXO5lMIJkrdq4yOTR+AW7FqutScmB9NkLwxo+jU+kZLbofZZq/w==" + }, + "node_modules/custom-event": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/custom-event/-/custom-event-1.0.1.tgz", + "integrity": "sha512-GAj5FOq0Hd+RsCGVJxZuKaIDXDf3h6GQoNEjFgbLLI/trgtavwUbSnZ5pVfg27DVCaWjIohryS0JFwIJyT2cMg==", + "dev": true + }, + "node_modules/cypress": { + "version": "13.17.0", + "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.17.0.tgz", + "integrity": "sha512-5xWkaPurwkIljojFidhw8lFScyxhtiFHl/i/3zov+1Z5CmY4t9tjIdvSXfu82Y3w7wt0uR9KkucbhkVvJZLQSA==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "@cypress/request": "^3.0.6", + "@cypress/xvfb": "^1.2.4", + "@types/sinonjs__fake-timers": "8.1.1", + "@types/sizzle": "^2.3.2", + "arch": "^2.2.0", + "blob-util": "^2.0.2", + "bluebird": "^3.7.2", + "buffer": "^5.7.1", + "cachedir": "^2.3.0", + "chalk": "^4.1.0", + "check-more-types": "^2.24.0", + "ci-info": "^4.0.0", + "cli-cursor": "^3.1.0", + "cli-table3": "~0.6.1", + "commander": "^6.2.1", + "common-tags": "^1.8.0", + "dayjs": "^1.10.4", + "debug": "^4.3.4", + "enquirer": "^2.3.6", + "eventemitter2": "6.4.7", + "execa": "4.1.0", + "executable": "^4.1.1", + "extract-zip": "2.0.1", + "figures": "^3.2.0", + "fs-extra": "^9.1.0", + "getos": "^3.2.1", + "is-installed-globally": "~0.4.0", + "lazy-ass": "^1.6.0", + "listr2": "^3.8.3", + "lodash": "^4.17.21", + "log-symbols": "^4.0.0", + "minimist": "^1.2.8", + "ospath": "^1.2.2", + "pretty-bytes": "^5.6.0", + "process": "^0.11.10", + "proxy-from-env": "1.0.0", + "request-progress": "^3.0.0", + "semver": "^7.5.3", + "supports-color": "^8.1.1", + "tmp": "~0.2.3", + "tree-kill": "1.2.2", + "untildify": "^4.0.0", + "yauzl": "^2.10.0" + }, + "bin": { + "cypress": "bin/cypress" + }, + "engines": { + "node": "^16.0.0 || ^18.0.0 || >=20.0.0" + } + }, + "node_modules/cypress-axe": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/cypress-axe/-/cypress-axe-1.5.0.tgz", + "integrity": "sha512-Hy/owCjfj+25KMsecvDgo4fC/781ccL+e8p+UUYoadGVM2ogZF9XIKbiM6KI8Y3cEaSreymdD6ZzccbI2bY0lQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "axe-core": "^3 || ^4", + "cypress": "^10 || ^11 || ^12 || ^13" + } + }, + "node_modules/cypress/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cypress/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/cypress/node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cypress/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/cypress/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/cypress/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cypress/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/cypress/node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/cypress/node_modules/proxy-from-env": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.0.0.tgz", + "integrity": "sha512-F2JHgJQ1iqwnHDcQjVBsq3n/uoaFL+iPW/eAeL7kVxy/2RrWaN4WroKjjvbsoRtv0ftelNyC01bjRhn/bhcf4A==", + "dev": true + }, + "node_modules/cypress/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/cypress/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", + "dev": true, + "dependencies": { + "assert-plus": "^1.0.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/data-view-buffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz", + "integrity": "sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz", + "integrity": "sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz", + "integrity": "sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/date-fns": { + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", + "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", + "dependencies": { + "@babel/runtime": "^7.21.0" + }, + "engines": { + "node": ">=0.11" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/date-fns" + } + }, + "node_modules/date-fns-tz": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/date-fns-tz/-/date-fns-tz-1.3.8.tgz", + "integrity": "sha512-qwNXUFtMHTTU6CFSFjoJ80W8Fzzp24LntbjFFBgL/faqds4e5mo9mftoRLgr3Vi1trISsg4awSpYVsOQCRnapQ==", + "peerDependencies": { + "date-fns": ">=2.0.0" + } + }, + "node_modules/date-format": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/date-format/-/date-format-4.0.14.tgz", + "integrity": "sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/dayjs": { + "version": "1.11.11", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.11.tgz", + "integrity": "sha512-okzr3f11N6WuqYtZSvm+F776mB41wRZMhKP+hc34YdW+KmtYYK9iqvHSwo2k9FEH3fhGXvOPV6yz2IcSrfRUDg==", + "dev": true + }, + "node_modules/debug": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-5.0.1.tgz", + "integrity": "sha512-VfxadyCECXgQlkoEAjeghAr5gY3Hf+IKjKb+X8tGVDtveCjN+USwprd2q3QXBR9T1+x2DG0XZF5/w+7HAtSaXA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-freeze": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/deep-freeze/-/deep-freeze-0.0.1.tgz", + "integrity": "sha512-Z+z8HiAvsGwmjqlphnHW5oz6yWlOwu6EQfFTjmeTWlDeda3FS2yv3jhq35TX/ewmsnqB+RX2IdsIOyjJCQN5tg==", + "dev": true + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/default-gateway": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-6.0.3.tgz", + "integrity": "sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg==", + "dev": true, + "dependencies": { + "execa": "^5.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/default-gateway/node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/default-gateway/node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-gateway/node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "dev": true, + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delay": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/delay/-/delay-5.0.0.tgz", + "integrity": "sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "dev": true, + "optional": true, + "bin": { + "detect-libc": "bin/detect-libc.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "dev": true + }, + "node_modules/dev-ip": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dev-ip/-/dev-ip-1.0.1.tgz", + "integrity": "sha512-LmVkry/oDShEgSZPNgqCIp2/TlqtExeGmymru3uCELnfyjY11IzpAproLYs+1X88fXO6DBoYP3ul2Xo2yz2j6A==", + "dev": true, + "optional": true, + "peer": true, + "bin": { + "dev-ip": "lib/dev-ip.js" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/di": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/di/-/di-0.0.1.tgz", + "integrity": "sha512-uJaamHkagcZtHPqCIHZxnFrXlunQXgBOsZSUOWwFw31QJCAbyTBoHMW75YOTur5ZNx8pIeAKgf6GWIgaqqiLhA==", + "dev": true + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dnd-core": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/dnd-core/-/dnd-core-10.0.2.tgz", + "integrity": "sha512-PrxEjxF0+6Y1n1n1Z9hSWZ1tvnDXv9syL+BccV1r1RC08uWNsyetf8AnWmUF3NgYPwy0HKQJwTqGkZK+1NlaFA==", + "dependencies": { + "@react-dnd/asap": "^4.0.0", + "@react-dnd/invariant": "^2.0.0", + "redux": "^4.0.4" + } + }, + "node_modules/dnd-multi-backend": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/dnd-multi-backend/-/dnd-multi-backend-5.0.1.tgz", + "integrity": "sha512-BAOj6fIeGfZPA1LreehaV8mUD7Ww0EpaKqSDRrZ5mZXLWLIxFPPT3bIvpJhHUCt0+lTrOqkXu11Hudh+gHXNUA==" + }, + "node_modules/dns-packet": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz", + "integrity": "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==", + "dev": true, + "dependencies": { + "@leichtgewicht/ip-codec": "^2.0.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, + "node_modules/dom-helpers/node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" + }, + "node_modules/dom-serialize": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/dom-serialize/-/dom-serialize-2.2.1.tgz", + "integrity": "sha512-Yra4DbvoW7/Z6LBN560ZwXMjoNOSAN2wRsKFGc4iBeso+mpIA6qj1vfdf9HpMaKAqG6wXTy+1SYEzmNpKXOSsQ==", + "dev": true, + "dependencies": { + "custom-event": "~1.0.0", + "ent": "~2.2.0", + "extend": "^3.0.0", + "void-elements": "^2.0.0" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ] + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/dompurify": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.5.6.tgz", + "integrity": "sha512-zUTaUBO8pY4+iJMPE1B9XlO2tXVYIcEA4SNGtvDELzTSCQO7RzH+j7S180BmhmJId78lqGU2z19vgVx2Sxs/PQ==" + }, + "node_modules/domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, + "node_modules/easy-extender": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/easy-extender/-/easy-extender-2.3.4.tgz", + "integrity": "sha512-8cAwm6md1YTiPpOvDULYJL4ZS6WfM5/cTeVVh4JsvyYZAoqlRVUpHL9Gr5Fy7HA6xcSZicUia3DeAgO3Us8E+Q==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "lodash": "^4.17.10" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/eazy-logger": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/eazy-logger/-/eazy-logger-4.0.1.tgz", + "integrity": "sha512-2GSFtnnC6U4IEKhEI7+PvdxrmjJ04mdsj3wHZTFiw0tUtG4HCWzTr13ZYTk8XOGnA1xQMaDljoBOYlk3D/MMSw==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "chalk": "4.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/eazy-logger/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/eazy-logger/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/eazy-logger/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/eazy-logger/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "optional": true, + "peer": true + }, + "node_modules/eazy-logger/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "optional": true, + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/eazy-logger/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", + "dev": true, + "dependencies": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "node_modules/ecc-jsbn/node_modules/jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==", + "dev": true + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.38", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.38.tgz", + "integrity": "sha512-VbeVexmZ1IFh+5EfrYz1I0HTzHVIlJa112UEWhciPyeOcKJGeTv6N8WnG4wsQB81DGCaVEGhpSb6o6a8WYFXXg==" + }, + "node_modules/element-resize-detector": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/element-resize-detector/-/element-resize-detector-1.2.4.tgz", + "integrity": "sha512-Fl5Ftk6WwXE0wqCgNoseKWndjzZlDCwuPTcoVZfCP9R3EHQF8qUtr3YUPNETegRBOKqQKPW3n4kiIWngGi8tKg==", + "dependencies": { + "batch-processor": "1.0.0" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dev": true, + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/engine.io": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.2.tgz", + "integrity": "sha512-gmNvsYi9C8iErnZdVcJnvCpSKbWTt1E8+JZo8b+daLninywUWi5NQ5STSHZ9rFjFO7imNcvb8Pc5pe/wMR5xEw==", + "dev": true, + "dependencies": { + "@types/cookie": "^0.4.1", + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.7.2", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-client": { + "version": "6.6.1", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.1.tgz", + "integrity": "sha512-aYuoak7I+R83M/BBPIOs2to51BmFIpC1wZe6zZzMrT2llVsHy5cvcmdsJgP2Qz6smHu+sD9oexiSUAVd8OfBPw==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1", + "xmlhttprequest-ssl": "~2.1.1" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.2.tgz", + "integrity": "sha512-RcyUFKA93/CXH20l4SoVvzZfrSDMOTUS3bWVpTt2FuFP+XYrL8i8oonHP7WInRyVHXh0n/ORtoeiE1os+8qkSw==", + "dev": true, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.17.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz", + "integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/enquirer": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", + "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==", + "dev": true, + "dependencies": { + "ansi-colors": "^4.1.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/ent": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.1.tgz", + "integrity": "sha512-QHuXVeZx9d+tIQAz/XztU0ZwZf2Agg9CcXcgE1rurqvdBeDBrpSwjl8/6XUqMg7tw2Y7uAdKb2sRv+bSEFqQ5A==", + "dev": true, + "dependencies": { + "punycode": "^1.4.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/envinfo": { + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.13.0.tgz", + "integrity": "sha512-cvcaMr7KqXVh4nyzGTVqTum+gAiL265x5jUWQIDLq//zOGbW+gSW/C+OWLleY/rs9Qole6AZLMXPbtIFQbqu+Q==", + "dev": true, + "bin": { + "envinfo": "dist/cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "dev": true + }, + "node_modules/errno": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", + "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", + "dev": true, + "optional": true, + "dependencies": { + "prr": "~1.0.1" + }, + "bin": { + "errno": "cli.js" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-abstract": { + "version": "1.23.3", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.3.tgz", + "integrity": "sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "arraybuffer.prototype.slice": "^1.0.3", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "data-view-buffer": "^1.0.1", + "data-view-byte-length": "^1.0.1", + "data-view-byte-offset": "^1.0.0", + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-set-tostringtag": "^2.0.3", + "es-to-primitive": "^1.2.1", + "function.prototype.name": "^1.1.6", + "get-intrinsic": "^1.2.4", + "get-symbol-description": "^1.0.2", + "globalthis": "^1.0.3", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.0.3", + "has-symbols": "^1.0.3", + "hasown": "^2.0.2", + "internal-slot": "^1.0.7", + "is-array-buffer": "^3.0.4", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.1", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.3", + "is-string": "^1.0.7", + "is-typed-array": "^1.1.13", + "is-weakref": "^1.0.2", + "object-inspect": "^1.13.1", + "object-keys": "^1.1.1", + "object.assign": "^4.1.5", + "regexp.prototype.flags": "^1.5.2", + "safe-array-concat": "^1.1.2", + "safe-regex-test": "^1.0.3", + "string.prototype.trim": "^1.2.9", + "string.prototype.trimend": "^1.0.8", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.2", + "typed-array-byte-length": "^1.0.1", + "typed-array-byte-offset": "^1.0.2", + "typed-array-length": "^1.0.6", + "unbox-primitive": "^1.0.2", + "which-typed-array": "^1.1.15" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.4.tgz", + "integrity": "sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==", + "dev": true + }, + "node_modules/es-object-atoms": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", + "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz", + "integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.4", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz", + "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==", + "dev": true, + "dependencies": { + "hasown": "^2.0.0" + } + }, + "node_modules/es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "dependencies": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es6-promisify": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-7.0.0.tgz", + "integrity": "sha512-ginqzK3J90Rd4/Yz7qRrqUeIpe3TwSXTPPZtPne7tGBPeAaQiU8qt4fpKApnxHcq1AwtUdHVg5P77x/yrggG8Q==", + "engines": { + "node": ">=6" + } + }, + "node_modules/esbuild": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.1.tgz", + "integrity": "sha512-OJwEgrpWm/PCMsLVWXKqvcjme3bHNpOgN7Tb6cQnR5n0TPbQx1/Xrn7rqM+wn17bYeT6MGB5sn1Bh5YiGi70nA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.20.1", + "@esbuild/android-arm": "0.20.1", + "@esbuild/android-arm64": "0.20.1", + "@esbuild/android-x64": "0.20.1", + "@esbuild/darwin-arm64": "0.20.1", + "@esbuild/darwin-x64": "0.20.1", + "@esbuild/freebsd-arm64": "0.20.1", + "@esbuild/freebsd-x64": "0.20.1", + "@esbuild/linux-arm": "0.20.1", + "@esbuild/linux-arm64": "0.20.1", + "@esbuild/linux-ia32": "0.20.1", + "@esbuild/linux-loong64": "0.20.1", + "@esbuild/linux-mips64el": "0.20.1", + "@esbuild/linux-ppc64": "0.20.1", + "@esbuild/linux-riscv64": "0.20.1", + "@esbuild/linux-s390x": "0.20.1", + "@esbuild/linux-x64": "0.20.1", + "@esbuild/netbsd-x64": "0.20.1", + "@esbuild/openbsd-x64": "0.20.1", + "@esbuild/sunos-x64": "0.20.1", + "@esbuild/win32-arm64": "0.20.1", + "@esbuild/win32-ia32": "0.20.1", + "@esbuild/win32-x64": "0.20.1" + } + }, + "node_modules/esbuild-wasm": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/esbuild-wasm/-/esbuild-wasm-0.20.1.tgz", + "integrity": "sha512-6v/WJubRsjxBbQdz6izgvx7LsVFvVaGmSdwrFHmEzoVgfXL89hkKPoQHsnVI2ngOkcBUQT9kmAM1hVL1k/Av4A==", + "dev": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/escalade": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/eslint": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", + "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.0", + "@humanwhocodes/config-array": "^0.11.14", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-compat-utils": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/eslint-compat-utils/-/eslint-compat-utils-0.6.4.tgz", + "integrity": "sha512-/u+GQt8NMfXO8w17QendT4gvO5acfxQsAKirAt0LVxDnr2N8YLCVbregaNc/Yhp7NM128DwCaRvr8PLDfeNkQw==", + "dev": true, + "dependencies": { + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "eslint": ">=6.0.0" + } + }, + "node_modules/eslint-etc": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/eslint-etc/-/eslint-etc-5.2.1.tgz", + "integrity": "sha512-lFJBSiIURdqQKq9xJhvSJFyPA+VeTh5xvk24e8pxVL7bwLBtGF60C/KRkLTMrvCZ6DA3kbPuYhLWY0TZMlqTsg==", + "dev": true, + "dependencies": { + "@typescript-eslint/experimental-utils": "^5.0.0", + "tsutils": "^3.17.1", + "tsutils-etc": "^1.4.1" + }, + "peerDependencies": { + "eslint": "^8.0.0", + "typescript": ">=4.0.0" + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", + "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", + "dev": true, + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-json-compat-utils": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/eslint-json-compat-utils/-/eslint-json-compat-utils-0.2.1.tgz", + "integrity": "sha512-YzEodbDyW8DX8bImKhAcCeu/L31Dd/70Bidx2Qex9OFUtgzXLqtfWL4Hr5fM/aCCB8QUZLuJur0S9k6UfgFkfg==", + "dev": true, + "dependencies": { + "esquery": "^1.6.0" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "eslint": "*", + "jsonc-eslint-parser": "^2.4.0" + }, + "peerDependenciesMeta": { + "@eslint/json": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz", + "integrity": "sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==", + "dev": true, + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-deprecation": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-deprecation/-/eslint-plugin-deprecation-1.5.0.tgz", + "integrity": "sha512-mRcssI/tLROueBQ6yf4LnnGTijbMsTCPIpbRbPj5R5wGYVCpk1zDmAS0SEkgcUDXOPc22qMNFR24Qw7vSPrlTA==", + "dev": true, + "dependencies": { + "@typescript-eslint/utils": "^5.57.0", + "tslib": "^2.3.1", + "tsutils": "^3.21.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0", + "typescript": "^3.7.5 || ^4.0.0 || ^5.0.0" + } + }, + "node_modules/eslint-plugin-deprecation/node_modules/@typescript-eslint/scope-manager": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz", + "integrity": "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/eslint-plugin-deprecation/node_modules/@typescript-eslint/types": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz", + "integrity": "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/eslint-plugin-deprecation/node_modules/@typescript-eslint/typescript-estree": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz", + "integrity": "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-deprecation/node_modules/@typescript-eslint/utils": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz", + "integrity": "sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@types/json-schema": "^7.0.9", + "@types/semver": "^7.3.12", + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/typescript-estree": "5.62.0", + "eslint-scope": "^5.1.1", + "semver": "^7.3.7" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/eslint-plugin-deprecation/node_modules/@typescript-eslint/visitor-keys": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz", + "integrity": "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/eslint-plugin-deprecation/node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/eslint-plugin-deprecation/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/eslint-plugin-dspace-angular-html": { + "resolved": "lint/dist/src/rules/html", + "link": true + }, + "node_modules/eslint-plugin-dspace-angular-ts": { + "resolved": "lint/dist/src/rules/ts", + "link": true + }, + "node_modules/eslint-plugin-import": { + "version": "2.31.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz", + "integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==", + "dev": true, + "dependencies": { + "@rtsao/scc": "^1.1.0", + "array-includes": "^3.1.8", + "array.prototype.findlastindex": "^1.2.5", + "array.prototype.flat": "^1.3.2", + "array.prototype.flatmap": "^1.3.2", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.12.0", + "hasown": "^2.0.2", + "is-core-module": "^2.15.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "object.groupby": "^1.0.3", + "object.values": "^1.2.0", + "semver": "^6.3.1", + "string.prototype.trimend": "^1.0.8", + "tsconfig-paths": "^3.15.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-import-newlines": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import-newlines/-/eslint-plugin-import-newlines-1.4.0.tgz", + "integrity": "sha512-+Cz1x2xBLtI9gJbmuYEpvY7F8K75wskBmJ7rk4VRObIJo+jklUJaejFJgtnWeL0dCFWabGEkhausrikXaNbtoQ==", + "dev": true, + "bin": { + "import-linter": "lib/index.js" + }, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "eslint": ">=6.0.0" + } + }, + "node_modules/eslint-plugin-import/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-import/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/eslint-plugin-import/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint-plugin-import/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-plugin-import/node_modules/tsconfig-paths": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", + "dev": true, + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/eslint-plugin-jsdoc": { + "version": "45.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-45.0.0.tgz", + "integrity": "sha512-l2+Jcs/Ps7oFA+SWY+0sweU/e5LgricnEl6EsDlyRTF5y0+NWL1y9Qwz9PHwHAxtdJq6lxPjEQWmYLMkvhzD4g==", + "dev": true, + "dependencies": { + "@es-joy/jsdoccomment": "~0.39.4", + "are-docs-informative": "^0.0.2", + "comment-parser": "1.3.1", + "debug": "^4.3.4", + "escape-string-regexp": "^4.0.0", + "esquery": "^1.5.0", + "semver": "^7.5.1", + "spdx-expression-parse": "^3.0.1" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + } + }, + "node_modules/eslint-plugin-jsdoc/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-plugin-jsonc": { + "version": "2.18.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsonc/-/eslint-plugin-jsonc-2.18.2.tgz", + "integrity": "sha512-SDhJiSsWt3nItl/UuIv+ti4g3m4gpGkmnUJS9UWR3TrpyNsIcnJoBRD7Kof6cM4Rk3L0wrmY5Tm3z7ZPjR2uGg==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "eslint-compat-utils": "^0.6.0", + "eslint-json-compat-utils": "^0.2.1", + "espree": "^9.6.1", + "graphemer": "^1.4.0", + "jsonc-eslint-parser": "^2.0.4", + "natural-compare": "^1.4.0", + "synckit": "^0.6.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ota-meshi" + }, + "peerDependencies": { + "eslint": ">=6.0.0" + } + }, + "node_modules/eslint-plugin-lodash": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-lodash/-/eslint-plugin-lodash-7.4.0.tgz", + "integrity": "sha512-Tl83UwVXqe1OVeBRKUeWcfg6/pCW1GTRObbdnbEJgYwjxp5Q92MEWQaH9+dmzbRt6kvYU1Mp893E79nJiCSM8A==", + "dev": true, + "dependencies": { + "lodash": "^4.17.21" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": ">=2" + } + }, + "node_modules/eslint-plugin-rxjs": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-rxjs/-/eslint-plugin-rxjs-5.0.3.tgz", + "integrity": "sha512-fcVkqLmYLRfRp+ShafjpUKuaZ+cw/sXAvM5dfSxiEr7M28QZ/NY7vaOr09FB4rSaZsQyLBnNPh5SL+4EgKjh8Q==", + "dev": true, + "dependencies": { + "@typescript-eslint/experimental-utils": "^5.0.0", + "common-tags": "^1.8.0", + "decamelize": "^5.0.0", + "eslint-etc": "^5.1.0", + "requireindex": "~1.2.0", + "rxjs-report-usage": "^1.0.4", + "tslib": "^2.0.0", + "tsutils": "^3.0.0", + "tsutils-etc": "^1.4.1" + }, + "peerDependencies": { + "eslint": "^8.0.0", + "typescript": ">=4.0.0" + } + }, + "node_modules/eslint-plugin-simple-import-sort": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-simple-import-sort/-/eslint-plugin-simple-import-sort-10.0.0.tgz", + "integrity": "sha512-AeTvO9UCMSNzIHRkg8S6c3RPy5YEwKWSQPx3DYghLedo2ZQxowPFLGDN1AZ2evfg6r6mjBSZSLxLFsWSu3acsw==", + "dev": true, + "peerDependencies": { + "eslint": ">=5.0.0" + } + }, + "node_modules/eslint-plugin-unused-imports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-3.2.0.tgz", + "integrity": "sha512-6uXyn6xdINEpxE1MtDjxQsyXB37lfyO2yKGVVgtD7WEWQGORSOZjgrD6hBhvGv4/SO+TOlS+UnC6JppRqbuwGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-rule-composer": "^0.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "@typescript-eslint/eslint-plugin": "6 - 7", + "eslint": "8" + }, + "peerDependenciesMeta": { + "@typescript-eslint/eslint-plugin": { + "optional": true + } + } + }, + "node_modules/eslint-rule-composer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/eslint-rule-composer/-/eslint-rule-composer-0.3.0.tgz", + "integrity": "sha512-bt+Sh8CtDmn2OajxvNO+BX7Wn4CIWMpTRm3MaiKPCQcnnlm0CS2mhui6QaoeQugs+3Kj2ESKEEGJUdVafwhiCg==", + "dev": true, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/eslint-scope": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.0.1.tgz", + "integrity": "sha512-pL8XjgP4ZOmmwfFE8mEhSxA7ZY4C+LWyqjQ3o4yWkkmD0qcMT9kkW3zWHOczhWcjTSgqycYAgwSlXvZltv65og==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/eslint/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/eslint/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/eslint/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/eslint/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/eslint/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/eslint/node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint/node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventemitter2": { + "version": "6.4.7", + "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.7.tgz", + "integrity": "sha512-tYUSVOGeQPKt/eC1ABfhHy5Xd96N3oIijJvN3O9+TsC28T5V9yX9oEfEK5faP0EFSNVOG97qtAS68GBrQB2hDg==", + "dev": true + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/execa": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz", + "integrity": "sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "get-stream": "^5.0.0", + "human-signals": "^1.1.1", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.0", + "onetime": "^5.1.0", + "signal-exit": "^3.0.2", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/executable": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/executable/-/executable-4.1.1.tgz", + "integrity": "sha512-8iA79xD3uAch729dUG8xaaBBFGaEa0wdD2VkYLFHwlqosEj/jT66AzcreRDSgV7ehnNLBW2WR5jIXwGKjVdTLg==", + "dev": true, + "dependencies": { + "pify": "^2.2.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/exenv": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/exenv/-/exenv-1.2.2.tgz", + "integrity": "sha512-Z+ktTxTwv9ILfgKCk32OX3n/doe+OcLTRtqK9pcL+JsP3J1/VW8Uvl4ZjLlKqeW4rzK4oesDOGMEMRIZqtP4Iw==" + }, + "node_modules/exponential-backoff": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.1.tgz", + "integrity": "sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw==", + "dev": true + }, + "node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-5.5.1.tgz", + "integrity": "sha512-MTjE2eIbHv5DyfuFz4zLYWxpqVhEhkTiwFGuB74Q9CSou2WHO52nlE5y3Zlg6SIsiYUIPj6ifFxnkPz6O3sIUg==" + }, + "node_modules/express-static-gzip": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/express-static-gzip/-/express-static-gzip-2.2.0.tgz", + "integrity": "sha512-4ZQ0pHX0CAauxmzry2/8XFLM6aZA4NBvg9QezSlsEO1zLnl7vMFa48/WIcjzdfOiEUS4S1npPPKP2NHHYAp6qg==", + "dev": true, + "dependencies": { + "parseurl": "^1.3.3", + "serve-static": "^1.16.2" + } + }, + "node_modules/express/node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/express/node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/express/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true + }, + "node_modules/external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "dev": true, + "dependencies": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/external-editor/node_modules/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "dependencies": { + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", + "dev": true, + "engines": [ + "node >=0.6.0" + ] + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-json-patch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/fast-json-patch/-/fast-json-patch-3.1.1.tgz", + "integrity": "sha512-vf6IHUX2SBcA+5/+4883dsIjpBTqmfBjmYiWK1savxQmFk4JfBMLa7ynTYOs1Rolp/T1betJxHiGD3g1Mn8lUQ==" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/fast-printf": { + "version": "1.6.9", + "resolved": "https://registry.npmjs.org/fast-printf/-/fast-printf-1.6.9.tgz", + "integrity": "sha512-FChq8hbz65WMj4rstcQsFB0O7Cy++nmbNfLYnD9cYv2cRn8EG6k/MGn9kO/tjO66t09DLDugj3yL+V2o6Qftrg==", + "dependencies": { + "boolean": "^3.1.4" + }, + "engines": { + "node": ">=10.0" + } + }, + "node_modules/fastest-levenshtein": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", + "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", + "dev": true, + "engines": { + "node": ">= 4.9.1" + } + }, + "node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "dev": true, + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dev": true, + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/filesize": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/filesize/-/filesize-6.4.0.tgz", + "integrity": "sha512-mjFIpOHC4jbfcTfoh4rkWpI31mF7viw9ikj/JyLoKzqlwG/YsefKfvYlYhdYdg/9mtK2z1AzgN/0LvVQ3zdlSQ==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.0.tgz", + "integrity": "sha512-ejnvM9ZXYzp6PUPUyQBMBf0Co5VX2gr5H2VQe2Ui2jWXNlxv+PYZo8wpAymJNJdLsG1R4p+M4aynF8KuoUEwRw==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.1", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.2", + "statuses": "~1.3.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "optional": true, + "peer": true + }, + "node_modules/finalhandler/node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/statuses": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz", + "integrity": "sha512-wuTCPGlJONk/a1kqZ4fQM2+908lC7fa7nPYpTC1EhnvqLX/IICbeP1OZGDtA374trpSq68YubKUMo8oRhN46yg==", + "dev": true, + "optional": true, + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/find-cache-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-4.0.0.tgz", + "integrity": "sha512-9ZonPT4ZAK4a+1pUPVPZJapbi7O5qbbJPdYw/NOQWZZbVLdDTYM3A4R9z/DpAM08IDaFGsvPgiGZ82WEwUDWjg==", + "dev": true, + "dependencies": { + "common-path-prefix": "^3.0.0", + "pkg-dir": "^7.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "bin": { + "flat": "cli.js" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", + "dev": true + }, + "node_modules/follow-redirects": { + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/for-each": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", + "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "dev": true, + "dependencies": { + "is-callable": "^1.1.3" + } + }, + "node_modules/foreground-child": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.2.1.tgz", + "integrity": "sha512-PXUUyLqrR2XCWICfv6ukppP96sdFwWbNEnfEMt7jNsISjMsvaLNinAHNDYyvkyU+SZG2BTSbT5NjG+vZslfGTA==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "dev": true, + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-extra": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-3.0.1.tgz", + "integrity": "sha512-V3Z3WZWVUYd8hoCL5xfXJCaHWYzmtwW5XWYSlLgERi8PWd8bx1kUHUk8L1BT57e49oKnDDD180mjfrHc1yA9rg==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "jsonfile": "^3.0.0", + "universalify": "^0.1.0" + } + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-monkey": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.6.tgz", + "integrity": "sha512-b1FMfwetIKymC0eioW7mTywihSQE4oLzQn1dB6rZB5fx/3NpNEdAWeCSMB+60/AeT0TCXsxzAlcYVEFCTAksWg==", + "dev": true + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/fscreen": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fscreen/-/fscreen-1.2.0.tgz", + "integrity": "sha512-hlq4+BU0hlPmwsFjwGGzZ+OZ9N/wq9Ljg/sq3pX+2CD7hrJsX9tJgWWK/wiNTFM212CLHWhicOoqwXyZGGetJg==" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz", + "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "functions-have-names": "^1.2.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-symbol-description": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz", + "integrity": "sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/getos": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/getos/-/getos-3.2.1.tgz", + "integrity": "sha512-U56CfOK17OKgTVqozZjUKNdkfEv6jk5WISBJ8SHoagjE6L69zOwl3Z+O8myjY9MEW3i2HPWQBt/LTbCgcC973Q==", + "dev": true, + "dependencies": { + "async": "^3.2.0" + } + }, + "node_modules/getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", + "dev": true, + "dependencies": { + "assert-plus": "^1.0.0" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/global-dirs": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.1.tgz", + "integrity": "sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==", + "dev": true, + "dependencies": { + "ini": "2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/global-dirs/node_modules/ini": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz", + "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "engines": { + "node": ">=4" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, + "node_modules/handle-thing": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", + "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==", + "dev": true + }, + "node_modules/has-bigints": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "engines": { + "node": ">=4" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/hoist-non-react-statics/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, + "node_modules/hosted-git-info": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz", + "integrity": "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==", + "dev": true, + "dependencies": { + "lru-cache": "^10.0.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/hosted-git-info/node_modules/lru-cache": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.0.tgz", + "integrity": "sha512-bfJaPTuEiTYBu+ulDaeQ0F+uLmlfFkMgXj4cbwfuMSjgObGMzb55FMMbDvbRU0fAHZ4sLGkz2mKwcMg8Dvm8Ww==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/hpack.js": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", + "integrity": "sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==", + "dev": true, + "dependencies": { + "inherits": "^2.0.1", + "obuf": "^1.0.0", + "readable-stream": "^2.0.1", + "wbuf": "^1.1.0" + } + }, + "node_modules/hpack.js/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true + }, + "node_modules/hpack.js/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/hpack.js/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/html-entities": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.5.2.tgz", + "integrity": "sha512-K//PSRMQk4FZ78Kyau+mZurHn3FH0Vwr+H36eE0rPbeYkRRi9YxceYPhuN60UwWorxyKHhqoAJl2OFKa4BVtaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/mdevils" + }, + { + "type": "patreon", + "url": "https://patreon.com/mdevils" + } + ] + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, + "node_modules/html-parse-stringify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", + "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "dependencies": { + "void-elements": "3.1.0" + } + }, + "node_modules/html-parse-stringify/node_modules/void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", + "dev": true + }, + "node_modules/http-deceiver": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", + "integrity": "sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==", + "dev": true + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-parser-js": { + "version": "0.5.8", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.8.tgz", + "integrity": "sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==", + "dev": true + }, + "node_modules/http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "dependencies": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/http-proxy-middleware": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.7.tgz", + "integrity": "sha512-fgVY8AV7qU7z/MmXJ/rxwbrtQH4jBQ9m7kp3llF0liB7glmFeVZFBepQb32T3y8n8k2+AEYuMPCpinYW+/CuRA==", + "license": "MIT", + "dependencies": { + "@types/http-proxy": "^1.17.8", + "http-proxy": "^1.18.1", + "is-glob": "^4.0.1", + "is-plain-obj": "^3.0.0", + "micromatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "@types/express": "^4.17.13" + }, + "peerDependenciesMeta": { + "@types/express": { + "optional": true + } + } + }, + "node_modules/http-signature": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.4.0.tgz", + "integrity": "sha512-G5akfn7eKbpDN+8nPS/cb57YeA1jLTVxjpCj7tmm3QKPdyDy7T+qSC40e9ptydSWvkwjSXw1VbkpyEm39ukeAg==", + "dev": true, + "dependencies": { + "assert-plus": "^1.0.0", + "jsprim": "^2.0.2", + "sshpk": "^1.18.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/http-terminator": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/http-terminator/-/http-terminator-3.2.0.tgz", + "integrity": "sha512-JLjck1EzPaWjsmIf8bziM3p9fgR1Y3JoUKAkyYEbZmFrIvJM6I8vVJfBGWlEtV9IWOvzNnaTtjuwZeBY2kwB4g==", + "dependencies": { + "delay": "^5.0.0", + "p-wait-for": "^3.2.0", + "roarr": "^7.0.4", + "type-fest": "^2.3.3" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz", + "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==", + "dev": true, + "dependencies": { + "agent-base": "^7.0.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/human-signals": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", + "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==", + "dev": true, + "engines": { + "node": ">=8.12.0" + } + }, + "node_modules/hyphenate-style-name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.1.0.tgz", + "integrity": "sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==" + }, + "node_modules/i18next": { + "version": "19.9.2", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-19.9.2.tgz", + "integrity": "sha512-0i6cuo6ER6usEOtKajUUDj92zlG+KArFia0857xxiEHAQcUwh/RtOQocui1LPJwunSYT574Pk64aNva1kwtxZg==", + "dependencies": { + "@babel/runtime": "^7.12.0" + } + }, + "node_modules/icomcom-react": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/icomcom-react/-/icomcom-react-1.0.1.tgz", + "integrity": "sha512-Xbz81qZ+er8RYZ6DFMmXxCl9YjxNWngNfPANTSOvzYNrQDieYvBZi+nv1MspI/ze+PAzfHUrmDcUii5RGCUifg==", + "dependencies": { + "prop-types": "^15.6.0" + }, + "peerDependencies": { + "react": "^16.0.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/icss-utils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", + "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", + "dev": true, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/ignore": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", + "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true + }, + "node_modules/ignore-walk": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-6.0.5.tgz", + "integrity": "sha512-VuuG0wCnjhnylG1ABXT3dAuIpTNDs/G8jlpmwXY03fXoXy/8ZK8/T+hMzt8L4WnrLCJgdybqgPagnF/f97cg3A==", + "dev": true, + "dependencies": { + "minimatch": "^9.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/image-size": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz", + "integrity": "sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==", + "dev": true, + "optional": true, + "bin": { + "image-size": "bin/image-size.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/immutability-helper": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/immutability-helper/-/immutability-helper-3.1.1.tgz", + "integrity": "sha512-Q0QaXjPjwIju/28TsugCHNEASwoCcJSyJV3uO1sOIQGI0jKgm9f41Lvz0DZj3n46cNCyAZTsEYoY4C2bVRUzyQ==" + }, + "node_modules/immutable": { + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-3.8.2.tgz", + "integrity": "sha512-15gZoQ38eYjEjxkorfbcgBKBL6R7T459OuK+CpcWt7O3KF4uPCx2tD0uFETlUDIyo+1789crbMhTvQBSR5yBMg==", + "dev": true, + "optional": true, + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-fresh/node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/import-local": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", + "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", + "dev": true, + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-local/node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/infer-owner": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", + "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", + "dev": true + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ini": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.2.tgz", + "integrity": "sha512-AMB1mvwR1pyBFY/nSevUX6y8nJWS63/SzUKD3JyQn97s4xgIdgQPT75IRouIiBAN4yLQBUShNYVW0+UG25daCw==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/inquirer": { + "version": "9.2.15", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-9.2.15.tgz", + "integrity": "sha512-vI2w4zl/mDluHt9YEQ/543VTCwPKWiHzKtm9dM2V0NdFcqEexDAjUHzO1oA60HRNaVifGXXM1tRRNluLVHa0Kg==", + "dev": true, + "dependencies": { + "@ljharb/through": "^2.3.12", + "ansi-escapes": "^4.3.2", + "chalk": "^5.3.0", + "cli-cursor": "^3.1.0", + "cli-width": "^4.1.0", + "external-editor": "^3.1.0", + "figures": "^3.2.0", + "lodash": "^4.17.21", + "mute-stream": "1.0.0", + "ora": "^5.4.1", + "run-async": "^3.0.0", + "rxjs": "^7.8.1", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^6.2.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/inquirer/node_modules/chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "dev": true, + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/internal-slot": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz", + "integrity": "sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.0", + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/interpret": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", + "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", + "dev": true, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/intersection-observer": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/intersection-observer/-/intersection-observer-0.10.0.tgz", + "integrity": "sha512-fn4bQ0Xq8FTej09YC/jqKZwtijpvARlRp6wxL5WTA6yPe2YWSJ5RJh7Nm79rK2qB0wr6iDQzH60XGq5V/7u8YQ==" + }, + "node_modules/invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, + "node_modules/ip-address": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", + "dev": true, + "dependencies": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", + "integrity": "sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true + }, + "node_modules/is-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dev": true, + "dependencies": { + "has-bigints": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", + "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", + "dev": true, + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.1.tgz", + "integrity": "sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==", + "dev": true, + "dependencies": { + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-in-browser": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/is-in-browser/-/is-in-browser-1.1.3.tgz", + "integrity": "sha512-FeXIBgG/CPGd/WUxuEyvgGTEfwiG9Z4EKGxjNMRqviiIIfsmgrpnHLffEDdwUHqNva1VEW91o3xBT/m8Elgl9g==" + }, + "node_modules/is-installed-globally": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.4.0.tgz", + "integrity": "sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==", + "dev": true, + "dependencies": { + "global-dirs": "^3.0.0", + "is-path-inside": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-lambda": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", + "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", + "dev": true + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-like": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/is-number-like/-/is-number-like-1.0.8.tgz", + "integrity": "sha512-6rZi3ezCyFcn5L71ywzz2bS5b2Igl1En3eTlZlvKjpz1n3IZLAYMbKYAIQgFmEu0GENg92ziU/faEOA/aixjbA==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "lodash.isfinite": "^3.3.2" + } + }, + "node_modules/is-number-object": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", + "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-obj": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", + "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz", + "integrity": "sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz", + "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==", + "dev": true, + "dependencies": { + "which-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "dev": true + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-weakref": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", + "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-what": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-3.14.1.tgz", + "integrity": "sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA==", + "dev": true + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true + }, + "node_modules/isbinaryfile": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", + "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==", + "dev": true, + "engines": { + "node": ">= 8.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "node_modules/isbot": { + "version": "5.1.21", + "resolved": "https://registry.npmjs.org/isbot/-/isbot-5.1.21.tgz", + "integrity": "sha512-0q3naRVpENL0ReKHeNcwn/G7BDynp0DqZUckKyFtM9+hmpnPqgm8+8wbjiVZ0XNhq1wPQV28/Pb8Snh5adeUHA==", + "engines": { + "node": ">=18" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isomorphic-unfetch": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/isomorphic-unfetch/-/isomorphic-unfetch-3.1.0.tgz", + "integrity": "sha512-geDJjpoZ8N0kWexiwkX8F9NkTsXhetLPVbZFQ+JTW239QNOwvB0gniuR1Wc6f0AMTn7/mFGyXvHTifrCp/GH8Q==", + "dependencies": { + "node-fetch": "^2.6.1", + "unfetch": "^4.2.0" + } + }, + "node_modules/isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", + "dev": true + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-3.0.6.tgz", + "integrity": "sha512-R47KzMtDJH6X4/YW9XTx+jrLnZnscW4VpNN+1PViSYTejLVPWv7oov+Duf8YQSPyVRUvueQqz1TcsC6mooZTXw==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^2.0.5", + "make-dir": "^2.1.0", + "rimraf": "^2.6.3", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/istanbul-lib-coverage": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz", + "integrity": "sha512-8aXznuEPCJvGnMSRft4udDRDtb1V3pkQkMMI5LI+6HuQz5oQ4J2UFn1H82raA3qJtyOLkkwVqICBQkjnGtn5mA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "dependencies": { + "pify": "^4.0.1", + "semver": "^5.6.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "dev": true, + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jackspeak": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.1.tgz", + "integrity": "sha512-U23pQPDnmYybVkYjObcuYMk43VRlMLLqLI+RdZy8s8WV8WsxO9SnqSroKaluuvcNOdCAlauKszDwd+umbot5Mg==", + "dev": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jake": { + "version": "10.9.1", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.1.tgz", + "integrity": "sha512-61btcOHNnLnsOdtLgA5efqQWjnSi/vow5HbI7HMdKKWqvrKR1bLK3BPlJn9gcSaP2ewuamUSMB5XEy76KUIS2w==", + "dependencies": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.4", + "minimatch": "^3.1.2" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jake/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jake/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/jake/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jake/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jake/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/jake/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/jake/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/jake/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jasmine": { + "version": "3.99.0", + "resolved": "https://registry.npmjs.org/jasmine/-/jasmine-3.99.0.tgz", + "integrity": "sha512-YIThBuHzaIIcjxeuLmPD40SjxkEcc8i//sGMDKCgkRMVgIwRJf5qyExtlJpQeh7pkeoBSOe6lQEdg+/9uKg9mw==", + "dev": true, + "dependencies": { + "glob": "^7.1.6", + "jasmine-core": "~3.99.0" + }, + "bin": { + "jasmine": "bin/jasmine.js" + } + }, + "node_modules/jasmine-core": { + "version": "3.99.1", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-3.99.1.tgz", + "integrity": "sha512-Hu1dmuoGcZ7AfyynN3LsfruwMbxMALMka+YtZeGoLuDEySVmVAPaonkNoBRIw/ectu8b9tVQCJNgp4a4knp+tg==", + "dev": true + }, + "node_modules/jasmine-marbles": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/jasmine-marbles/-/jasmine-marbles-0.9.2.tgz", + "integrity": "sha512-T7RjG4fRsdiGGzbQZ6Kj39qYt6O1/KIcR4FkUNsD3DUGkd/AzpwzN+xtk0DXlLWEz5BaVdK1SzMgQDVw879c4Q==", + "dev": true, + "dependencies": { + "lodash": "^4.17.20" + }, + "peerDependencies": { + "rxjs": "^7.0.0" + } + }, + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/jest-worker/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/jiti": { + "version": "1.21.6", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.6.tgz", + "integrity": "sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==", + "dev": true, + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/jquery": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz", + "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==", + "license": "MIT", + "peer": true + }, + "node_modules/js-cookie": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-2.2.1.tgz", + "integrity": "sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ==" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", + "dev": true + }, + "node_modules/jsdoc-type-pratt-parser": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-4.0.0.tgz", + "integrity": "sha512-YtOli5Cmzy3q4dP26GraSOeAhqecewG04hoO8DY56CH4KJ9Fvv5qKWUCCo3HZob7esJQHCv6/+bnTy72xZZaVQ==", + "dev": true, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, + "node_modules/json-parse-even-better-errors": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.2.tgz", + "integrity": "sha512-fi0NG4bPjCHunUJffmLd0gxssIgkNmArMvis4iNah6Owg1MCJjWhEcDLmsK6iGkJq3tHwbDkTlce70/tmXN4cQ==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "dev": true + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonc-eslint-parser": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/jsonc-eslint-parser/-/jsonc-eslint-parser-2.4.0.tgz", + "integrity": "sha512-WYDyuc/uFcGp6YtM2H0uKmUwieOuzeE/5YocFJLnLfclZ4inf3mRn8ZVy1s7Hxji7Jxm6Ss8gqpexD/GlKoGgg==", + "dev": true, + "dependencies": { + "acorn": "^8.5.0", + "eslint-visitor-keys": "^3.0.0", + "espree": "^9.0.0", + "semver": "^7.3.5" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ota-meshi" + } + }, + "node_modules/jsonc-parser": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.1.tgz", + "integrity": "sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==", + "dev": true + }, + "node_modules/jsonfile": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-3.0.1.tgz", + "integrity": "sha512-oBko6ZHlubVB5mRFkur5vgYR1UyqX+S6Y/oCfLhqNdcc2fYFlDpIoNc7AfKS1KOGcnNAkvsr0grLck9ANM815w==", + "dev": true, + "optional": true, + "peer": true, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonparse": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", + "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==", + "dev": true, + "engines": [ + "node >= 0.2.0" + ] + }, + "node_modules/jsonschema": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/jsonschema/-/jsonschema-1.5.0.tgz", + "integrity": "sha512-K+A9hhqbn0f3pJX17Q/7H6yQfD/5OXgdrR5UE12gMXCiN9D5Xq2o5mddV2QEcX/bjla99ASsAAQUyMCCRWAEhw==", + "engines": { + "node": "*" + } + }, + "node_modules/jsprim": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-2.0.2.tgz", + "integrity": "sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ==", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "dependencies": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.4.0", + "verror": "1.10.0" + } + }, + "node_modules/jss": { + "version": "10.10.0", + "resolved": "https://registry.npmjs.org/jss/-/jss-10.10.0.tgz", + "integrity": "sha512-cqsOTS7jqPsPMjtKYDUpdFC0AbhYFLTcuGRqymgmdJIeQ8cH7+AgX7YSgQy79wXloZq2VvATYxUOUQEvS1V/Zw==", + "dependencies": { + "@babel/runtime": "^7.3.1", + "csstype": "^3.0.2", + "is-in-browser": "^1.1.3", + "tiny-warning": "^1.0.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/jss" + } + }, + "node_modules/jss-plugin-camel-case": { + "version": "10.10.0", + "resolved": "https://registry.npmjs.org/jss-plugin-camel-case/-/jss-plugin-camel-case-10.10.0.tgz", + "integrity": "sha512-z+HETfj5IYgFxh1wJnUAU8jByI48ED+v0fuTuhKrPR+pRBYS2EDwbusU8aFOpCdYhtRc9zhN+PJ7iNE8pAWyPw==", + "dependencies": { + "@babel/runtime": "^7.3.1", + "hyphenate-style-name": "^1.0.3", + "jss": "10.10.0" + } + }, + "node_modules/jss-plugin-default-unit": { + "version": "10.10.0", + "resolved": "https://registry.npmjs.org/jss-plugin-default-unit/-/jss-plugin-default-unit-10.10.0.tgz", + "integrity": "sha512-SvpajxIECi4JDUbGLefvNckmI+c2VWmP43qnEy/0eiwzRUsafg5DVSIWSzZe4d2vFX1u9nRDP46WCFV/PXVBGQ==", + "dependencies": { + "@babel/runtime": "^7.3.1", + "jss": "10.10.0" + } + }, + "node_modules/jss-plugin-global": { + "version": "10.10.0", + "resolved": "https://registry.npmjs.org/jss-plugin-global/-/jss-plugin-global-10.10.0.tgz", + "integrity": "sha512-icXEYbMufiNuWfuazLeN+BNJO16Ge88OcXU5ZDC2vLqElmMybA31Wi7lZ3lf+vgufRocvPj8443irhYRgWxP+A==", + "dependencies": { + "@babel/runtime": "^7.3.1", + "jss": "10.10.0" + } + }, + "node_modules/jss-plugin-nested": { + "version": "10.10.0", + "resolved": "https://registry.npmjs.org/jss-plugin-nested/-/jss-plugin-nested-10.10.0.tgz", + "integrity": "sha512-9R4JHxxGgiZhurDo3q7LdIiDEgtA1bTGzAbhSPyIOWb7ZubrjQe8acwhEQ6OEKydzpl8XHMtTnEwHXCARLYqYA==", + "dependencies": { + "@babel/runtime": "^7.3.1", + "jss": "10.10.0", + "tiny-warning": "^1.0.2" + } + }, + "node_modules/jss-plugin-props-sort": { + "version": "10.10.0", + "resolved": "https://registry.npmjs.org/jss-plugin-props-sort/-/jss-plugin-props-sort-10.10.0.tgz", + "integrity": "sha512-5VNJvQJbnq/vRfje6uZLe/FyaOpzP/IH1LP+0fr88QamVrGJa0hpRRyAa0ea4U/3LcorJfBFVyC4yN2QC73lJg==", + "dependencies": { + "@babel/runtime": "^7.3.1", + "jss": "10.10.0" + } + }, + "node_modules/jss-plugin-rule-value-function": { + "version": "10.10.0", + "resolved": "https://registry.npmjs.org/jss-plugin-rule-value-function/-/jss-plugin-rule-value-function-10.10.0.tgz", + "integrity": "sha512-uEFJFgaCtkXeIPgki8ICw3Y7VMkL9GEan6SqmT9tqpwM+/t+hxfMUdU4wQ0MtOiMNWhwnckBV0IebrKcZM9C0g==", + "dependencies": { + "@babel/runtime": "^7.3.1", + "jss": "10.10.0", + "tiny-warning": "^1.0.2" + } + }, + "node_modules/jss-plugin-vendor-prefixer": { + "version": "10.10.0", + "resolved": "https://registry.npmjs.org/jss-plugin-vendor-prefixer/-/jss-plugin-vendor-prefixer-10.10.0.tgz", + "integrity": "sha512-UY/41WumgjW8r1qMCO8l1ARg7NHnfRVWRhZ2E2m0DMYsr2DD91qIXLyNhiX83hHswR7Wm4D+oDYNC1zWCJWtqg==", + "dependencies": { + "@babel/runtime": "^7.3.1", + "css-vendor": "^2.0.8", + "jss": "10.10.0" + } + }, + "node_modules/jss-rtl": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/jss-rtl/-/jss-rtl-0.3.0.tgz", + "integrity": "sha512-rg9jJmP1bAyhNOAp+BDZgOP/lMm4+oQ76qGueupDQ68Wq+G+6SGvCZvhIEg8OHSONRWOwFT6skCI+APGi8DgmA==", + "dependencies": { + "rtl-css-js": "^1.13.1" + }, + "peerDependencies": { + "jss": "^10.0.0" + } + }, + "node_modules/jss/node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" + }, + "node_modules/jwt-decode": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-3.1.2.tgz", + "integrity": "sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A==" + }, + "node_modules/karma": { + "version": "6.4.4", + "resolved": "https://registry.npmjs.org/karma/-/karma-6.4.4.tgz", + "integrity": "sha512-LrtUxbdvt1gOpo3gxG+VAJlJAEMhbWlM4YrFQgql98FwF7+K8K12LYO4hnDdUkNjeztYrOXEMqgTajSWgmtI/w==", + "dev": true, + "dependencies": { + "@colors/colors": "1.5.0", + "body-parser": "^1.19.0", + "braces": "^3.0.2", + "chokidar": "^3.5.1", + "connect": "^3.7.0", + "di": "^0.0.1", + "dom-serialize": "^2.2.1", + "glob": "^7.1.7", + "graceful-fs": "^4.2.6", + "http-proxy": "^1.18.1", + "isbinaryfile": "^4.0.8", + "lodash": "^4.17.21", + "log4js": "^6.4.1", + "mime": "^2.5.2", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.5", + "qjobs": "^1.2.0", + "range-parser": "^1.2.1", + "rimraf": "^3.0.2", + "socket.io": "^4.7.2", + "source-map": "^0.6.1", + "tmp": "^0.2.1", + "ua-parser-js": "^0.7.30", + "yargs": "^16.1.1" + }, + "bin": { + "karma": "bin/karma" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/karma-chrome-launcher": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/karma-chrome-launcher/-/karma-chrome-launcher-3.2.0.tgz", + "integrity": "sha512-rE9RkUPI7I9mAxByQWkGJFXfFD6lE4gC5nPuZdobf/QdTEJI6EU4yIay/cfU/xV4ZxlM5JiTv7zWYgA64NpS5Q==", + "dev": true, + "dependencies": { + "which": "^1.2.1" + } + }, + "node_modules/karma-chrome-launcher/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/karma-coverage-istanbul-reporter": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/karma-coverage-istanbul-reporter/-/karma-coverage-istanbul-reporter-3.0.3.tgz", + "integrity": "sha512-wE4VFhG/QZv2Y4CdAYWDbMmcAHeS926ZIji4z+FkB2aF/EposRb6DP6G5ncT/wXhqUfAb/d7kZrNKPonbvsATw==", + "dev": true, + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^3.0.6", + "istanbul-reports": "^3.0.2", + "minimatch": "^3.0.4" + }, + "funding": { + "url": "https://github.com/sponsors/mattlewis92" + } + }, + "node_modules/karma-coverage-istanbul-reporter/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/karma-coverage-istanbul-reporter/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/karma-jasmine": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/karma-jasmine/-/karma-jasmine-4.0.2.tgz", + "integrity": "sha512-ggi84RMNQffSDmWSyyt4zxzh2CQGwsxvYYsprgyR1j8ikzIduEdOlcLvXjZGwXG/0j41KUXOWsUCBfbEHPWP9g==", + "dev": true, + "dependencies": { + "jasmine-core": "^3.6.0" + }, + "engines": { + "node": ">= 10" + }, + "peerDependencies": { + "karma": "*" + } + }, + "node_modules/karma-jasmine-html-reporter": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/karma-jasmine-html-reporter/-/karma-jasmine-html-reporter-1.7.0.tgz", + "integrity": "sha512-pzum1TL7j90DTE86eFt48/s12hqwQuiD+e5aXx2Dc9wDEn2LfGq6RoAxEZZjFiN0RDSCOnosEKRZWxbQ+iMpQQ==", + "dev": true, + "peerDependencies": { + "jasmine-core": ">=3.8", + "karma": ">=0.9", + "karma-jasmine": ">=1.1" + } + }, + "node_modules/karma-mocha-reporter": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/karma-mocha-reporter/-/karma-mocha-reporter-2.2.5.tgz", + "integrity": "sha512-Hr6nhkIp0GIJJrvzY8JFeHpQZNseuIakGac4bpw8K1+5F0tLb6l7uvXRa8mt2Z+NVwYgCct4QAfp2R2QP6o00w==", + "dev": true, + "dependencies": { + "chalk": "^2.1.0", + "log-symbols": "^2.1.0", + "strip-ansi": "^4.0.0" + }, + "peerDependencies": { + "karma": ">=0.13" + } + }, + "node_modules/karma-mocha-reporter/node_modules/ansi-regex": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.1.tgz", + "integrity": "sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/karma-mocha-reporter/node_modules/log-symbols": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz", + "integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==", + "dev": true, + "dependencies": { + "chalk": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/karma-mocha-reporter/node_modules/strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow==", + "dev": true, + "dependencies": { + "ansi-regex": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/karma-source-map-support": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/karma-source-map-support/-/karma-source-map-support-1.4.0.tgz", + "integrity": "sha512-RsBECncGO17KAoJCYXjv+ckIz+Ii9NCi+9enk+rq6XC81ezYkb4/RHE6CTXdA7IOJqoF3wcaLfVG0CPmE5ca6A==", + "dev": true, + "dependencies": { + "source-map-support": "^0.5.5" + } + }, + "node_modules/karma/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/karma/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/karma/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/karma/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/karma/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/karma/node_modules/connect": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz", + "integrity": "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==", + "dev": true, + "dependencies": { + "debug": "2.6.9", + "finalhandler": "1.1.2", + "parseurl": "~1.3.3", + "utils-merge": "1.0.1" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/karma/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/karma/node_modules/finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "dev": true, + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/karma/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/karma/node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/karma/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/karma/node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "dev": true, + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/karma/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/karma/node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/karma/node_modules/ua-parser-js": { + "version": "0.7.38", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.38.tgz", + "integrity": "sha512-fYmIy7fKTSFAhG3fuPlubeGaMoAd6r0rSnfEsO5nEY55i26KSLt9EH7PLQiiqPUhNqYIJvSkTy1oArIcXAbPbA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + }, + { + "type": "github", + "url": "https://github.com/sponsors/faisalman" + } + ], + "engines": { + "node": "*" + } + }, + "node_modules/karma/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/karma/node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/karma/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/klona": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.6.tgz", + "integrity": "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/launch-editor": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.8.0.tgz", + "integrity": "sha512-vJranOAJrI/llyWGRQqiDM+adrw+k83fvmmx3+nV47g3+36xM15jE+zyZ6Ffel02+xSvuM0b2GDRosXZkbb6wA==", + "dev": true, + "dependencies": { + "picocolors": "^1.0.0", + "shell-quote": "^1.8.1" + } + }, + "node_modules/lazy-ass": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/lazy-ass/-/lazy-ass-1.6.0.tgz", + "integrity": "sha512-cc8oEVoctTvsFZ/Oje/kGnHbpWHYBe8IAJe4C0QNc3t8uM/0Y8+erSz/7Y1ALuXTEZTMvxXwO6YbX1ey3ujiZw==", + "dev": true, + "engines": { + "node": "> 0.8" + } + }, + "node_modules/less": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/less/-/less-4.2.0.tgz", + "integrity": "sha512-P3b3HJDBtSzsXUl0im2L7gTO5Ubg8mEN6G8qoTS77iXxXX4Hvu4Qj540PZDvQ8V6DmX6iXo98k7Md0Cm1PrLaA==", + "dev": true, + "dependencies": { + "copy-anything": "^2.0.1", + "parse-node-version": "^1.0.1", + "tslib": "^2.3.0" + }, + "bin": { + "lessc": "bin/lessc" + }, + "engines": { + "node": ">=6" + }, + "optionalDependencies": { + "errno": "^0.1.1", + "graceful-fs": "^4.1.2", + "image-size": "~0.5.0", + "make-dir": "^2.1.0", + "mime": "^1.4.1", + "needle": "^3.1.0", + "source-map": "~0.6.0" + } + }, + "node_modules/less-loader": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/less-loader/-/less-loader-11.1.0.tgz", + "integrity": "sha512-C+uDBV7kS7W5fJlUjq5mPBeBVhYpTIm5gB09APT9o3n/ILeaXVsiSFTbZpTJCJwQ/Crczfn3DmfQFwxYusWFug==", + "dev": true, + "dependencies": { + "klona": "^2.0.4" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "less": "^3.5.0 || ^4.0.0", + "webpack": "^5.0.0" + } + }, + "node_modules/less/node_modules/make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "optional": true, + "dependencies": { + "pify": "^4.0.1", + "semver": "^5.6.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/less/node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "optional": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/less/node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true, + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/less/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "optional": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/less/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/license-webpack-plugin": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/license-webpack-plugin/-/license-webpack-plugin-4.0.2.tgz", + "integrity": "sha512-771TFWFD70G1wLTC4oU2Cw4qvtmNrIw+wRvBtn+okgHl7slJVi7zfNcdmqDL72BojM30VNJ2UHylr1o77U37Jw==", + "dev": true, + "dependencies": { + "webpack-sources": "^3.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + }, + "webpack-sources": { + "optional": true + } + } + }, + "node_modules/license-webpack-plugin/node_modules/webpack-sources": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", + "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "dev": true, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/limiter": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz", + "integrity": "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==", + "dev": true, + "optional": true, + "peer": true + }, + "node_modules/linkify-it": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-4.0.1.tgz", + "integrity": "sha512-C7bfi1UZmoj8+PQx22XyeXCuBlokoyWQL5pWSP+EI6nzRylyThouddufc2c1NDIcP9k5agmN9fLpA7VNJfIiqw==", + "dependencies": { + "uc.micro": "^1.0.1" + } + }, + "node_modules/listr2": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-3.14.0.tgz", + "integrity": "sha512-TyWI8G99GX9GjE54cJ+RrNMcIFBfwMPxc3XTFiAYGN4s10hWROGtOg7+O6u6LE3mNkyld7RSLE6nrKBvTfcs3g==", + "dev": true, + "dependencies": { + "cli-truncate": "^2.1.0", + "colorette": "^2.0.16", + "log-update": "^4.0.0", + "p-map": "^4.0.0", + "rfdc": "^1.3.0", + "rxjs": "^7.5.1", + "through": "^2.3.8", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "enquirer": ">= 2.3.0 < 3" + }, + "peerDependenciesMeta": { + "enquirer": { + "optional": true + } + } + }, + "node_modules/listr2/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/listr2/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/listr2/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/listr2/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/loader-runner": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", + "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "dev": true, + "engines": { + "node": ">=6.11.5" + } + }, + "node_modules/loader-utils": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.2.1.tgz", + "integrity": "sha512-ZvFw1KWS3GVyYBYb7qkmRM/WwL2TQQBxgCK62rlvm4WpVQ23Nb4tYjApUlfjrEGvOs7KHEsmyUn75OHZrJMWPw==", + "dev": true, + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "dev": true + }, + "node_modules/lodash.isfinite": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/lodash.isfinite/-/lodash.isfinite-3.3.2.tgz", + "integrity": "sha512-7FGG40uhC8Mm633uKW1r58aElFlBlxCrg9JfSi3P6aYiWmfiWF0PgMd86ZUsxE5GwWPdHoS2+48bwTh2VPkIQA==", + "dev": true, + "optional": true, + "peer": true + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "dev": true + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-symbols/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/log-symbols/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/log-symbols/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/log-symbols/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/log-symbols/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/log-symbols/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/log-update": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-4.0.0.tgz", + "integrity": "sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==", + "dev": true, + "dependencies": { + "ansi-escapes": "^4.3.0", + "cli-cursor": "^3.1.0", + "slice-ansi": "^4.0.0", + "wrap-ansi": "^6.2.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/log-update/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/log-update/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/log-update/node_modules/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/log4js": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/log4js/-/log4js-6.9.1.tgz", + "integrity": "sha512-1somDdy9sChrr9/f4UlzhdaGfDR2c/SaD2a4T7qEkG4jTS57/B3qmnjLYePwQ8cqWnUHZI0iAKxMBpCZICiZ2g==", + "dev": true, + "dependencies": { + "date-format": "^4.0.14", + "debug": "^4.3.4", + "flatted": "^3.2.7", + "rfdc": "^1.3.0", + "streamroller": "^3.1.5" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/magic-string": { + "version": "0.30.8", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.8.tgz", + "integrity": "sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, + "node_modules/make-fetch-happen": { + "version": "13.0.1", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-13.0.1.tgz", + "integrity": "sha512-cKTUFc/rbKUd/9meOvgrpJ2WrNzymt6jfRDdwg5UCnVzv9dTpEj9JS5m3wtziXVCjluIXyL8pcaukYqezIzZQA==", + "dev": true, + "dependencies": { + "@npmcli/agent": "^2.0.0", + "cacache": "^18.0.0", + "http-cache-semantics": "^4.1.1", + "is-lambda": "^1.0.1", + "minipass": "^7.0.2", + "minipass-fetch": "^3.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.3", + "proc-log": "^4.2.0", + "promise-retry": "^2.0.1", + "ssri": "^10.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/make-fetch-happen/node_modules/@npmcli/fs": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-3.1.1.tgz", + "integrity": "sha512-q9CRWjpHCMIh5sVyefoD1cA7PkvILqCZsnSOEUUivORLjxCO/Irmue2DprETiNgEqktDBZaM1Bi+jrarx1XdCg==", + "dev": true, + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/make-fetch-happen/node_modules/cacache": { + "version": "18.0.3", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-18.0.3.tgz", + "integrity": "sha512-qXCd4rh6I07cnDqh8V48/94Tc/WSfj+o3Gn6NZ0aZovS255bUx8O13uKxRFd2eWG0xgsco7+YItQNPaa5E85hg==", + "dev": true, + "dependencies": { + "@npmcli/fs": "^3.1.0", + "fs-minipass": "^3.0.0", + "glob": "^10.2.2", + "lru-cache": "^10.0.1", + "minipass": "^7.0.3", + "minipass-collect": "^2.0.1", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "p-map": "^4.0.0", + "ssri": "^10.0.0", + "tar": "^6.1.11", + "unique-filename": "^3.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/make-fetch-happen/node_modules/fs-minipass": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", + "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", + "dev": true, + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/make-fetch-happen/node_modules/glob": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.3.tgz", + "integrity": "sha512-Q38SGlYRpVtDBPSWEylRyctn7uDeTp4NQERTLiCT1FqA9JXPYWqAVmQU6qh4r/zMM5ehxTcbaO8EjhWnvEhmyg==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/make-fetch-happen/node_modules/lru-cache": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.0.tgz", + "integrity": "sha512-bfJaPTuEiTYBu+ulDaeQ0F+uLmlfFkMgXj4cbwfuMSjgObGMzb55FMMbDvbRU0fAHZ4sLGkz2mKwcMg8Dvm8Ww==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/make-fetch-happen/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/make-fetch-happen/node_modules/minipass-collect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz", + "integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==", + "dev": true, + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/make-fetch-happen/node_modules/proc-log": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-4.2.0.tgz", + "integrity": "sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/make-fetch-happen/node_modules/ssri": { + "version": "10.0.6", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-10.0.6.tgz", + "integrity": "sha512-MGrFH9Z4NP9Iyhqn16sDtBpRRNJ0Y2hNa6D65h736fVSaPCHr4DM4sWUNvVaSuC+0OBGhwsrydQwmgfg5LncqQ==", + "dev": true, + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/make-fetch-happen/node_modules/unique-filename": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-3.0.0.tgz", + "integrity": "sha512-afXhuC55wkAmZ0P18QsVE6kp8JaxrEokN2HGIoIVv2ijHQd419H0+6EigAFcIzXeMIkcIkNBpB3L/DXB3cTS/g==", + "dev": true, + "dependencies": { + "unique-slug": "^4.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/make-fetch-happen/node_modules/unique-slug": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-4.0.0.tgz", + "integrity": "sha512-WrcA6AyEfqDX5bWige/4NQfPZMtASNVxdmWR76WESYQVAACSgWcR6e9i0mofqqBxYFtL4oAxPIptY73/0YE1DQ==", + "dev": true, + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/manifesto.js": { + "version": "4.2.17", + "resolved": "https://registry.npmjs.org/manifesto.js/-/manifesto.js-4.2.17.tgz", + "integrity": "sha512-UjctsJ2PkgwGDUQ/ZzvyObXJO/yiFYwiz49xrzkayi9fhrwUVC3Vc0aQyGm723BZTl5nKYJQ8YdEhJRp08xOtA==", + "dependencies": { + "@edsilv/http-status-codes": "^1.0.3", + "@iiif/vocabulary": "^1.0.26", + "isomorphic-unfetch": "^3.0.0", + "lodash": "^4.17.21" + }, + "engines": { + "node": ">=8.9.1", + "npm": ">=3.10.8" + } + }, + "node_modules/markdown-it": { + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-13.0.2.tgz", + "integrity": "sha512-FtwnEuuK+2yVU7goGn/MJ0WBZMM9ZPgU9spqlFs7/A/pDIUNSOQZhUgOqYCficIuR2QaFnrt8LHqBWsbTAoI5w==", + "dependencies": { + "argparse": "^2.0.1", + "entities": "~3.0.1", + "linkify-it": "^4.0.1", + "mdurl": "^1.0.1", + "uc.micro": "^1.0.5" + }, + "bin": { + "markdown-it": "bin/markdown-it.js" + } + }, + "node_modules/markdown-it/node_modules/entities": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-3.0.1.tgz", + "integrity": "sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/md5": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", + "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==", + "dependencies": { + "charenc": "0.0.2", + "crypt": "0.0.2", + "is-buffer": "~1.1.6" + } + }, + "node_modules/mdurl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", + "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==" + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/memfs": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.5.3.tgz", + "integrity": "sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==", + "dev": true, + "dependencies": { + "fs-monkey": "^1.0.4" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/memoize-one": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", + "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==" + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/mini-css-extract-plugin": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.8.1.tgz", + "integrity": "sha512-/1HDlyFRxWIZPI1ZpgqlZ8jMw/1Dp/dl3P0L1jtZ+zVcHqwPhGwaJwKL00WVgfnBy6PWCde9W65or7IIETImuA==", + "dev": true, + "dependencies": { + "schema-utils": "^4.0.0", + "tapable": "^2.2.1" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "dev": true + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-collect": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", + "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-fetch": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-3.0.5.tgz", + "integrity": "sha512-2N8elDQAtSnFV0Dk7gt15KHsS0Fyz6CbYZ360h0WTYV1Ty46li3rAXVOQj1THMNLdmrD9Vt5pBPtWtVkpwGBqg==", + "dev": true, + "dependencies": { + "minipass": "^7.0.3", + "minipass-sized": "^1.0.3", + "minizlib": "^2.1.2" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + }, + "optionalDependencies": { + "encoding": "^0.1.13" + } + }, + "node_modules/minipass-fetch/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-json-stream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minipass-json-stream/-/minipass-json-stream-1.0.1.tgz", + "integrity": "sha512-ODqY18UZt/I8k+b7rl2AENgbWE8IDYam+undIJONvigAz8KR5GWblsFTEfQs0WODsjbSXWlm+JHEv8Gr6Tfdbg==", + "dev": true, + "dependencies": { + "jsonparse": "^1.3.1", + "minipass": "^3.0.0" + } + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/mirador": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/mirador/-/mirador-3.4.3.tgz", + "integrity": "sha512-yHoug0MHy4e9apykbbBhK+4CmbZS94zMxmugw2E2VX6iB0b2PKKY0JfYr/QfXh9P29YnWAbymaXJVpgbHVpTVw==", + "dependencies": { + "@material-ui/core": "^4.12.3", + "@material-ui/icons": "^4.9.1", + "@material-ui/lab": "^4.0.0-alpha.53", + "@researchgate/react-intersection-observer": "^1.0.0", + "classnames": "^2.2.6", + "clsx": "^1.0.4", + "deepmerge": "^4.2.2", + "dompurify": "^2.0.11", + "i18next": "^19.5.0", + "icomcom-react": "^1.0.1", + "intersection-observer": "^0.10.0", + "isomorphic-unfetch": "^3.0.0", + "jss": "^10.3.0", + "jss-rtl": "^0.3.0", + "lodash": "^4.17.11", + "manifesto.js": "^4.2.0", + "normalize-url": "^4.5.0", + "openseadragon": "^2.4.2 || ^3.0.0 || 4.0.x || ^4.1.1 || ^5.0.0", + "prop-types": "^15.6.2", + "re-reselect": "^4.0.0", + "react-aria-live": "^2.0.5", + "react-beautiful-dnd": "^13.0.0", + "react-copy-to-clipboard": "^5.0.1", + "react-dnd": "^10.0.2", + "react-dnd-html5-backend": "^10.0.2", + "react-dnd-multi-backend": "^5.0.0", + "react-dnd-touch-backend": "^10.0.2", + "react-full-screen": "^0.2.4", + "react-i18next": "^11.7.0", + "react-image": "^4.0.1", + "react-mosaic-component": "^4.0.1", + "react-redux": "^7.1.0", + "react-resize-observer": "^1.1.1", + "react-rnd": "^10.1", + "react-sizeme": "^2.6.7", + "react-virtualized-auto-sizer": "^1.0.2", + "react-window": "^1.8.5", + "redux": "^4.0.5", + "redux-devtools-extension": "^2.13.2", + "redux-saga": "^1.1.3", + "redux-thunk": "^2.3.0", + "reselect": "^4.0.0", + "uuid": "^8.1.0" + }, + "peerDependencies": { + "react": "^16.8.3", + "react-dom": "^16.8.3" + } + }, + "node_modules/mirador-dl-plugin": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/mirador-dl-plugin/-/mirador-dl-plugin-0.13.0.tgz", + "integrity": "sha512-I/6etIvpTtO1zgjxx2uEUFoyB9NxQ43JWg8CMkKmZqblW7AAeFqRn1/zUlQH7N8KFZft9Rah6D8qxtuNAo9jmA==", + "peerDependencies": { + "@material-ui/core": "^4.11.0", + "@material-ui/icons": "^4.9.1", + "lodash": "^4.17.11", + "mirador": "^3.0.0-rc.7", + "prop-types": "^15.7.2", + "react": "16.x", + "react-dom": "16.x" + } + }, + "node_modules/mirador-share-plugin": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/mirador-share-plugin/-/mirador-share-plugin-0.16.0.tgz", + "integrity": "sha512-Rlywq/06nXf4/BUc2LTBD6jf+Q5VO97NK+MTd4VPMfY50Fb8NMr6L0edZXZETcTML1qh11GDEVrHx/csxMSlMA==", + "dependencies": { + "notistack": "^3.0.1" + }, + "peerDependencies": { + "@material-ui/core": "^4.7.2", + "@material-ui/icons": "^4.5.1", + "mirador": "^3.0.0-beta.0", + "prop-types": "^15.7.2", + "react": "16.x", + "react-copy-to-clipboard": "^5.0.1", + "react-dom": "16.x" + } + }, + "node_modules/mitt": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-1.2.0.tgz", + "integrity": "sha512-r6lj77KlwqLhIUku9UWYes7KJtsczvolZkzp8hbaDPPaE24OmWl5s539Mytlj22siEQKosZ26qCBgda2PKwoJw==", + "dev": true, + "optional": true, + "peer": true + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/morgan": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz", + "integrity": "sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==", + "dependencies": { + "basic-auth": "~2.0.1", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-finished": "~2.3.0", + "on-headers": "~1.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/morgan/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/morgan/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/morgan/node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/mrmime": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz", + "integrity": "sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/multicast-dns": { + "version": "7.2.5", + "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz", + "integrity": "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==", + "dev": true, + "dependencies": { + "dns-packet": "^5.2.2", + "thunky": "^1.0.2" + }, + "bin": { + "multicast-dns": "cli.js" + } + }, + "node_modules/mute-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz", + "integrity": "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", + "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/needle": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/needle/-/needle-3.3.1.tgz", + "integrity": "sha512-6k0YULvhpw+RoLNiQCRKOl09Rv1dPLr8hHnVjHqdolKwDrdNyk+Hmrthi4lIGPPz3r39dLx0hsF5s40sZ3Us4Q==", + "dev": true, + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.3", + "sax": "^1.2.4" + }, + "bin": { + "needle": "bin/needle" + }, + "engines": { + "node": ">= 4.4.x" + } + }, + "node_modules/needle/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true + }, + "node_modules/ng-mocks": { + "version": "14.13.2", + "resolved": "https://registry.npmjs.org/ng-mocks/-/ng-mocks-14.13.2.tgz", + "integrity": "sha512-ItAB72Pc0uznL1j4TPsFp1wehhitVp7DARkc67aafeIk1FDgwnAZvzJwntMnIp/IWMSbzrEQ6kl3cc5euX1NRA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/help-me-mom" + }, + "peerDependencies": { + "@angular/common": "5.0.0-alpha - 5 || 6.0.0-alpha - 6 || 7.0.0-alpha - 7 || 8.0.0-alpha - 8 || 9.0.0-alpha - 9 || 10.0.0-alpha - 10 || 11.0.0-alpha - 11 || 12.0.0-alpha - 12 || 13.0.0-alpha - 13 || 14.0.0-alpha - 14 || 15.0.0-alpha - 15 || 16.0.0-alpha - 16 || 17.0.0-alpha - 17 || 18.0.0-alpha - 18 || 19.0.0-alpha - 19", + "@angular/core": "5.0.0-alpha - 5 || 6.0.0-alpha - 6 || 7.0.0-alpha - 7 || 8.0.0-alpha - 8 || 9.0.0-alpha - 9 || 10.0.0-alpha - 10 || 11.0.0-alpha - 11 || 12.0.0-alpha - 12 || 13.0.0-alpha - 13 || 14.0.0-alpha - 14 || 15.0.0-alpha - 15 || 16.0.0-alpha - 16 || 17.0.0-alpha - 17 || 18.0.0-alpha - 18 || 19.0.0-alpha - 19", + "@angular/forms": "5.0.0-alpha - 5 || 6.0.0-alpha - 6 || 7.0.0-alpha - 7 || 8.0.0-alpha - 8 || 9.0.0-alpha - 9 || 10.0.0-alpha - 10 || 11.0.0-alpha - 11 || 12.0.0-alpha - 12 || 13.0.0-alpha - 13 || 14.0.0-alpha - 14 || 15.0.0-alpha - 15 || 16.0.0-alpha - 16 || 17.0.0-alpha - 17 || 18.0.0-alpha - 18 || 19.0.0-alpha - 19", + "@angular/platform-browser": "5.0.0-alpha - 5 || 6.0.0-alpha - 6 || 7.0.0-alpha - 7 || 8.0.0-alpha - 8 || 9.0.0-alpha - 9 || 10.0.0-alpha - 10 || 11.0.0-alpha - 11 || 12.0.0-alpha - 12 || 13.0.0-alpha - 13 || 14.0.0-alpha - 14 || 15.0.0-alpha - 15 || 16.0.0-alpha - 16 || 17.0.0-alpha - 17 || 18.0.0-alpha - 18 || 19.0.0-alpha - 19" + } + }, + "node_modules/ng2-file-upload": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ng2-file-upload/-/ng2-file-upload-5.0.0.tgz", + "integrity": "sha512-M2JaX0unB/30GmQUScfXko2vseaPlU1R5jnhL5cfvOLNw8BBSVJAdOSmcavJzicgTZWCYz7y7Fvpr939bd4eCA==", + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/common": "^16.0.0", + "@angular/core": "^16.0.0" + } + }, + "node_modules/ng2-nouislider": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ng2-nouislider/-/ng2-nouislider-2.0.0.tgz", + "integrity": "sha512-NGbF/0w0+bZqclpSPFOlWIeVJaVwRRYFJzD1x8PClbw9GIeo7fCHoBzZ81y7K7FTJg6to+cgjSTFETPZG/Dizg==", + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/common": ">=14.x", + "@angular/core": ">=14.x", + "nouislider": ">=15.x" + } + }, + "node_modules/ngx-infinite-scroll": { + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/ngx-infinite-scroll/-/ngx-infinite-scroll-16.0.0.tgz", + "integrity": "sha512-bzyNYd+wVlUUxcopRVr2DAa81eEc8vITtKVvb+c7R1uy8hWPTlxOEXf3L1qA4FMwTEzCQ9b37TXzlJji3qBy+A==", + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/common": ">=16.0.0 <17.0.0", + "@angular/core": ">=16.0.0 <17.0.0" + } + }, + "node_modules/ngx-mask": { + "version": "14.2.4", + "resolved": "https://registry.npmjs.org/ngx-mask/-/ngx-mask-14.2.4.tgz", + "integrity": "sha512-158nAe2tyiZa2T8COoI6SvJCQHqpJ4+JW0amGcvVEYUBF6FIoYK66BlnOJURAOy5qry0d0N45w7J/LGsCBgZcg==", + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/common": ">=10.0.0", + "@angular/core": ">=10.0.0", + "@angular/forms": ">=10.0.0" + } + }, + "node_modules/ngx-pagination": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/ngx-pagination/-/ngx-pagination-6.0.3.tgz", + "integrity": "sha512-lONjTQ7hFPh1SyhwDrRd5ZwM4NMGQ7bNR6vLrs6mrU0Z8Q1zCcWbf/pvyp4DOlGyd9uyZxRy2wUsSZLeIPjbAw==", + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/common": ">=13.0.0", + "@angular/core": ">=13.0.0" + } + }, + "node_modules/ngx-skeleton-loader": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/ngx-skeleton-loader/-/ngx-skeleton-loader-9.0.0.tgz", + "integrity": "sha512-aO4/V6oGdZGNcTjasTg/fwzJJYl/ZmNKgCukOEQdUK3GSFOZtB/3GGULMJuZ939hk3Hzqh1OBiLfIM1SqTfhqg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@angular/common": ">=16.0.0", + "@angular/core": ">=16.0.0" + } + }, + "node_modules/ngx-ui-switch": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/ngx-ui-switch/-/ngx-ui-switch-14.1.0.tgz", + "integrity": "sha512-uGGLppBP1FXjyD+x7f8Pm6I3JQTMmsBqPtwERAX27jSZxDWFp/sewnl75fuDvu75qFofJd/BIOtV4xHxzgZOvw==", + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/animations": ">=10.0.0", + "@angular/common": ">=10.0.0", + "@angular/core": ">=10.0.0", + "@angular/forms": ">=10.0.0", + "@angular/platform-browser": ">=10.0.0" + } + }, + "node_modules/nice-napi": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/nice-napi/-/nice-napi-1.0.2.tgz", + "integrity": "sha512-px/KnJAJZf5RuBGcfD+Sp2pAKq0ytz8j+1NehvgIGFkvtvFrDM3T8E4x/JJODXK9WZow8RRGrbA9QQ3hs+pDhA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "!win32" + ], + "dependencies": { + "node-addon-api": "^3.0.0", + "node-gyp-build": "^4.2.2" + } + }, + "node_modules/node-addon-api": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz", + "integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==", + "dev": true, + "optional": true + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-forge": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "dev": true, + "engines": { + "node": ">= 6.13.0" + } + }, + "node_modules/node-gyp": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-10.1.0.tgz", + "integrity": "sha512-B4J5M1cABxPc5PwfjhbV5hoy2DP9p8lFXASnEN6hugXOa61416tnTZ29x9sSwAd0o99XNIcpvDDy1swAExsVKA==", + "dev": true, + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "glob": "^10.3.10", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^13.0.0", + "nopt": "^7.0.0", + "proc-log": "^3.0.0", + "semver": "^7.3.5", + "tar": "^6.1.2", + "which": "^4.0.0" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/node-gyp-build": { + "version": "4.8.2", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.2.tgz", + "integrity": "sha512-IRUxE4BVsHWXkV/SFOut4qTlagw2aM8T5/vnTsmrHJvVoKueJHRc/JaFND7QDDc61kLYUJ6qlZM3sqTSyx2dTw==", + "dev": true, + "optional": true, + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/node-gyp/node_modules/glob": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.3.tgz", + "integrity": "sha512-Q38SGlYRpVtDBPSWEylRyctn7uDeTp4NQERTLiCT1FqA9JXPYWqAVmQU6qh4r/zMM5ehxTcbaO8EjhWnvEhmyg==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/node-gyp/node_modules/isexe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", + "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "dev": true, + "engines": { + "node": ">=16" + } + }, + "node_modules/node-gyp/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/node-gyp/node_modules/which": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", + "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "dev": true, + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^16.13.0 || >=18.0.0" + } + }, + "node_modules/node-releases": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", + "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==" + }, + "node_modules/nodemon": { + "version": "2.0.22", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.22.tgz", + "integrity": "sha512-B8YqaKMmyuCO7BowF1Z1/mkPqLk6cs/l63Ojtd6otKjMx47Dq1utxfRxcavH1I7VSaL8n5BUaoutadnsX3AAVQ==", + "dev": true, + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^3.2.7", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^5.7.1", + "simple-update-notifier": "^1.0.7", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=8.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/nodemon/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/nodemon/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/nodemon/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/nopt": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.1.tgz", + "integrity": "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==", + "dev": true, + "dependencies": { + "abbrev": "^2.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/normalize-package-data": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-6.0.2.tgz", + "integrity": "sha512-V6gygoYb/5EmNI+MEGrWkC+e6+Rr7mTmfHrxDbLzxQogBkgzo76rkok0Am6thgSF7Mv2nLOajAJj5vDJZEFn7g==", + "dev": true, + "dependencies": { + "hosted-git-info": "^7.0.0", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-url": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.1.tgz", + "integrity": "sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA==", + "engines": { + "node": ">=8" + } + }, + "node_modules/notistack": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/notistack/-/notistack-3.0.1.tgz", + "integrity": "sha512-ntVZXXgSQH5WYfyU+3HfcXuKaapzAJ8fBLQ/G618rn3yvSzEbnOB8ZSOwhX+dAORy/lw+GC2N061JA0+gYWTVA==", + "dependencies": { + "clsx": "^1.1.0", + "goober": "^2.0.33" + }, + "engines": { + "node": ">=12.0.0", + "npm": ">=6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/notistack" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/notistack/node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "peer": true + }, + "node_modules/notistack/node_modules/goober": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.16.tgz", + "integrity": "sha512-erjk19y1U33+XAMe1VTvIONHYoSqE4iS7BYUZfHaqeohLmnC0FdxEh7rQU+6MZ4OajItzjZFSRtVANrQwNq6/g==", + "peerDependencies": { + "csstype": "^3.0.10" + } + }, + "node_modules/nouislider": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/nouislider/-/nouislider-15.8.1.tgz", + "integrity": "sha512-93TweAi8kqntHJSPiSWQ1o/uZ29VWOmal9YKb6KKGGlCkugaNfAupT7o1qTHqdJvNQ7S0su5rO6qRFCjP8fxtw==" + }, + "node_modules/npm-bundled": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-3.0.1.tgz", + "integrity": "sha512-+AvaheE/ww1JEwRHOrn4WHNzOxGtVp+adrg2AeZS/7KuxGUYFuBta98wYpfHBbJp6Tg6j1NKSEVHNcfZzJHQwQ==", + "dev": true, + "dependencies": { + "npm-normalize-package-bin": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-install-checks": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-6.3.0.tgz", + "integrity": "sha512-W29RiK/xtpCGqn6f3ixfRYGk+zRyr+Ew9F2E20BfXxT5/euLdA/Nm7fO7OeTGuAmTs30cpgInyJ0cYe708YTZw==", + "dev": true, + "dependencies": { + "semver": "^7.1.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-normalize-package-bin": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-3.0.1.tgz", + "integrity": "sha512-dMxCf+zZ+3zeQZXKxmyuCKlIDPGuv8EF940xbkC4kQVDTtqoh6rJFO+JTKSA6/Rwi0getWmtuy4Itup0AMcaDQ==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-package-arg": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-11.0.1.tgz", + "integrity": "sha512-M7s1BD4NxdAvBKUPqqRW957Xwcl/4Zvo8Aj+ANrzvIPzGJZElrH7Z//rSaec2ORcND6FHHLnZeY8qgTpXDMFQQ==", + "dev": true, + "dependencies": { + "hosted-git-info": "^7.0.0", + "proc-log": "^3.0.0", + "semver": "^7.3.5", + "validate-npm-package-name": "^5.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm-packlist": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-8.0.2.tgz", + "integrity": "sha512-shYrPFIS/JLP4oQmAwDyk5HcyysKW8/JLTEA32S0Z5TzvpaeeX2yMFfoK1fjEBnCBvVyIB/Jj/GBFdm0wsgzbA==", + "dev": true, + "dependencies": { + "ignore-walk": "^6.0.4" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-pick-manifest": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-9.0.0.tgz", + "integrity": "sha512-VfvRSs/b6n9ol4Qb+bDwNGUXutpy76x6MARw/XssevE0TnctIKcmklJZM5Z7nqs5z5aW+0S63pgCNbpkUNNXBg==", + "dev": true, + "dependencies": { + "npm-install-checks": "^6.0.0", + "npm-normalize-package-bin": "^3.0.0", + "npm-package-arg": "^11.0.0", + "semver": "^7.3.5" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm-registry-fetch": { + "version": "16.2.1", + "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-16.2.1.tgz", + "integrity": "sha512-8l+7jxhim55S85fjiDGJ1rZXBWGtRLi1OSb4Z3BPLObPuIaeKRlPRiYMSHU4/81ck3t71Z+UwDDl47gcpmfQQA==", + "dev": true, + "dependencies": { + "@npmcli/redact": "^1.1.0", + "make-fetch-happen": "^13.0.0", + "minipass": "^7.0.2", + "minipass-fetch": "^3.0.0", + "minipass-json-stream": "^1.0.1", + "minizlib": "^2.1.2", + "npm-package-arg": "^11.0.0", + "proc-log": "^4.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm-registry-fetch/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/npm-registry-fetch/node_modules/proc-log": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-4.2.0.tgz", + "integrity": "sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", + "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", + "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.groupby": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.values": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.0.tgz", + "integrity": "sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/obuf": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", + "dev": true + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "dev": true, + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/openseadragon": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/openseadragon/-/openseadragon-2.4.2.tgz", + "integrity": "sha512-398KbZwRtOYA6OmeWRY4Q0737NTacQ9Q6whmr9Lp1MNQO3p0eBz5LIASRne+4gwequcSM1vcHcjfy3dIndQziw==" + }, + "node_modules/opn": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/opn/-/opn-5.3.0.tgz", + "integrity": "sha512-bYJHo/LOmoTd+pfiYhfZDnf9zekVJrY+cnS2a5F2x+w5ppvTqObojTP7WiFG+kVZs9Inw+qQ/lw7TroWwhdd2g==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "is-wsl": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/opn/node_modules/is-wsl": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", + "integrity": "sha512-gfygJYZ2gLTDlmbWMI0CE2MwnFzSN/2SZfkMlItC4K/JBlsWVDB0bO6XhqcY13YXE7iMcAJnzTCJjPiTeJJ0Mw==", + "dev": true, + "optional": true, + "peer": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dev": true, + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ora/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/ora/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/ora/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/ora/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ora/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/orejime": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/orejime/-/orejime-2.3.1.tgz", + "integrity": "sha512-LpJCuOG7mvR5fwD3wr+FoHPG+DX2t7+dJ4gBZnWICnNHtOTNuBzne0T+JcK0nW30JjiYULq1vfOaSLU2qmHCPg==", + "dependencies": { + "@babel/runtime": "^7.1.2", + "react-modal": "^3.13.1" + }, + "peerDependencies": { + "react": "^0.14.0 || ^15.0.0 || ^16", + "react-dom": "^0.14.0 || ^15.0.0 || ^16" + } + }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ospath": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/ospath/-/ospath-1.2.2.tgz", + "integrity": "sha512-o6E5qJV5zkAbIDNhGSIlyOhScKXgQrSRMilfph0clDfM0nEnBOlKlH4sWDmG95BW/CvwNz0vmm7dJVtU2KlMiA==", + "dev": true + }, + "node_modules/p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", + "engines": { + "node": ">=4" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-locate/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "dev": true, + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-retry": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", + "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", + "dev": true, + "dependencies": { + "@types/retry": "0.12.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-retry/node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/p-timeout": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz", + "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==", + "dependencies": { + "p-finally": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/p-wait-for": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/p-wait-for/-/p-wait-for-3.2.0.tgz", + "integrity": "sha512-wpgERjNkLrBiFmkMEjuZJEWKKDrNfHCKA1OhyN1wg1FrLkULbviEy6py1AyJUgZ72YWFbZ38FIpnqvVqAlDUwA==", + "dependencies": { + "p-timeout": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", + "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==", + "dev": true + }, + "node_modules/pacote": { + "version": "17.0.6", + "resolved": "https://registry.npmjs.org/pacote/-/pacote-17.0.6.tgz", + "integrity": "sha512-cJKrW21VRE8vVTRskJo78c/RCvwJCn1f4qgfxL4w77SOWrTCRcmfkYHlHtS0gqpgjv3zhXflRtgsrUCX5xwNnQ==", + "dev": true, + "dependencies": { + "@npmcli/git": "^5.0.0", + "@npmcli/installed-package-contents": "^2.0.1", + "@npmcli/promise-spawn": "^7.0.0", + "@npmcli/run-script": "^7.0.0", + "cacache": "^18.0.0", + "fs-minipass": "^3.0.0", + "minipass": "^7.0.2", + "npm-package-arg": "^11.0.0", + "npm-packlist": "^8.0.0", + "npm-pick-manifest": "^9.0.0", + "npm-registry-fetch": "^16.0.0", + "proc-log": "^3.0.0", + "promise-retry": "^2.0.1", + "read-package-json": "^7.0.0", + "read-package-json-fast": "^3.0.0", + "sigstore": "^2.2.0", + "ssri": "^10.0.0", + "tar": "^6.1.11" + }, + "bin": { + "pacote": "lib/bin.js" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/pacote/node_modules/@npmcli/fs": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-3.1.1.tgz", + "integrity": "sha512-q9CRWjpHCMIh5sVyefoD1cA7PkvILqCZsnSOEUUivORLjxCO/Irmue2DprETiNgEqktDBZaM1Bi+jrarx1XdCg==", + "dev": true, + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/pacote/node_modules/cacache": { + "version": "18.0.3", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-18.0.3.tgz", + "integrity": "sha512-qXCd4rh6I07cnDqh8V48/94Tc/WSfj+o3Gn6NZ0aZovS255bUx8O13uKxRFd2eWG0xgsco7+YItQNPaa5E85hg==", + "dev": true, + "dependencies": { + "@npmcli/fs": "^3.1.0", + "fs-minipass": "^3.0.0", + "glob": "^10.2.2", + "lru-cache": "^10.0.1", + "minipass": "^7.0.3", + "minipass-collect": "^2.0.1", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "p-map": "^4.0.0", + "ssri": "^10.0.0", + "tar": "^6.1.11", + "unique-filename": "^3.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/pacote/node_modules/fs-minipass": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", + "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", + "dev": true, + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/pacote/node_modules/glob": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.3.tgz", + "integrity": "sha512-Q38SGlYRpVtDBPSWEylRyctn7uDeTp4NQERTLiCT1FqA9JXPYWqAVmQU6qh4r/zMM5ehxTcbaO8EjhWnvEhmyg==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/pacote/node_modules/lru-cache": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.0.tgz", + "integrity": "sha512-bfJaPTuEiTYBu+ulDaeQ0F+uLmlfFkMgXj4cbwfuMSjgObGMzb55FMMbDvbRU0fAHZ4sLGkz2mKwcMg8Dvm8Ww==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/pacote/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/pacote/node_modules/minipass-collect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz", + "integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==", + "dev": true, + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/pacote/node_modules/ssri": { + "version": "10.0.6", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-10.0.6.tgz", + "integrity": "sha512-MGrFH9Z4NP9Iyhqn16sDtBpRRNJ0Y2hNa6D65h736fVSaPCHr4DM4sWUNvVaSuC+0OBGhwsrydQwmgfg5LncqQ==", + "dev": true, + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/pacote/node_modules/unique-filename": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-3.0.0.tgz", + "integrity": "sha512-afXhuC55wkAmZ0P18QsVE6kp8JaxrEokN2HGIoIVv2ijHQd419H0+6EigAFcIzXeMIkcIkNBpB3L/DXB3cTS/g==", + "dev": true, + "dependencies": { + "unique-slug": "^4.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/pacote/node_modules/unique-slug": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-4.0.0.tgz", + "integrity": "sha512-WrcA6AyEfqDX5bWige/4NQfPZMtASNVxdmWR76WESYQVAACSgWcR6e9i0mofqqBxYFtL4oAxPIptY73/0YE1DQ==", + "dev": true, + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-json/node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "node_modules/parse-json/node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "node_modules/parse-node-version": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz", + "integrity": "sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", + "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "devOptional": true, + "dependencies": { + "entities": "^4.4.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-html-rewriting-stream": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/parse5-html-rewriting-stream/-/parse5-html-rewriting-stream-7.0.0.tgz", + "integrity": "sha512-mazCyGWkmCRWDI15Zp+UiCqMp/0dgEmkZRvhlsqqKYr4SsVm/TvnSpD9fCvqCA2zoWJcfRym846ejWBBHRiYEg==", + "dev": true, + "dependencies": { + "entities": "^4.3.0", + "parse5": "^7.0.0", + "parse5-sax-parser": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-sax-parser": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/parse5-sax-parser/-/parse5-sax-parser-7.0.0.tgz", + "integrity": "sha512-5A+v2SNsq8T6/mG3ahcz8ZtQ0OUFTatxPbeidoMB7tkJSGDY3tdfl4MHovtLQHkEn5CGxijNWRQHhRQ6IRpXKg==", + "dev": true, + "dependencies": { + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.0.tgz", + "integrity": "sha512-bfJaPTuEiTYBu+ulDaeQ0F+uLmlfFkMgXj4cbwfuMSjgObGMzb55FMMbDvbRU0fAHZ4sLGkz2mKwcMg8Dvm8Ww==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/path-scurry/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/pem": { + "version": "1.14.8", + "resolved": "https://registry.npmjs.org/pem/-/pem-1.14.8.tgz", + "integrity": "sha512-ZpbOf4dj9/fQg5tQzTqv4jSKJQsK7tPl0pm4/pvPcZVjZcJg7TMfr3PBk6gJH97lnpJDu4e4v8UUqEz5daipCg==", + "dependencies": { + "es6-promisify": "^7.0.0", + "md5": "^2.3.0", + "os-tmpdir": "^1.0.2", + "which": "^2.0.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true + }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", + "dev": true + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" + }, + "node_modules/picomatch": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.1.tgz", + "integrity": "sha512-xUXwsxNjwTQ8K3GnT4pCJm+xq3RUPQbmkYJTP5aFIfNIvbcc/4MUxgBaaRSZJ6yGJZiGSyYlM6MzwTsRk8SYCg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/piscina": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/piscina/-/piscina-4.4.0.tgz", + "integrity": "sha512-+AQduEJefrOApE4bV7KRmp3N2JnnyErlVqq4P/jmko4FPz9Z877BCccl/iB3FdrWSUkvbGV9Kan/KllJgat3Vg==", + "dev": true, + "optionalDependencies": { + "nice-napi": "^1.0.2" + } + }, + "node_modules/pkg-dir": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-7.0.0.tgz", + "integrity": "sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA==", + "dev": true, + "dependencies": { + "find-up": "^6.3.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-6.3.0.tgz", + "integrity": "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==", + "dev": true, + "dependencies": { + "locate-path": "^7.1.0", + "path-exists": "^5.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz", + "integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==", + "dev": true, + "dependencies": { + "p-locate": "^6.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", + "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz", + "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==", + "dev": true, + "dependencies": { + "p-limit": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/path-exists": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", + "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/pkg-dir/node_modules/yocto-queue": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.1.1.tgz", + "integrity": "sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g==", + "dev": true, + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/popper.js": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz", + "integrity": "sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==", + "deprecated": "You can find the new Popper v2 at @popperjs/core, this package is dedicated to the legacy v1", + "license": "MIT", + "peer": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/portscanner": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/portscanner/-/portscanner-2.2.0.tgz", + "integrity": "sha512-IFroCz/59Lqa2uBvzK3bKDbDDIEaAY8XJ1jFxcLWTqosrsc32//P4VuSB2vZXoHiHqOmx8B5L5hnKOxL/7FlPw==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "async": "^2.6.0", + "is-number-like": "^1.0.3" + }, + "engines": { + "node": ">=0.4", + "npm": ">=1.0.0" + } + }, + "node_modules/portscanner/node_modules/async": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", + "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "lodash": "^4.17.14" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", + "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postcss": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.1.tgz", + "integrity": "sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.8", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-attribute-case-insensitive": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/postcss-attribute-case-insensitive/-/postcss-attribute-case-insensitive-5.0.2.tgz", + "integrity": "sha512-XIidXV8fDr0kKt28vqki84fRK8VW8eTuIa4PChv2MqKuT6C9UjmSKzen6KaWhWEoYvwxFCa7n/tC1SZ3tyq4SQ==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.10" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/postcss-clamp": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-clamp/-/postcss-clamp-4.1.0.tgz", + "integrity": "sha512-ry4b1Llo/9zz+PKC+030KUnPITTJAHeOwjfAyyB60eT0AorGLdzp52s31OsPRHRf8NchkgFoG2y6fCfn1IV1Ow==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=7.6.0" + }, + "peerDependencies": { + "postcss": "^8.4.6" + } + }, + "node_modules/postcss-color-functional-notation": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/postcss-color-functional-notation/-/postcss-color-functional-notation-4.2.4.tgz", + "integrity": "sha512-2yrTAUZUab9s6CpxkxC4rVgFEVaR6/2Pipvi6qcgvnYiVqZcbDHEoBDhrXzyb7Efh2CCfHQNtcqWcIruDTIUeg==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/postcss-color-hex-alpha": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/postcss-color-hex-alpha/-/postcss-color-hex-alpha-8.0.4.tgz", + "integrity": "sha512-nLo2DCRC9eE4w2JmuKgVA3fGL3d01kGq752pVALF68qpGLmx2Qrk91QTKkdUqqp45T1K1XV8IhQpcu1hoAQflQ==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-color-rebeccapurple": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-color-rebeccapurple/-/postcss-color-rebeccapurple-7.1.1.tgz", + "integrity": "sha512-pGxkuVEInwLHgkNxUc4sdg4g3py7zUeCQ9sMfwyHAT+Ezk8a4OaaVZ8lIY5+oNqA/BXXgLyXv0+5wHP68R79hg==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/postcss-custom-media": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/postcss-custom-media/-/postcss-custom-media-8.0.2.tgz", + "integrity": "sha512-7yi25vDAoHAkbhAzX9dHx2yc6ntS4jQvejrNcC+csQJAXjj15e7VcWfMgLqBNAbOvqi5uIa9huOVwdHbf+sKqg==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.3" + } + }, + "node_modules/postcss-custom-properties": { + "version": "12.1.11", + "resolved": "https://registry.npmjs.org/postcss-custom-properties/-/postcss-custom-properties-12.1.11.tgz", + "integrity": "sha512-0IDJYhgU8xDv1KY6+VgUwuQkVtmYzRwu+dMjnmdMafXYv86SWqfxkc7qdDvWS38vsjaEtv8e0vGOUQrAiMBLpQ==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/postcss-custom-selectors": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/postcss-custom-selectors/-/postcss-custom-selectors-6.0.3.tgz", + "integrity": "sha512-fgVkmyiWDwmD3JbpCmB45SvvlCD6z9CG6Ie6Iere22W5aHea6oWa7EM2bpnv2Fj3I94L3VbtvX9KqwSi5aFzSg==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.4" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.3" + } + }, + "node_modules/postcss-dir-pseudo-class": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/postcss-dir-pseudo-class/-/postcss-dir-pseudo-class-6.0.5.tgz", + "integrity": "sha512-eqn4m70P031PF7ZQIvSgy9RSJ5uI2171O/OO/zcRNYpJbvaeKFUlar1aJ7rmgiQtbm0FSPsRewjpdS0Oew7MPA==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.10" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/postcss-double-position-gradients": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/postcss-double-position-gradients/-/postcss-double-position-gradients-3.1.2.tgz", + "integrity": "sha512-GX+FuE/uBR6eskOK+4vkXgT6pDkexLokPaz/AbJna9s5Kzp/yl488pKPjhy0obB475ovfT1Wv8ho7U/cHNaRgQ==", + "dev": true, + "dependencies": { + "@csstools/postcss-progressive-custom-properties": "^1.1.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/postcss-env-function": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/postcss-env-function/-/postcss-env-function-4.0.6.tgz", + "integrity": "sha512-kpA6FsLra+NqcFnL81TnsU+Z7orGtDTxcOhl6pwXeEq1yFPpRMkCDpHhrz8CFQDr/Wfm0jLiNQ1OsGGPjlqPwA==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-focus-visible": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/postcss-focus-visible/-/postcss-focus-visible-6.0.4.tgz", + "integrity": "sha512-QcKuUU/dgNsstIK6HELFRT5Y3lbrMLEOwG+A4s5cA+fx3A3y/JTq3X9LaOj3OC3ALH0XqyrgQIgey/MIZ8Wczw==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.9" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-focus-within": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/postcss-focus-within/-/postcss-focus-within-5.0.4.tgz", + "integrity": "sha512-vvjDN++C0mu8jz4af5d52CB184ogg/sSxAFS+oUJQq2SuCe7T5U2iIsVJtsCp2d6R4j0jr5+q3rPkBVZkXD9fQ==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.9" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-font-variant": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/postcss-font-variant/-/postcss-font-variant-5.0.0.tgz", + "integrity": "sha512-1fmkBaCALD72CK2a9i468mA/+tr9/1cBxRRMXOUaZqO43oWPR5imcyPjXwuv7PXbCid4ndlP5zWhidQVVa3hmA==", + "dev": true, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-gap-properties": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/postcss-gap-properties/-/postcss-gap-properties-3.0.5.tgz", + "integrity": "sha512-IuE6gKSdoUNcvkGIqdtjtcMtZIFyXZhmFd5RUlg97iVEvp1BZKV5ngsAjCjrVy+14uhGBQl9tzmi1Qwq4kqVOg==", + "dev": true, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/postcss-image-set-function": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/postcss-image-set-function/-/postcss-image-set-function-4.0.7.tgz", + "integrity": "sha512-9T2r9rsvYzm5ndsBE8WgtrMlIT7VbtTfE7b3BQnudUqnBcBo7L758oc+o+pdj/dUV0l5wjwSdjeOH2DZtfv8qw==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/postcss-import": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-14.1.0.tgz", + "integrity": "sha512-flwI+Vgm4SElObFVPpTIT7SU7R3qk2L7PyduMcokiaVKuWv9d/U+Gm/QAd8NDLuykTWTkcrjOeD2Pp1rMeBTGw==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-initial": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-initial/-/postcss-initial-4.0.1.tgz", + "integrity": "sha512-0ueD7rPqX8Pn1xJIjay0AZeIuDoF+V+VvMt/uOnn+4ezUKhZM/NokDeP6DwMNyIoYByuN/94IQnt5FEkaN59xQ==", + "dev": true, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-lab-function": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/postcss-lab-function/-/postcss-lab-function-4.2.1.tgz", + "integrity": "sha512-xuXll4isR03CrQsmxyz92LJB2xX9n+pZJ5jE9JgcnmsCammLyKdlzrBin+25dy6wIjfhJpKBAN80gsTlCgRk2w==", + "dev": true, + "dependencies": { + "@csstools/postcss-progressive-custom-properties": "^1.1.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/postcss-loader": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-4.3.0.tgz", + "integrity": "sha512-M/dSoIiNDOo8Rk0mUqoj4kpGq91gcxCfb9PoyZVdZ76/AuhxylHDYZblNE8o+EQ9AMSASeMFEKxZf5aU6wlx1Q==", + "dev": true, + "dependencies": { + "cosmiconfig": "^7.0.0", + "klona": "^2.0.4", + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0", + "semver": "^7.3.4" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "postcss": "^7.0.0 || ^8.0.1", + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/postcss-loader/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/postcss-loader/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/postcss-loader/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/postcss-loader/node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dev": true, + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/postcss-loader/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/postcss-logical": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/postcss-logical/-/postcss-logical-5.0.4.tgz", + "integrity": "sha512-RHXxplCeLh9VjinvMrZONq7im4wjWGlRJAqmAVLXyZaXwfDWP73/oq4NdIp+OZwhQUMj0zjqDfM5Fj7qby+B4g==", + "dev": true, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-media-minmax": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/postcss-media-minmax/-/postcss-media-minmax-5.0.0.tgz", + "integrity": "sha512-yDUvFf9QdFZTuCUg0g0uNSHVlJ5X1lSzDZjPSFaiCWvjgsvu8vEVxtahPrLMinIDEEGnx6cBe6iqdx5YWz08wQ==", + "dev": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-media-query-parser": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz", + "integrity": "sha512-3sOlxmbKcSHMjlUXQZKQ06jOswE7oVkXPxmZdoB1r5l0q6gTFTQSHxNxOrCccElbW7dxNytifNEo8qidX2Vsig==" + }, + "node_modules/postcss-modules-extract-imports": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz", + "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==", + "dev": true, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-local-by-default": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.5.tgz", + "integrity": "sha512-6MieY7sIfTK0hYfafw1OMEG+2bg8Q1ocHCpoWLqOKj3JXlKu4G7btkmM/B7lFubYkYWmRSPLZi5chid63ZaZYw==", + "dev": true, + "dependencies": { + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^6.0.2", + "postcss-value-parser": "^4.1.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-scope": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.0.tgz", + "integrity": "sha512-oq+g1ssrsZOsx9M96c5w8laRmvEu9C3adDSjI8oTcbfkrTE8hx/zfyobUoWIxaKPO8bt6S62kxpw5GqypEw1QQ==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.4" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-values": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", + "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", + "dev": true, + "dependencies": { + "icss-utils": "^5.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-nesting": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/postcss-nesting/-/postcss-nesting-10.2.0.tgz", + "integrity": "sha512-EwMkYchxiDiKUhlJGzWsD9b2zvq/r2SSubcRrgP+jujMXFzqvANLt16lJANC+5uZ6hjI7lpRmI6O8JIl+8l1KA==", + "dev": true, + "dependencies": { + "@csstools/selector-specificity": "^2.0.0", + "postcss-selector-parser": "^6.0.10" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/postcss-opacity-percentage": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/postcss-opacity-percentage/-/postcss-opacity-percentage-1.1.3.tgz", + "integrity": "sha512-An6Ba4pHBiDtyVpSLymUUERMo2cU7s+Obz6BTrS+gxkbnSBNKSuD0AVUc+CpBMrpVPKKfoVz0WQCX+Tnst0i4A==", + "dev": true, + "funding": [ + { + "type": "kofi", + "url": "https://ko-fi.com/mrcgrtz" + }, + { + "type": "liberapay", + "url": "https://liberapay.com/mrcgrtz" + } + ], + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/postcss-overflow-shorthand": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/postcss-overflow-shorthand/-/postcss-overflow-shorthand-3.0.4.tgz", + "integrity": "sha512-otYl/ylHK8Y9bcBnPLo3foYFLL6a6Ak+3EQBPOTR7luMYCOsiVTUk1iLvNf6tVPNGXcoL9Hoz37kpfriRIFb4A==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/postcss-page-break": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/postcss-page-break/-/postcss-page-break-3.0.4.tgz", + "integrity": "sha512-1JGu8oCjVXLa9q9rFTo4MbeeA5FMe00/9C7lN4va606Rdb+HkxXtXsmEDrIraQ11fGz/WvKWa8gMuCKkrXpTsQ==", + "dev": true, + "peerDependencies": { + "postcss": "^8" + } + }, + "node_modules/postcss-place": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/postcss-place/-/postcss-place-7.0.5.tgz", + "integrity": "sha512-wR8igaZROA6Z4pv0d+bvVrvGY4GVHihBCBQieXFY3kuSuMyOmEnnfFzHl/tQuqHZkfkIVBEbDvYcFfHmpSet9g==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/postcss-preset-env": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/postcss-preset-env/-/postcss-preset-env-7.8.3.tgz", + "integrity": "sha512-T1LgRm5uEVFSEF83vHZJV2z19lHg4yJuZ6gXZZkqVsqv63nlr6zabMH3l4Pc01FQCyfWVrh2GaUeCVy9Po+Aag==", + "dev": true, + "dependencies": { + "@csstools/postcss-cascade-layers": "^1.1.1", + "@csstools/postcss-color-function": "^1.1.1", + "@csstools/postcss-font-format-keywords": "^1.0.1", + "@csstools/postcss-hwb-function": "^1.0.2", + "@csstools/postcss-ic-unit": "^1.0.1", + "@csstools/postcss-is-pseudo-class": "^2.0.7", + "@csstools/postcss-nested-calc": "^1.0.0", + "@csstools/postcss-normalize-display-values": "^1.0.1", + "@csstools/postcss-oklab-function": "^1.1.1", + "@csstools/postcss-progressive-custom-properties": "^1.3.0", + "@csstools/postcss-stepped-value-functions": "^1.0.1", + "@csstools/postcss-text-decoration-shorthand": "^1.0.0", + "@csstools/postcss-trigonometric-functions": "^1.0.2", + "@csstools/postcss-unset-value": "^1.0.2", + "autoprefixer": "^10.4.13", + "browserslist": "^4.21.4", + "css-blank-pseudo": "^3.0.3", + "css-has-pseudo": "^3.0.4", + "css-prefers-color-scheme": "^6.0.3", + "cssdb": "^7.1.0", + "postcss-attribute-case-insensitive": "^5.0.2", + "postcss-clamp": "^4.1.0", + "postcss-color-functional-notation": "^4.2.4", + "postcss-color-hex-alpha": "^8.0.4", + "postcss-color-rebeccapurple": "^7.1.1", + "postcss-custom-media": "^8.0.2", + "postcss-custom-properties": "^12.1.10", + "postcss-custom-selectors": "^6.0.3", + "postcss-dir-pseudo-class": "^6.0.5", + "postcss-double-position-gradients": "^3.1.2", + "postcss-env-function": "^4.0.6", + "postcss-focus-visible": "^6.0.4", + "postcss-focus-within": "^5.0.4", + "postcss-font-variant": "^5.0.0", + "postcss-gap-properties": "^3.0.5", + "postcss-image-set-function": "^4.0.7", + "postcss-initial": "^4.0.1", + "postcss-lab-function": "^4.2.1", + "postcss-logical": "^5.0.4", + "postcss-media-minmax": "^5.0.0", + "postcss-nesting": "^10.2.0", + "postcss-opacity-percentage": "^1.1.2", + "postcss-overflow-shorthand": "^3.0.4", + "postcss-page-break": "^3.0.4", + "postcss-place": "^7.0.5", + "postcss-pseudo-class-any-link": "^7.1.6", + "postcss-replace-overflow-wrap": "^4.0.0", + "postcss-selector-not": "^6.0.1", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/postcss-pseudo-class-any-link": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/postcss-pseudo-class-any-link/-/postcss-pseudo-class-any-link-7.1.6.tgz", + "integrity": "sha512-9sCtZkO6f/5ML9WcTLcIyV1yz9D1rf0tWc+ulKcvV30s0iZKS/ONyETvoWsr6vnrmW+X+KmuK3gV/w5EWnT37w==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.10" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/postcss-replace-overflow-wrap": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-replace-overflow-wrap/-/postcss-replace-overflow-wrap-4.0.0.tgz", + "integrity": "sha512-KmF7SBPphT4gPPcKZc7aDkweHiKEEO8cla/GjcBK+ckKxiZslIu3C4GCRW3DNfL0o7yW7kMQu9xlZ1kXRXLXtw==", + "dev": true, + "peerDependencies": { + "postcss": "^8.0.3" + } + }, + "node_modules/postcss-selector-not": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-selector-not/-/postcss-selector-not-6.0.1.tgz", + "integrity": "sha512-1i9affjAe9xu/y9uqWH+tD4r6/hDaXJruk8xn2x1vzxC2U3J3LKO3zJW4CyxlNhA56pADJ/djpEwpH1RClI2rQ==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.10" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.0.tgz", + "integrity": "sha512-UMz42UD0UY0EApS0ZL9o1XnLhSTtvvvLe5Dc2H2O56fvRZi+KulDyf5ctDhhtYJBGKStV2FL1fy6253cmLgqVQ==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/pretty-bytes": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", + "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==", + "dev": true, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/proc-log": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-3.0.0.tgz", + "integrity": "sha512-++Vn7NS4Xf9NacaU9Xq3URUuqZETPsf8L4j5/ckhaRYsfPeRyzGw+iDjFhV/Jr3uNmTvvddEJFWh5R1gRgUH8A==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "dev": true, + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, + "node_modules/promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", + "dev": true + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "dev": true, + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "node_modules/prr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==", + "dev": true, + "optional": true + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true + }, + "node_modules/pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", + "dev": true + }, + "node_modules/qjobs": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/qjobs/-/qjobs-1.2.0.tgz", + "integrity": "sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg==", + "dev": true, + "engines": { + "node": ">=0.9" + } + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/raf-schd": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.3.tgz", + "integrity": "sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==" + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/re-reselect": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/re-reselect/-/re-reselect-4.0.1.tgz", + "integrity": "sha512-xVTNGQy/dAxOolunBLmVMGZ49VUUR1s8jZUiJQb+g1sI63GAv9+a5Jas9yHvdxeUgiZkU9r3gDExDorxHzOgRA==", + "peerDependencies": { + "reselect": ">1.0.0" + } + }, + "node_modules/re-resizable": { + "version": "6.9.17", + "resolved": "https://registry.npmjs.org/re-resizable/-/re-resizable-6.9.17.tgz", + "integrity": "sha512-OBqd1BwVXpEJJn/yYROG+CbeqIDBWIp6wathlpB0kzZWWZIY1gPTsgK2yJEui5hOvkCdC2mcexF2V3DZVfLq2g==", + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.13.1 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/react": { + "version": "16.14.0", + "resolved": "https://registry.npmjs.org/react/-/react-16.14.0.tgz", + "integrity": "sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g==", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1", + "prop-types": "^15.6.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-aria-live": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/react-aria-live/-/react-aria-live-2.0.5.tgz", + "integrity": "sha512-rXiH1HNKJrr/UfVeGwA2aKY43r5WbjLs+AYB6/kJF1qny2hwxzQc1qewQmUpdQ5h8HAOTD8O/XlGcEHjqlCl0g==", + "dependencies": { + "uuid": "^3.2.1" + }, + "peerDependencies": { + "react": "^16.3.x" + } + }, + "node_modules/react-aria-live/node_modules/uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "bin": { + "uuid": "bin/uuid" + } + }, + "node_modules/react-beautiful-dnd": { + "version": "13.1.1", + "resolved": "https://registry.npmjs.org/react-beautiful-dnd/-/react-beautiful-dnd-13.1.1.tgz", + "integrity": "sha512-0Lvs4tq2VcrEjEgDXHjT98r+63drkKEgqyxdA7qD3mvKwga6a5SscbdLPO2IExotU1jW8L0Ksdl0Cj2AF67nPQ==", + "dependencies": { + "@babel/runtime": "^7.9.2", + "css-box-model": "^1.2.0", + "memoize-one": "^5.1.1", + "raf-schd": "^4.0.2", + "react-redux": "^7.2.0", + "redux": "^4.0.4", + "use-memo-one": "^1.1.1" + }, + "peerDependencies": { + "react": "^16.8.5 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.5 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/react-copy-to-clipboard": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/react-copy-to-clipboard/-/react-copy-to-clipboard-5.1.0.tgz", + "integrity": "sha512-k61RsNgAayIJNoy9yDsYzDe/yAZAzEbEgcz3DZMhF686LEyukcE1hzurxe85JandPUG+yTfGVFzuEw3xt8WP/A==", + "dependencies": { + "copy-to-clipboard": "^3.3.1", + "prop-types": "^15.8.1" + }, + "peerDependencies": { + "react": "^15.3.0 || 16 || 17 || 18" + } + }, + "node_modules/react-dnd": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/react-dnd/-/react-dnd-10.0.2.tgz", + "integrity": "sha512-SC2Ymvntynhoqtf5zaFhZscm9xenCoMofilxPdlwUlaelAzmbl9fw82C4ZJ//+lNm3kWAKXjGDZg2/aWjKEAtg==", + "dependencies": { + "@react-dnd/shallowequal": "^2.0.0", + "@types/hoist-non-react-statics": "^3.3.1", + "dnd-core": "^10.0.2", + "hoist-non-react-statics": "^3.3.0" + }, + "peerDependencies": { + "react": ">= 16.8", + "react-dom": ">= 16.8" + } + }, + "node_modules/react-dnd-html5-backend": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/react-dnd-html5-backend/-/react-dnd-html5-backend-10.0.2.tgz", + "integrity": "sha512-ny17gUdInZ6PIGXdzfwPhoztRdNVVvjoJMdG80hkDBamJBeUPuNF2Wv4D3uoQJLjXssX1+i9PhBqc7EpogClwQ==", + "dependencies": { + "dnd-core": "^10.0.2" + } + }, + "node_modules/react-dnd-multi-backend": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/react-dnd-multi-backend/-/react-dnd-multi-backend-5.0.1.tgz", + "integrity": "sha512-E45T5xVl4CLt9QFV9ZMjWCGLyF16+B6B6FgbeZE9J5o0rvLHaCqyAJTelRDXtbSiSBPaDlN1STO86jkiD31AHA==", + "dependencies": { + "dnd-multi-backend": "^5.0.1", + "prop-types": "^15.7.2", + "react-dnd-preview": "^5.0.1" + }, + "peerDependencies": { + "react": "^16.13", + "react-dnd-html5-backend": "^10.0.2", + "react-dnd-touch-backend": "^10.0.2" + } + }, + "node_modules/react-dnd-preview": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/react-dnd-preview/-/react-dnd-preview-5.0.1.tgz", + "integrity": "sha512-wwUgk8jWuO4WMOoa+GviHoZyYAiXer6vsSW2aEhpz0gsde08O+4cI+j7DG7q9vYlBnljNutdQ1WjDV0VskZ4jQ==", + "dependencies": { + "prop-types": "^15.7.2" + }, + "peerDependencies": { + "react": "^16.13.1", + "react-dnd": "^10.0.2" + } + }, + "node_modules/react-dnd-touch-backend": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/react-dnd-touch-backend/-/react-dnd-touch-backend-10.0.2.tgz", + "integrity": "sha512-+lW/Ern0dKyHToD0oP+Wc/ZD6l7qJazosLqbjzL7OnPlig6WxdlrHkJylOLkeAdZj41fIJJ551Lb57pIL0CcPw==", + "dependencies": { + "@react-dnd/invariant": "^2.0.0", + "dnd-core": "^10.0.2" + } + }, + "node_modules/react-dom": { + "version": "16.14.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.14.0.tgz", + "integrity": "sha512-1gCeQXDLoIqMgqD3IO2Ah9bnf0w9kzhwN5q4FGnHZ67hBm9yePzB5JJAIQCc8x3pFnNlwFq4RidZggNAAkzWWw==", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1", + "prop-types": "^15.6.2", + "scheduler": "^0.19.1" + }, + "peerDependencies": { + "react": "^16.14.0" + } + }, + "node_modules/react-draggable": { + "version": "4.4.6", + "resolved": "https://registry.npmjs.org/react-draggable/-/react-draggable-4.4.6.tgz", + "integrity": "sha512-LtY5Xw1zTPqHkVmtM3X8MUOxNDOUhv/khTgBgrUvwaS064bwVvxT+q5El0uUFNx5IEPKXuRejr7UqLwBIg5pdw==", + "dependencies": { + "clsx": "^1.1.1", + "prop-types": "^15.8.1" + }, + "peerDependencies": { + "react": ">= 16.3.0", + "react-dom": ">= 16.3.0" + } + }, + "node_modules/react-full-screen": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/react-full-screen/-/react-full-screen-0.2.5.tgz", + "integrity": "sha512-LNkxjLWmiR+AwemSVdn/miUcBy8tHA6mDVS1qz1AM/DHNEtQbzkh5ok9A6g99502OqutQq1zBvCBGLV8rsB2tw==", + "dependencies": { + "@types/react": "*", + "fscreen": "^1.0.1" + }, + "peerDependencies": { + "prop-types": "^15.5", + "react": "^15.6 || ^16" + } + }, + "node_modules/react-i18next": { + "version": "11.18.6", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-11.18.6.tgz", + "integrity": "sha512-yHb2F9BiT0lqoQDt8loZ5gWP331GwctHz9tYQ8A2EIEUu+CcEdjBLQWli1USG3RdWQt3W+jqQLg/d4rrQR96LA==", + "dependencies": { + "@babel/runtime": "^7.14.5", + "html-parse-stringify": "^3.0.1" + }, + "peerDependencies": { + "i18next": ">= 19.0.0", + "react": ">= 16.8.0" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, + "node_modules/react-image": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/react-image/-/react-image-4.1.0.tgz", + "integrity": "sha512-qwPNlelQe9Zy14K2pGWSwoL+vHsAwmJKS6gkotekDgRpcnRuzXNap00GfibD3eEPYu3WCPlyIUUNzcyHOrLHjw==", + "peerDependencies": { + "@babel/runtime": ">=7", + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" + }, + "node_modules/react-lifecycles-compat": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", + "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" + }, + "node_modules/react-modal": { + "version": "3.16.1", + "resolved": "https://registry.npmjs.org/react-modal/-/react-modal-3.16.1.tgz", + "integrity": "sha512-VStHgI3BVcGo7OXczvnJN7yT2TWHJPDXZWyI/a0ssFNhGZWsPmB8cF0z33ewDXq4VfYMO1vXgiv/g8Nj9NDyWg==", + "dependencies": { + "exenv": "^1.2.0", + "prop-types": "^15.7.2", + "react-lifecycles-compat": "^3.0.0", + "warning": "^4.0.3" + }, + "engines": { + "node": ">=8" + }, + "peerDependencies": { + "react": "^0.14.0 || ^15.0.0 || ^16 || ^17 || ^18", + "react-dom": "^0.14.0 || ^15.0.0 || ^16 || ^17 || ^18" + } + }, + "node_modules/react-mosaic-component": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/react-mosaic-component/-/react-mosaic-component-4.1.1.tgz", + "integrity": "sha512-HVlLvfYQ/AKmoKvw95Orx3Qyc7SNuS/QlAy+SkAVit1g9ipzXBGYoBg7RMXP5sF5w47CgYxA+1gT+fYRVf73jA==", + "dependencies": { + "classnames": "^2.2.6", + "immutability-helper": "^3.0.1", + "lodash": "^4.17.11", + "prop-types": "^15.7.2", + "react-dnd": "^10.0.2", + "react-dnd-html5-backend": "^10.0.2", + "react-dnd-multi-backend": "^5.0.0", + "react-dnd-touch-backend": "^10.0.2", + "uuid": "^3.3.2" + }, + "peerDependencies": { + "react": "^16.3.0" + } + }, + "node_modules/react-mosaic-component/node_modules/uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "bin": { + "uuid": "bin/uuid" + } + }, + "node_modules/react-redux": { + "version": "7.2.9", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.9.tgz", + "integrity": "sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==", + "dependencies": { + "@babel/runtime": "^7.15.4", + "@types/react-redux": "^7.1.20", + "hoist-non-react-statics": "^3.3.2", + "loose-envify": "^1.4.0", + "prop-types": "^15.7.2", + "react-is": "^17.0.2" + }, + "peerDependencies": { + "react": "^16.8.3 || ^17 || ^18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, + "node_modules/react-resize-observer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/react-resize-observer/-/react-resize-observer-1.1.1.tgz", + "integrity": "sha512-3R+90Hou90Mr3wJYc+unsySC8Pn91V4nmjO32NKvUvjphRUbq9HisyLg7bDyGBE7xlMrrM6Fax7iNQaFdc/FYA==", + "peerDependencies": { + "react": ">=0.14" + } + }, + "node_modules/react-rnd": { + "version": "10.4.11", + "resolved": "https://registry.npmjs.org/react-rnd/-/react-rnd-10.4.11.tgz", + "integrity": "sha512-XTfNGNcS0ad2vo3to7qNTB0BkFML9k1csIUI0Nlj44M6Uuh7yP/2h8WXiXcV3v3bxxVJck1C9K6FS1LrLH0E0Q==", + "dependencies": { + "re-resizable": "6.9.17", + "react-draggable": "4.4.6", + "tslib": "2.6.2" + }, + "peerDependencies": { + "react": ">=16.3.0", + "react-dom": ">=16.3.0" + } + }, + "node_modules/react-sizeme": { + "version": "2.6.12", + "resolved": "https://registry.npmjs.org/react-sizeme/-/react-sizeme-2.6.12.tgz", + "integrity": "sha512-tL4sCgfmvapYRZ1FO2VmBmjPVzzqgHA7kI8lSJ6JS6L78jXFNRdOZFpXyK6P1NBZvKPPCZxReNgzZNUajAerZw==", + "dependencies": { + "element-resize-detector": "^1.2.1", + "invariant": "^2.2.4", + "shallowequal": "^1.1.0", + "throttle-debounce": "^2.1.0" + }, + "peerDependencies": { + "react": "^0.14.0 || ^15.0.0-0 || ^16.0.0", + "react-dom": "^0.14.0 || ^15.0.0-0 || ^16.0.0" + } + }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, + "node_modules/react-virtualized-auto-sizer": { + "version": "1.0.24", + "resolved": "https://registry.npmjs.org/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.24.tgz", + "integrity": "sha512-3kCn7N9NEb3FlvJrSHWGQ4iVl+ydQObq2fHMn12i5wbtm74zHOPhz/i64OL3c1S1vi9i2GXtZqNqUJTQ+BnNfg==", + "peerDependencies": { + "react": "^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0", + "react-dom": "^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/react-window": { + "version": "1.8.10", + "resolved": "https://registry.npmjs.org/react-window/-/react-window-1.8.10.tgz", + "integrity": "sha512-Y0Cx+dnU6NLa5/EvoHukUD0BklJ8qITCtVEPY1C/nL8wwoZ0b5aEw8Ff1dOVHw7fCzMt55XfJDd8S8W8LCaUCg==", + "dependencies": { + "@babel/runtime": "^7.0.0", + "memoize-one": ">=3.1.1 <6" + }, + "engines": { + "node": ">8.0.0" + }, + "peerDependencies": { + "react": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/read-package-json": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/read-package-json/-/read-package-json-7.0.1.tgz", + "integrity": "sha512-8PcDiZ8DXUjLf687Ol4BR8Bpm2umR7vhoZOzNRt+uxD9GpBh/K+CAAALVIiYFknmvlmyg7hM7BSNUXPaCCqd0Q==", + "deprecated": "This package is no longer supported. Please use @npmcli/package-json instead.", + "dev": true, + "dependencies": { + "glob": "^10.2.2", + "json-parse-even-better-errors": "^3.0.0", + "normalize-package-data": "^6.0.0", + "npm-normalize-package-bin": "^3.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/read-package-json-fast": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/read-package-json-fast/-/read-package-json-fast-3.0.2.tgz", + "integrity": "sha512-0J+Msgym3vrLOUB3hzQCuZHII0xkNGCtz/HJH9xZshwv9DbDwkw1KaE3gx/e2J5rpEY5rtOy6cyhKOPrkP7FZw==", + "dev": true, + "dependencies": { + "json-parse-even-better-errors": "^3.0.0", + "npm-normalize-package-bin": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/read-package-json/node_modules/glob": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.3.tgz", + "integrity": "sha512-Q38SGlYRpVtDBPSWEylRyctn7uDeTp4NQERTLiCT1FqA9JXPYWqAVmQU6qh4r/zMM5ehxTcbaO8EjhWnvEhmyg==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/read-package-json/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/readdirp/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/rechoir": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", + "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", + "dev": true, + "dependencies": { + "resolve": "^1.20.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/redux": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", + "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", + "dependencies": { + "@babel/runtime": "^7.9.2" + } + }, + "node_modules/redux-devtools-extension": { + "version": "2.13.9", + "resolved": "https://registry.npmjs.org/redux-devtools-extension/-/redux-devtools-extension-2.13.9.tgz", + "integrity": "sha512-cNJ8Q/EtjhQaZ71c8I9+BPySIBVEKssbPpskBfsXqb8HJ002A3KRVHfeRzwRo6mGPqsm7XuHTqNSNeS1Khig0A==", + "deprecated": "Package moved to @redux-devtools/extension.", + "peerDependencies": { + "redux": "^3.1.0 || ^4.0.0" + } + }, + "node_modules/redux-saga": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/redux-saga/-/redux-saga-1.3.0.tgz", + "integrity": "sha512-J9RvCeAZXSTAibFY0kGw6Iy4EdyDNW7k6Q+liwX+bsck7QVsU78zz8vpBRweEfANxnnlG/xGGeOvf6r8UXzNJQ==", + "dependencies": { + "@redux-saga/core": "^1.3.0" + } + }, + "node_modules/redux-thunk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.2.tgz", + "integrity": "sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q==", + "peerDependencies": { + "redux": "^4" + } + }, + "node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==" + }, + "node_modules/regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", + "dev": true + }, + "node_modules/regenerate-unicode-properties": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.0.tgz", + "integrity": "sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA==", + "dev": true, + "dependencies": { + "regenerate": "^1.4.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" + }, + "node_modules/regenerator-transform": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz", + "integrity": "sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.8.4" + } + }, + "node_modules/regex-parser": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/regex-parser/-/regex-parser-2.3.0.tgz", + "integrity": "sha512-TVILVSz2jY5D47F4mA4MppkBrafEaiUWJO/TcZHEIuI13AqoZMkK1WMA4Om1YkYbTx+9Ki1/tSUXbceyr9saRg==", + "dev": true + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz", + "integrity": "sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.6", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "set-function-name": "^2.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexpu-core": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.1.1.tgz", + "integrity": "sha512-k67Nb9jvwJcJmVpw0jPttR1/zVfnKf8Km0IPatrU/zJ5XeG3+Slx0xLXs9HByJSzXzrlz5EDvN6yLNMDc2qdnw==", + "dev": true, + "dependencies": { + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.2.0", + "regjsgen": "^0.8.0", + "regjsparser": "^0.11.0", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==", + "dev": true + }, + "node_modules/regjsparser": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.11.1.tgz", + "integrity": "sha512-1DHODs4B8p/mQHU9kr+jv8+wIC9mtG4eBHxWxIq5mhjE3D5oORhCc6deRKzTjs9DcfRFmj9BHSDguZklqCGFWQ==", + "dev": true, + "dependencies": { + "jsesc": "~3.0.2" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/regjsparser/node_modules/jsesc": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", + "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/request-progress": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/request-progress/-/request-progress-3.0.0.tgz", + "integrity": "sha512-MnWzEHHaxHO2iWiQuHrUPBi/1WeBf5PkxQqNyNvLl9VAYSdXkP8tQ3pBSeCPD+yw0v0Aq1zosWLz0BdeXpWwZg==", + "dev": true, + "dependencies": { + "throttleit": "^1.0.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/requireindex": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/requireindex/-/requireindex-1.2.0.tgz", + "integrity": "sha512-L9jEkOi3ASd9PYit2cwRfyppc9NoABujTP8/5gFcbERmo5jUoAKovIC3fsF17pkTnGsrByysqX+Kxd2OTNI1ww==", + "dev": true, + "engines": { + "node": ">=0.10.5" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" + }, + "node_modules/reselect": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.1.8.tgz", + "integrity": "sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ==" + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-url-loader": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-url-loader/-/resolve-url-loader-5.0.0.tgz", + "integrity": "sha512-uZtduh8/8srhBoMx//5bwqjQ+rfYOUq8zC9NrMUGtjBiGTtFJM42s58/36+hTqeqINcnYe08Nj3LkK9lW4N8Xg==", + "dev": true, + "dependencies": { + "adjust-sourcemap-loader": "^4.0.0", + "convert-source-map": "^1.7.0", + "loader-utils": "^2.0.0", + "postcss": "^8.2.14", + "source-map": "0.6.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/resolve-url-loader/node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dev": true, + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/resolve-url-loader/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resp-modifier": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/resp-modifier/-/resp-modifier-6.0.2.tgz", + "integrity": "sha512-U1+0kWC/+4ncRFYqQWTx/3qkfE6a4B/h3XXgmXypfa0SPZ3t7cbbaFk297PjQS/yov24R18h6OZe6iZwj3NSLw==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "debug": "^2.2.0", + "minimatch": "^3.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/resp-modifier/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/resp-modifier/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/resp-modifier/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/resp-modifier/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "optional": true, + "peer": true + }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/roarr": { + "version": "7.21.1", + "resolved": "https://registry.npmjs.org/roarr/-/roarr-7.21.1.tgz", + "integrity": "sha512-3niqt5bXFY1InKU8HKWqqYTYjtrBaxBMnXELXCXUYgtNYGUtZM5rB46HIC430AyacL95iEniGf7RgqsesykLmQ==", + "dependencies": { + "fast-printf": "^1.6.9", + "safe-stable-stringify": "^2.4.3", + "semver-compare": "^1.0.0" + }, + "engines": { + "node": ">=18.0" + } + }, + "node_modules/rollup": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.24.0.tgz", + "integrity": "sha512-DOmrlGSXNk1DM0ljiQA+i+o0rSLhtii1je5wgk60j49d1jHT5YYttBv1iWOnYSTG+fZZESUOSNiAl89SIet+Cg==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.6" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.24.0", + "@rollup/rollup-android-arm64": "4.24.0", + "@rollup/rollup-darwin-arm64": "4.24.0", + "@rollup/rollup-darwin-x64": "4.24.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.24.0", + "@rollup/rollup-linux-arm-musleabihf": "4.24.0", + "@rollup/rollup-linux-arm64-gnu": "4.24.0", + "@rollup/rollup-linux-arm64-musl": "4.24.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.24.0", + "@rollup/rollup-linux-riscv64-gnu": "4.24.0", + "@rollup/rollup-linux-s390x-gnu": "4.24.0", + "@rollup/rollup-linux-x64-gnu": "4.24.0", + "@rollup/rollup-linux-x64-musl": "4.24.0", + "@rollup/rollup-win32-arm64-msvc": "4.24.0", + "@rollup/rollup-win32-ia32-msvc": "4.24.0", + "@rollup/rollup-win32-x64-msvc": "4.24.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/rtl-css-js": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/rtl-css-js/-/rtl-css-js-1.16.1.tgz", + "integrity": "sha512-lRQgou1mu19e+Ya0LsTvKrVJ5TYUbqCVPAiImX3UfLTenarvPUl1QFdvu5Z3PYmHT9RCcwIfbjRQBntExyj3Zg==", + "dependencies": { + "@babel/runtime": "^7.1.2" + } + }, + "node_modules/run-async": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-3.0.0.tgz", + "integrity": "sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/rx": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/rx/-/rx-4.1.0.tgz", + "integrity": "sha512-CiaiuN6gapkdl+cZUr67W6I8jquN4lkak3vtIsIWCl4XIPP8ffsoyN6/+PuGXnQy8Cu8W2y9Xxh31Rq4M6wUug==", + "dev": true, + "optional": true, + "peer": true + }, + "node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/rxjs-report-usage": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/rxjs-report-usage/-/rxjs-report-usage-1.0.6.tgz", + "integrity": "sha512-omv1DIv5z1kV+zDAEjaDjWSkx8w5TbFp5NZoPwUipwzYVcor/4So9ZU3bUyQ1c8lxY5Q0Es/ztWW7PGjY7to0Q==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.10.3", + "@babel/traverse": "^7.10.3", + "@babel/types": "^7.10.3", + "bent": "~7.3.6", + "chalk": "~4.1.0", + "glob": "~7.2.0", + "prompts": "~2.4.2" + }, + "bin": { + "rxjs-report-usage": "bin/rxjs-report-usage" + } + }, + "node_modules/rxjs-report-usage/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/rxjs-report-usage/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/rxjs-report-usage/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/rxjs-report-usage/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/rxjs-report-usage/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/rxjs-report-usage/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/safe-array-concat": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.2.tgz", + "integrity": "sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "get-intrinsic": "^1.2.4", + "has-symbols": "^1.0.3", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/safe-regex-test": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz", + "integrity": "sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-regex": "^1.1.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-stable-stringify": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz", + "integrity": "sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==", + "engines": { + "node": ">=10" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/sass": { + "version": "1.83.4", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.83.4.tgz", + "integrity": "sha512-B1bozCeNQiOgDcLd33e2Cs2U60wZwjUUXzh900ZyQF5qUasvMdDZYbQ566LJu7cqR+sAHlAfO6RMkaID5s6qpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^4.0.0", + "immutable": "^5.0.2", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + }, + "optionalDependencies": { + "@parcel/watcher": "^2.4.1" + } + }, + "node_modules/sass-loader": { + "version": "12.6.0", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-12.6.0.tgz", + "integrity": "sha512-oLTaH0YCtX4cfnJZxKSLAyglED0naiYfNG1iXfU5w1LNZ+ukoA5DtyDIN5zmKVZwYNJP4KRc5Y3hkWga+7tYfA==", + "dev": true, + "dependencies": { + "klona": "^2.0.4", + "neo-async": "^2.6.2" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "fibers": ">= 3.1.0", + "node-sass": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0", + "sass": "^1.3.0", + "sass-embedded": "*", + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "fibers": { + "optional": true + }, + "node-sass": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + } + } + }, + "node_modules/sass-resources-loader": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/sass-resources-loader/-/sass-resources-loader-2.2.5.tgz", + "integrity": "sha512-po8rfETH9cOQACWxubT/1CCu77KjxwRtCDm6QAXZH99aUHBydwSoxdIjC40SGp/dcS/FkSNJl0j1VEojGZqlvQ==", + "dev": true, + "dependencies": { + "async": "^3.2.3", + "chalk": "^4.1.0", + "glob": "^7.1.6", + "loader-utils": "^2.0.0" + } + }, + "node_modules/sass-resources-loader/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/sass-resources-loader/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/sass-resources-loader/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/sass-resources-loader/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/sass-resources-loader/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/sass-resources-loader/node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dev": true, + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/sass-resources-loader/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/sass/node_modules/chokidar": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz", + "integrity": "sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==", + "dev": true, + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/sass/node_modules/immutable": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.0.3.tgz", + "integrity": "sha512-P8IdPQHq3lA1xVeBRi5VPqUm5HDgKnx0Ru51wZz5mjxHr5n3RWhjIpOFU7ybkUxfB+5IToy+OLaHYDBIWsv+uw==", + "dev": true + }, + "node_modules/sass/node_modules/readdirp": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.2.tgz", + "integrity": "sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==", + "dev": true, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/sax": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", + "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==", + "dev": true, + "optional": true + }, + "node_modules/scheduler": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.19.1.tgz", + "integrity": "sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA==", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1" + } + }, + "node_modules/schema-utils": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz", + "integrity": "sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/select-hose": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", + "integrity": "sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==", + "dev": true + }, + "node_modules/selfsigned": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.4.1.tgz", + "integrity": "sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==", + "dev": true, + "dependencies": { + "@types/node-forge": "^1.3.0", + "node-forge": "^1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", + "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==" + }, + "node_modules/semver/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/send/node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/serve-index": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", + "integrity": "sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==", + "dev": true, + "dependencies": { + "accepts": "~1.3.4", + "batch": "0.6.1", + "debug": "2.6.9", + "escape-html": "~1.0.3", + "http-errors": "~1.6.2", + "mime-types": "~2.1.17", + "parseurl": "~1.3.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/serve-index/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/serve-index/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", + "dev": true, + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", + "dev": true + }, + "node_modules/serve-index/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/serve-index/node_modules/setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", + "dev": true + }, + "node_modules/serve-index/node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/serve-static/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/server-destroy": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/server-destroy/-/server-destroy-1.0.1.tgz", + "integrity": "sha512-rb+9B5YBIEzYcD6x2VKidaa+cqYBJQKnU4oe4E3ANwRRN56yk/ua1YCJT1n21NTS8w6CcOclAKNP3PhdCXKYtQ==", + "dev": true, + "optional": true, + "peer": true + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "node_modules/shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "dev": true, + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shallowequal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", + "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/shell-quote": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz", + "integrity": "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "node_modules/sigstore": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/sigstore/-/sigstore-2.3.1.tgz", + "integrity": "sha512-8G+/XDU8wNsJOQS5ysDVO0Etg9/2uA5gR9l4ZwijjlwxBcrU6RPfwi2+jJmbP+Ap1Hlp/nVAaEO4Fj22/SL2gQ==", + "dev": true, + "dependencies": { + "@sigstore/bundle": "^2.3.2", + "@sigstore/core": "^1.0.0", + "@sigstore/protobuf-specs": "^0.3.2", + "@sigstore/sign": "^2.3.2", + "@sigstore/tuf": "^2.3.4", + "@sigstore/verify": "^1.2.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/simple-update-notifier": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-1.1.0.tgz", + "integrity": "sha512-VpsrsJSUcJEseSbMHkrsrAVSdvVS5I96Qo1QAQ4FxQ9wXFcB+pjj7FB7/us9+GcgfW4ziHtYMc1J0PLczb55mg==", + "dev": true, + "dependencies": { + "semver": "~7.0.0" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/simple-update-notifier/node_modules/semver": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz", + "integrity": "sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/slice-ansi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", + "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/slice-ansi/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/slice-ansi/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socket.io": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.0.tgz", + "integrity": "sha512-8U6BEgGjQOfGz3HHTYaC/L1GaxDCJ/KM0XTkJly0EhZ5U/du9uNEZy4ZgYzEzIqlx2CMm25CrCqr1ck899eLNA==", + "dev": true, + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.3.2", + "engine.io": "~6.6.0", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", + "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", + "dev": true, + "dependencies": { + "debug": "~4.3.4", + "ws": "~8.17.1" + } + }, + "node_modules/socket.io-client": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.0.tgz", + "integrity": "sha512-C0jdhD5yQahMws9alf/yvtsMGTaIDBnZ8Rb5HU56svyq0l5LIrGzIDZZD5pHQlmzxLuU91Gz+VpQMKgCTNYtkw==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.2", + "engine.io-client": "~6.6.1", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "dev": true, + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/sockjs": { + "version": "0.3.24", + "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", + "integrity": "sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==", + "dev": true, + "dependencies": { + "faye-websocket": "^0.11.3", + "uuid": "^8.3.2", + "websocket-driver": "^0.7.4" + } + }, + "node_modules/socks": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz", + "integrity": "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==", + "dev": true, + "dependencies": { + "ip-address": "^9.0.5", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.4.tgz", + "integrity": "sha512-GNAq/eg8Udq2x0eNiFkr9gRg5bA7PXEWagQdeRX4cPSG+X/8V38v637gim9bjFptMk1QWsCTr0ttrJEiXbNnRw==", + "dev": true, + "dependencies": { + "agent-base": "^7.1.1", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/source-list-map": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", + "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==", + "dev": true + }, + "node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-loader": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-5.0.0.tgz", + "integrity": "sha512-k2Dur7CbSLcAH73sBcIkV5xjPV4SzqO1NJ7+XaQl8if3VODDUj3FNchNGpqgJSKbvUfJuhVdv8K2Eu8/TNl2eA==", + "dev": true, + "dependencies": { + "iconv-lite": "^0.6.3", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.72.1" + } + }, + "node_modules/source-map-loader/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sourcemap-codec": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", + "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", + "deprecated": "Please use @jridgewell/sourcemap-codec instead", + "dev": true + }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "dev": true + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.18", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.18.tgz", + "integrity": "sha512-xxRs31BqRYHwiMzudOrpSiHtZ8i/GeionCBDSilhYRj+9gIcI8wCZTlXZKu9vZIVqViP3dcp9qE5G6AlIaD+TQ==", + "dev": true + }, + "node_modules/spdy": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", + "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==", + "dev": true, + "dependencies": { + "debug": "^4.1.0", + "handle-thing": "^2.0.0", + "http-deceiver": "^1.2.7", + "select-hose": "^2.0.0", + "spdy-transport": "^3.0.0" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/spdy-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", + "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", + "dev": true, + "dependencies": { + "debug": "^4.1.0", + "detect-node": "^2.0.4", + "hpack.js": "^2.1.6", + "obuf": "^1.1.2", + "readable-stream": "^3.0.6", + "wbuf": "^1.7.3" + } + }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "dev": true + }, + "node_modules/sshpk": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", + "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==", + "dev": true, + "dependencies": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + }, + "bin": { + "sshpk-conv": "bin/sshpk-conv", + "sshpk-sign": "bin/sshpk-sign", + "sshpk-verify": "bin/sshpk-verify" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sshpk/node_modules/jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==", + "dev": true + }, + "node_modules/ssri": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz", + "integrity": "sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==", + "dev": true, + "dependencies": { + "minipass": "^3.1.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/stream-throttle": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/stream-throttle/-/stream-throttle-0.1.3.tgz", + "integrity": "sha512-889+B9vN9dq7/vLbGyuHeZ6/ctf5sNuGWsDy89uNxkFTAgzy0eK7+w5fL3KLNRTkLle7EgZGvHUphZW0Q26MnQ==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "commander": "^2.2.0", + "limiter": "^1.0.5" + }, + "bin": { + "throttleproxy": "bin/throttleproxy.js" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/stream-throttle/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "optional": true, + "peer": true + }, + "node_modules/streamroller": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-3.1.5.tgz", + "integrity": "sha512-KFxaM7XT+irxvdqSP1LGLgNWbYN7ay5owZ3r/8t77p+EtSUAfUgtl7be3xtqtOmGUl9K9YPO2ca8133RlTjvKw==", + "dev": true, + "dependencies": { + "date-format": "^4.0.14", + "debug": "^4.3.4", + "fs-extra": "^8.1.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/streamroller/node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/streamroller/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz", + "integrity": "sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz", + "integrity": "sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/symbol-observable": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz", + "integrity": "sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==", + "dev": true, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/synckit": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.6.2.tgz", + "integrity": "sha512-Vhf+bUa//YSTYKseDiiEuQmhGCoIF3CVBhunm3r/DQnYiGT4JssmnKQc44BIyOZRK2pKjXXAgbhfmbeoC9CJpA==", + "dev": true, + "dependencies": { + "tslib": "^2.3.1" + }, + "engines": { + "node": ">=12.20" + } + }, + "node_modules/tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "dev": true, + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/terser": { + "version": "5.29.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.29.1.tgz", + "integrity": "sha512-lZQ/fyaIGxsbGxApKmoPTODIzELy3++mXhS5hOqaAWZjQtpq/hFHAc+rm29NND1rYRxRWKcjuARNwULNXa5RtQ==", + "dev": true, + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.3.10", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz", + "integrity": "sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.20", + "jest-worker": "^27.4.5", + "schema-utils": "^3.1.1", + "serialize-javascript": "^6.0.1", + "terser": "^5.26.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/terser-webpack-plugin/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/terser-webpack-plugin/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/terser-webpack-plugin/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/terser-webpack-plugin/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "node_modules/throttle-debounce": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-2.3.0.tgz", + "integrity": "sha512-H7oLPV0P7+jgvrk+6mwwwBDmxTaxnu9HMXmloNLXwnNO0ZxZ31Orah2n8lU1eMPvsaowP2CX+USCgyovXfdOFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/throttleit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-1.0.1.tgz", + "integrity": "sha512-vDZpf9Chs9mAdfY046mcPt8fg5QSZr37hEH4TXYBnDF+izxgrbRGUAAaBvIk/fJm9aOFCGFd1EsNg5AZCbnQCQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true + }, + "node_modules/thunky": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", + "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", + "dev": true + }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==" + }, + "node_modules/tiny-warning": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", + "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==" + }, + "node_modules/tldts": { + "version": "6.1.65", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.65.tgz", + "integrity": "sha512-xU9gLTfAGsADQ2PcWee6Hg8RFAv0DnjMGVJmDnUmI8a9+nYmapMQix4afwrdaCtT+AqP4MaxEzu7cCrYmBPbzQ==", + "dev": true, + "dependencies": { + "tldts-core": "^6.1.65" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.65", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.65.tgz", + "integrity": "sha512-Uq5t0N0Oj4nQSbU8wFN1YYENvMthvwU13MQrMJRspYCGLSAZjAfoBOJki5IQpnBM/WFskxxC/gIOTwaedmHaSg==", + "dev": true + }, + "node_modules/tmp": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", + "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==", + "dev": true, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toggle-selection": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz", + "integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==" + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/tough-cookie": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.0.0.tgz", + "integrity": "sha512-FRKsF7cz96xIIeMZ82ehjC3xW2E+O2+v11udrDYewUbszngYhsGa8z6YUMMzO9QJZzzyd0nGGXnML/TReX6W8Q==", + "dev": true, + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/ts-api-utils": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", + "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", + "dev": true, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "node_modules/ts-node": { + "version": "8.10.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-8.10.2.tgz", + "integrity": "sha512-ISJJGgkIpDdBhWVu3jufsWpK3Rzo7bdiIXJjQc0ynKxVOVcg2oIrf2H2cejminGrptVc6q6/uynAHNCuWGbpVA==", + "dev": true, + "dependencies": { + "arg": "^4.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "source-map-support": "^0.5.17", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "engines": { + "node": ">=6.0.0" + }, + "peerDependencies": { + "typescript": ">=2.7" + } + }, + "node_modules/tsconfig-paths": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", + "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", + "dev": true, + "dependencies": { + "json5": "^2.2.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + }, + "node_modules/tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "dependencies": { + "tslib": "^1.8.1" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" + } + }, + "node_modules/tsutils-etc": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/tsutils-etc/-/tsutils-etc-1.4.2.tgz", + "integrity": "sha512-2Dn5SxTDOu6YWDNKcx1xu2YUy6PUeKrWZB/x2cQ8vY2+iz3JRembKn/iZ0JLT1ZudGNwQQvtFX9AwvRHbXuPUg==", + "dev": true, + "dependencies": { + "@types/yargs": "^17.0.0", + "yargs": "^17.0.0" + }, + "bin": { + "ts-flags": "bin/ts-flags", + "ts-kind": "bin/ts-kind" + }, + "peerDependencies": { + "tsutils": "^3.0.0", + "typescript": ">=4.0.0" + } + }, + "node_modules/tsutils/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "node_modules/tuf-js": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tuf-js/-/tuf-js-2.2.1.tgz", + "integrity": "sha512-GwIJau9XaA8nLVbUXsN3IlFi7WmQ48gBUrl3FTkkL/XLu/POhBzfmX9hd33FNMX1qAsfl6ozO1iMmW9NC8YniA==", + "dev": true, + "dependencies": { + "@tufjs/models": "2.0.1", + "debug": "^4.3.4", + "make-fetch-happen": "^13.0.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", + "dev": true + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz", + "integrity": "sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz", + "integrity": "sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz", + "integrity": "sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==", + "dev": true, + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.6.tgz", + "integrity": "sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-assert": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/typed-assert/-/typed-assert-1.0.9.tgz", + "integrity": "sha512-KNNZtayBCtmnNmbo5mG47p1XsCyrx6iVqomjcZnec/1Y5GGARaxPs6r49RnSPeUP3YjNYiU9sQHAtY4BBvnZwg==", + "dev": true + }, + "node_modules/typescript": { + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", + "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-compare": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/typescript-compare/-/typescript-compare-0.0.2.tgz", + "integrity": "sha512-8ja4j7pMHkfLJQO2/8tut7ub+J3Lw2S3061eJLFQcvs3tsmJKp8KG5NtpLn7KcY2w08edF74BSVN7qJS0U6oHA==", + "dependencies": { + "typescript-logic": "^0.0.0" + } + }, + "node_modules/typescript-logic": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/typescript-logic/-/typescript-logic-0.0.0.tgz", + "integrity": "sha512-zXFars5LUkI3zP492ls0VskH3TtdeHCqu0i7/duGt60i5IGPIpAHE/DWo5FqJ6EjQ15YKXrt+AETjv60Dat34Q==" + }, + "node_modules/typescript-tuple": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/typescript-tuple/-/typescript-tuple-2.2.1.tgz", + "integrity": "sha512-Zcr0lbt8z5ZdEzERHAMAniTiIKerFCMgd7yjq1fPnDJ43et/k9twIFQMUYff9k5oXcsQ0WpvFcgzK2ZKASoW6Q==", + "dependencies": { + "typescript-compare": "^0.0.2" + } + }, + "node_modules/ua-parser-js": { + "version": "1.0.38", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.38.tgz", + "integrity": "sha512-Aq5ppTOfvrCMgAPneW1HfWj66Xi7XL+/mIy996R1/CLS/rcyJQm6QZdsKrUeivDFQ+Oc9Wyuwor8Ze8peEoUoQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + }, + { + "type": "github", + "url": "https://github.com/sponsors/faisalman" + } + ], + "optional": true, + "peer": true, + "engines": { + "node": "*" + } + }, + "node_modules/uc.micro": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", + "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==" + }, + "node_modules/unbox-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", + "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", + "which-boxed-primitive": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true + }, + "node_modules/undici": { + "version": "6.11.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.11.1.tgz", + "integrity": "sha512-KyhzaLJnV1qa3BSHdj4AZ2ndqI0QWPxYzaIOio0WzcEJB9gvuysprJSLtpvc2D9mhR9jPDUk7xlJlZbH2KR5iw==", + "dev": true, + "engines": { + "node": ">=18.0" + } + }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true, + "optional": true, + "peer": true + }, + "node_modules/unfetch": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/unfetch/-/unfetch-4.2.0.tgz", + "integrity": "sha512-F9p7yYCn6cIW9El1zi0HI6vqpeIvBsr3dSuRO6Xuppb1u5rXpCPmMvLSyECLhybr9isec8Ohl0hPekMVrEinDA==" + }, + "node_modules/unicode-canonical-property-names-ecmascript": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", + "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "dev": true, + "dependencies": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-value-ecmascript": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.0.tgz", + "integrity": "sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-property-aliases-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", + "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/unique-filename": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", + "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", + "dev": true, + "dependencies": { + "unique-slug": "^2.0.0" + } + }, + "node_modules/unique-slug": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz", + "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", + "dev": true, + "dependencies": { + "imurmurhash": "^0.1.4" + } + }, + "node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/untildify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", + "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", + "integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.1.2", + "picocolors": "^1.0.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/uri-js/node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/use-memo-one": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-memo-one/-/use-memo-one-1.1.3.tgz", + "integrity": "sha512-g66/K7ZQGYrI6dy8GLpVcMsBp4s17xNkYJVSMvTEevGy3nDxHOfE6z8BVE22+5G5x7t3+bhzrlTDB7ObrEE0cQ==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/validate-npm-package-name": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-5.0.1.tgz", + "integrity": "sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "node_modules/void-elements": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz", + "integrity": "sha512-qZKX4RnBzH2ugr8Lxa7x+0V6XD9Sb/ouARtiasEQCHB1EVU4NXtmHsDDrx1dO4ne5fc3J6EW05BP1Dl0z0iung==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/warning": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", + "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, + "node_modules/watchpack": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", + "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", + "dev": true, + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/wbuf": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", + "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", + "dev": true, + "dependencies": { + "minimalistic-assert": "^1.0.0" + } + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dev": true, + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/webpack": { + "version": "5.97.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.97.1.tgz", + "integrity": "sha512-EksG6gFY3L1eFMROS/7Wzgrii5mBAFe4rIr3r2BTfo7bcc+DWwFZ4OJ/miOuHJO/A85HwyI4eQ0F6IKXesO7Fg==", + "dev": true, + "dependencies": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.6", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.14.0", + "browserslist": "^4.24.0", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.17.1", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^3.2.0", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.3.10", + "watchpack": "^2.4.1", + "webpack-sources": "^3.2.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-cli": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.1.4.tgz", + "integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==", + "dev": true, + "dependencies": { + "@discoveryjs/json-ext": "^0.5.0", + "@webpack-cli/configtest": "^2.1.1", + "@webpack-cli/info": "^2.0.2", + "@webpack-cli/serve": "^2.0.5", + "colorette": "^2.0.14", + "commander": "^10.0.1", + "cross-spawn": "^7.0.3", + "envinfo": "^7.7.3", + "fastest-levenshtein": "^1.0.12", + "import-local": "^3.0.2", + "interpret": "^3.1.1", + "rechoir": "^0.8.0", + "webpack-merge": "^5.7.3" + }, + "bin": { + "webpack-cli": "bin/cli.js" + }, + "engines": { + "node": ">=14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "5.x.x" + }, + "peerDependenciesMeta": { + "@webpack-cli/generators": { + "optional": true + }, + "webpack-bundle-analyzer": { + "optional": true + }, + "webpack-dev-server": { + "optional": true + } + } + }, + "node_modules/webpack-cli/node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dev": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/webpack-dev-middleware": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-6.1.2.tgz", + "integrity": "sha512-Wu+EHmX326YPYUpQLKmKbTyZZJIB8/n6R09pTmB03kJmnMsVPTo9COzHZFr01txwaCAuZvfBJE4ZCHRcKs5JaQ==", + "dev": true, + "dependencies": { + "colorette": "^2.0.10", + "memfs": "^3.4.12", + "mime-types": "^2.1.31", + "range-parser": "^1.2.1", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + } + } + }, + "node_modules/webpack-dev-server": { + "version": "4.15.2", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.15.2.tgz", + "integrity": "sha512-0XavAZbNJ5sDrCbkpWL8mia0o5WPOd2YGtxrEiZkBK9FjLppIUK2TgxK6qGD2P3hUXTJNNPVibrerKcx5WkR1g==", + "dev": true, + "dependencies": { + "@types/bonjour": "^3.5.9", + "@types/connect-history-api-fallback": "^1.3.5", + "@types/express": "^4.17.13", + "@types/serve-index": "^1.9.1", + "@types/serve-static": "^1.13.10", + "@types/sockjs": "^0.3.33", + "@types/ws": "^8.5.5", + "ansi-html-community": "^0.0.8", + "bonjour-service": "^1.0.11", + "chokidar": "^3.5.3", + "colorette": "^2.0.10", + "compression": "^1.7.4", + "connect-history-api-fallback": "^2.0.0", + "default-gateway": "^6.0.3", + "express": "^4.17.3", + "graceful-fs": "^4.2.6", + "html-entities": "^2.3.2", + "http-proxy-middleware": "^2.0.3", + "ipaddr.js": "^2.0.1", + "launch-editor": "^2.6.0", + "open": "^8.0.9", + "p-retry": "^4.5.0", + "rimraf": "^3.0.2", + "schema-utils": "^4.0.0", + "selfsigned": "^2.1.1", + "serve-index": "^1.9.1", + "sockjs": "^0.3.24", + "spdy": "^4.0.2", + "webpack-dev-middleware": "^5.3.4", + "ws": "^8.13.0" + }, + "bin": { + "webpack-dev-server": "bin/webpack-dev-server.js" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.37.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + }, + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-dev-server/node_modules/connect-history-api-fallback": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz", + "integrity": "sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==", + "dev": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/webpack-dev-server/node_modules/ipaddr.js": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz", + "integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/webpack-dev-server/node_modules/webpack-dev-middleware": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.4.tgz", + "integrity": "sha512-BVdTqhhs+0IfoeAf7EoH5WE+exCmqGerHfDM0IL096Px60Tq2Mn9MAbnaGUe6HiMa41KMCYF19gyzZmBcq/o4Q==", + "dev": true, + "dependencies": { + "colorette": "^2.0.10", + "memfs": "^3.4.3", + "mime-types": "^2.1.31", + "range-parser": "^1.2.1", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/webpack-merge": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", + "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==", + "dev": true, + "dependencies": { + "clone-deep": "^4.0.1", + "flat": "^5.0.2", + "wildcard": "^2.0.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/webpack-sources": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz", + "integrity": "sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==", + "dev": true, + "dependencies": { + "source-list-map": "^2.0.0", + "source-map": "~0.6.1" + } + }, + "node_modules/webpack-sources/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/webpack-subresource-integrity": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/webpack-subresource-integrity/-/webpack-subresource-integrity-5.1.0.tgz", + "integrity": "sha512-sacXoX+xd8r4WKsy9MvH/q/vBtEHr86cpImXwyg74pFIpERKt6FmB8cXpeuh0ZLgclOlHI4Wcll7+R5L02xk9Q==", + "dev": true, + "dependencies": { + "typed-assert": "^1.0.8" + }, + "engines": { + "node": ">= 12" + }, + "peerDependencies": { + "html-webpack-plugin": ">= 5.0.0-beta.1 < 6", + "webpack": "^5.12.0" + }, + "peerDependenciesMeta": { + "html-webpack-plugin": { + "optional": true + } + } + }, + "node_modules/webpack/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/webpack/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/webpack/node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/webpack/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/webpack/node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "node_modules/webpack/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/webpack/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/webpack/node_modules/watchpack": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz", + "integrity": "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack/node_modules/webpack-sources": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", + "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "dev": true, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "dev": true, + "dependencies": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dev": true, + "dependencies": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz", + "integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==", + "dev": true, + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/wildcard": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", + "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", + "dev": true + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/wrap-ansi/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "dev": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xhr2": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/xhr2/-/xhr2-0.2.1.tgz", + "integrity": "sha512-sID0rrVCqkVNUn8t6xuv9+6FViXjUVXq8H5rWOH2rz9fDNQEd4g0EA2XlcEdJXRz5BMEn4O1pJFdT+z4YHhoWw==", + "engines": { + "node": ">= 6" + } + }, + "node_modules/xmlhttprequest-ssl": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.1.tgz", + "integrity": "sha512-ptjR8YSJIXoA3Mbv5po7RtSYHO6mZr8s7i5VGmEk7QY2pQWyT1o0N+W1gKbOyJPUCGXGnuw0wqe8f0L6Y0ny7g==", + "dev": true, + "optional": true, + "peer": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "engines": { + "node": ">=12" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dev": true, + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zone.js": { + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.14.10.tgz", + "integrity": "sha512-YGAhaO7J5ywOXW6InXNlLmfU194F8lVgu7bRntUF3TiG8Y3nBK0x1UJJuHUP/e8IyihkjCYqhCScpSwnlaSRkQ==" + } + } +} diff --git a/package.json b/package.json index c0a3843605..9e72a4cc9d 100644 --- a/package.json +++ b/package.json @@ -1,27 +1,32 @@ { "name": "dspace-angular", - "version": "8.0.0-next", + "version": "9.0.0-next", "scripts": { "ng": "ng", "config:watch": "nodemon", "test:rest": "ts-node --project ./tsconfig.ts-node.json scripts/test-rest.ts", - "start": "yarn run start:prod", - "start:dev": "nodemon --exec \"cross-env NODE_ENV=development yarn run serve\"", - "start:prod": "yarn run build:prod && cross-env NODE_ENV=production yarn run serve:ssr", - "start:mirador:prod": "yarn run build:mirador && yarn run start:prod", - "preserve": "yarn base-href", + "start": "npm run start:prod", + "start:dev": "nodemon --exec \"cross-env NODE_ENV=development npm run serve\"", + "start:prod": "npm run build:prod && cross-env NODE_ENV=production npm run serve:ssr", + "start:mirador:prod": "npm run build:mirador && npm run start:prod", + "preserve": "npm run base-href", "serve": "ts-node --project ./tsconfig.ts-node.json scripts/serve.ts", "serve:ssr": "node dist/server/main", "analyze": "webpack-bundle-analyzer dist/browser/stats.json", "build": "ng build --configuration development", "build:stats": "ng build --stats-json", - "build:prod": "cross-env NODE_ENV=production yarn run build:ssr", + "build:prod": "cross-env NODE_ENV=production npm run build:ssr", "build:ssr": "ng build --configuration production && ng run dspace-angular:server:production", + "build:lint": "rimraf 'lint/dist/**/*.js' 'lint/dist/**/*.js.map' && tsc -b lint/tsconfig.json", "test": "ng test --source-map=true --watch=false --configuration test", "test:watch": "nodemon --exec \"ng test --source-map=true --watch=true --configuration test\"", "test:headless": "ng test --source-map=true --watch=false --configuration test --browsers=ChromeHeadless --code-coverage", - "lint": "ng lint", - "lint-fix": "ng lint --fix=true", + "test:lint": "npm run build:lint && npm run test:lint:nobuild", + "test:lint:nobuild": "jasmine --config=lint/jasmine.json", + "lint": "npm run build:lint && npm run lint:nobuild", + "lint:nobuild": "ng lint", + "lint-fix": "npm run build:lint && ng lint --fix=true", + "docs:lint": "ts-node --project ./lint/tsconfig.json ./lint/generate-docs.ts", "e2e": "cross-env NODE_ENV=production ng e2e", "clean:dev:config": "rimraf src/assets/config.json", "clean:coverage": "rimraf coverage", @@ -31,8 +36,8 @@ "clean:json": "rimraf *.records.json", "clean:node": "rimraf node_modules", "clean:cli": "rimraf .angular/cache", - "clean:prod": "yarn run clean:dist && yarn run clean:log && yarn run clean:doc && yarn run clean:coverage && yarn run clean:json", - "clean": "yarn run clean:prod && yarn run clean:dev:config && yarn run clean:cli && yarn run clean:node", + "clean:prod": "npm run clean:dist && npm run clean:log && npm run clean:doc && npm run clean:coverage && npm run clean:json", + "clean": "npm run clean:prod && npm run clean:dev:config && npm run clean:cli && npm run clean:node", "sync-i18n": "ts-node --project ./tsconfig.ts-node.json scripts/sync-i18n-files.ts", "build:mirador": "webpack --config webpack/webpack.mirador.config.ts", "merge-i18n": "ts-node --project ./tsconfig.ts-node.json scripts/merge-i18n-files.ts", @@ -40,7 +45,8 @@ "cypress:run": "cypress run", "env:yaml": "ts-node --project ./tsconfig.ts-node.json scripts/env-to-yaml.ts", "base-href": "ts-node --project ./tsconfig.ts-node.json scripts/base-href.ts", - "check-circ-deps": "npx madge --exclude '(bitstream|bundle|collection|config-submission-form|eperson|item|version)\\.model\\.ts$' --circular --extensions ts ./" + "check-circ-deps": "npx madge --exclude '(bitstream|bundle|collection|config-submission-form|eperson|item|version)\\.model\\.ts$' --circular --extensions ts ./", + "postinstall": "npm run build:lint || echo 'Skipped DSpace ESLint plugins.'" }, "browser": { "fs": false, @@ -49,163 +55,188 @@ "https": false }, "private": true, - "resolutions": { - "minimist": "^1.2.5", - "webdriver-manager": "^12.1.8", - "ts-node": "10.2.1" + "overrides": { + "@kolkov/ngx-gallery": { + "@angular/animations": "^17.3.11", + "@angular/common": "^17.3.11", + "@angular/core": "^17.3.11" + }, + "@ng-bootstrap/ng-bootstrap": { + "@angular/common": "^17.3.11", + "@angular/core": "^17.3.11", + "@angular/forms": "^17.3.11", + "@angular/localize": "^17.3.11" + }, + "@ng-dynamic-forms/core": { + "@angular/common": "^17.3.11", + "@angular/core": "^17.3.11", + "@angular/forms": "^17.3.11" + }, + "@ng-dynamic-forms/ui-ng-bootstrap": { + "ngx-mask": "14.2.4" + }, + "@ngtools/webpack": { + "@angular/compiler-cli": "^17.3.11", + "typescript": "~5.4.5" + }, + "@nicky-lenaers/ngx-scroll-to": { + "@angular/common": "^17.3.11", + "@angular/core": "^17.3.11" + }, + "eslint-plugin-unused-imports": { + "@typescript-eslint/eslint-plugin": "^7.2.0" + }, + "ng2-file-upload": { + "@angular/common": "^17.3.11", + "@angular/core": "^17.3.11" + }, + "ngx-infinite-scroll": { + "@angular/common": "^17.3.11", + "@angular/core": "^17.3.11" + } }, "dependencies": { - "@angular/animations": "^15.2.8", - "@angular/cdk": "^15.2.8", - "@angular/common": "^15.2.8", - "@angular/compiler": "^15.2.8", - "@angular/core": "^15.2.8", - "@angular/forms": "^15.2.8", - "@angular/localize": "15.2.8", - "@angular/platform-browser": "^15.2.8", - "@angular/platform-browser-dynamic": "^15.2.8", - "@angular/platform-server": "^15.2.8", - "@angular/router": "^15.2.8", - "@babel/runtime": "7.21.0", + "@angular/animations": "^17.3.12", + "@angular/cdk": "^17.3.10", + "@angular/common": "^17.3.12", + "@angular/compiler": "^17.3.12", + "@angular/core": "^17.3.12", + "@angular/forms": "^17.3.12", + "@angular/localize": "^17.3.12", + "@angular/platform-browser": "^17.3.12", + "@angular/platform-browser-dynamic": "^17.3.12", + "@angular/platform-server": "^17.3.12", + "@angular/router": "^17.3.12", + "@angular/ssr": "^17.3.11", + "@babel/runtime": "7.26.0", "@kolkov/ngx-gallery": "^2.0.1", - "@material-ui/core": "^4.11.0", - "@material-ui/icons": "^4.11.3", "@ng-bootstrap/ng-bootstrap": "^11.0.0", - "@ng-dynamic-forms/core": "^15.0.0", - "@ng-dynamic-forms/ui-ng-bootstrap": "^15.0.0", - "@ngrx/effects": "^15.4.0", - "@ngrx/router-store": "^15.4.0", - "@ngrx/store": "^15.4.0", - "@nguniversal/express-engine": "^15.2.1", + "@ng-dynamic-forms/core": "^16.0.0", + "@ng-dynamic-forms/ui-ng-bootstrap": "^16.0.0", + "@ngrx/effects": "^17.1.1", + "@ngrx/router-store": "^17.1.1", + "@ngrx/store": "^17.1.1", "@ngx-translate/core": "^14.0.0", "@nicky-lenaers/ngx-scroll-to": "^14.0.0", - "@types/grecaptcha": "^3.0.4", - "angular-idle-preload": "3.0.0", "angulartics2": "^12.2.0", - "axios": "^1.6.0", + "axios": "^1.7.9", "bootstrap": "^4.6.1", "cerialize": "0.1.18", "cli-progress": "^3.12.0", "colors": "^1.4.0", - "compression": "^1.7.4", - "cookie-parser": "1.4.6", - "core-js": "^3.30.1", + "compression": "^1.7.5", + "cookie-parser": "1.4.7", + "core-js": "^3.40.0", "date-fns": "^2.29.3", "date-fns-tz": "^1.3.7", "deepmerge": "^4.3.1", - "ejs": "^3.1.9", - "express": "^4.18.2", + "ejs": "^3.1.10", + "express": "^4.21.2", "express-rate-limit": "^5.1.3", "fast-json-patch": "^3.1.1", "filesize": "^6.1.0", - "http-proxy-middleware": "^1.0.5", + "http-proxy-middleware": "^2.0.7", "http-terminator": "^3.2.0", - "isbot": "^3.6.10", + "isbot": "^5.1.21", "js-cookie": "2.2.1", "js-yaml": "^4.1.0", "json5": "^2.2.3", - "jsonschema": "1.4.1", + "jsonschema": "1.5.0", "jwt-decode": "^3.1.2", - "klaro": "^0.7.18", "lodash": "^4.17.21", "lru-cache": "^7.14.1", "markdown-it": "^13.0.1", - "markdown-it-mathjax3": "^4.3.2", - "mirador": "^3.3.0", + "mirador": "^3.4.3", "mirador-dl-plugin": "^0.13.0", - "mirador-share-plugin": "^0.11.0", + "mirador-share-plugin": "^0.16.0", "morgan": "^1.10.0", - "ng-mocks": "^14.10.0", - "ng2-file-upload": "1.4.0", + "ng2-file-upload": "5.0.0", "ng2-nouislider": "^2.0.0", - "ngx-infinite-scroll": "^15.0.0", + "ngx-infinite-scroll": "^16.0.0", "ngx-pagination": "6.0.3", - "ngx-sortablejs": "^11.1.0", + "ngx-skeleton-loader": "^9.0.0", "ngx-ui-switch": "^14.1.0", "nouislider": "^15.7.1", - "pem": "1.14.7", - "prop-types": "^15.8.1", - "react-copy-to-clipboard": "^5.1.0", - "reflect-metadata": "^0.1.13", + "orejime": "^2.3.1", + "pem": "1.14.8", + "reflect-metadata": "^0.2.2", "rxjs": "^7.8.0", - "sanitize-html": "^2.12.1", - "sortablejs": "1.15.0", "uuid": "^8.3.2", - "webfontloader": "1.6.28", - "zone.js": "~0.11.5" + "zone.js": "~0.14.10" }, "devDependencies": { - "@angular-builders/custom-webpack": "~15.0.0", - "@angular-devkit/build-angular": "^15.2.6", - "@angular-eslint/builder": "15.2.1", - "@angular-eslint/eslint-plugin": "15.2.1", - "@angular-eslint/eslint-plugin-template": "15.2.1", - "@angular-eslint/schematics": "15.2.1", - "@angular-eslint/template-parser": "15.2.1", - "@angular/cli": "^16.0.4", - "@angular/compiler-cli": "^15.2.8", - "@angular/language-service": "^15.2.8", + "@angular-builders/custom-webpack": "~17.0.2", + "@angular-devkit/build-angular": "^17.3.11", + "@angular-eslint/builder": "^17.5.3", + "@angular-eslint/bundled-angular-compiler": "^17.5.3", + "@angular-eslint/eslint-plugin": "^17.5.3", + "@angular-eslint/eslint-plugin-template": "^17.5.3", + "@angular-eslint/schematics": "^17.5.3", + "@angular-eslint/template-parser": "^17.5.3", + "@angular-eslint/utils": "^17.5.3", + "@angular/cli": "^17.3.11", + "@angular/compiler-cli": "^17.3.11", + "@angular/language-service": "^17.3.12", "@cypress/schematic": "^1.5.0", - "@fortawesome/fontawesome-free": "^6.4.0", - "@ngrx/store-devtools": "^15.4.0", - "@ngtools/webpack": "^15.2.6", - "@nguniversal/builders": "^15.2.1", - "@types/deep-freeze": "0.1.2", + "@fortawesome/fontawesome-free": "^6.7.2", + "@ngrx/store-devtools": "^17.1.1", + "@ngtools/webpack": "^16.2.16", + "@types/deep-freeze": "0.1.5", "@types/ejs": "^3.1.2", "@types/express": "^4.17.17", + "@types/grecaptcha": "^3.0.9", "@types/jasmine": "~3.6.0", "@types/js-cookie": "2.2.6", - "@types/lodash": "^4.14.194", + "@types/lodash": "^4.17.14", "@types/node": "^14.14.9", - "@types/sanitize-html": "^2.9.0", - "@typescript-eslint/eslint-plugin": "^5.59.1", - "@typescript-eslint/parser": "^5.59.1", - "axe-core": "^4.7.2", + "@typescript-eslint/eslint-plugin": "^7.18.0", + "@typescript-eslint/parser": "^7.18.0", + "@typescript-eslint/rule-tester": "^7.18.0", + "@typescript-eslint/utils": "^7.18.0", + "axe-core": "^4.10.2", "compression-webpack-plugin": "^9.2.0", "copy-webpack-plugin": "^6.4.1", "cross-env": "^7.0.3", - "cypress": "12.17.4", - "cypress-axe": "^1.4.0", + "cypress": "^13.17.0", + "cypress-axe": "^1.5.0", "deep-freeze": "0.0.1", "eslint": "^8.39.0", "eslint-plugin-deprecation": "^1.4.1", - "eslint-plugin-import": "^2.27.5", + "eslint-plugin-dspace-angular-html": "file:./lint/dist/src/rules/html", + "eslint-plugin-dspace-angular-ts": "file:./lint/dist/src/rules/ts", + "eslint-plugin-import": "^2.31.0", "eslint-plugin-import-newlines": "^1.3.1", "eslint-plugin-jsdoc": "^45.0.0", - "eslint-plugin-jsonc": "^2.6.0", + "eslint-plugin-jsonc": "^2.18.2", "eslint-plugin-lodash": "^7.4.0", "eslint-plugin-rxjs": "^5.0.3", "eslint-plugin-simple-import-sort": "^10.0.0", - "eslint-plugin-unused-imports": "^2.0.0", - "express-static-gzip": "^2.1.7", + "eslint-plugin-unused-imports": "^3.2.0", + "express-static-gzip": "^2.2.0", + "jasmine": "^3.8.0", "jasmine-core": "^3.8.0", "jasmine-marbles": "0.9.2", - "karma": "^6.4.2", + "karma": "^6.4.4", "karma-chrome-launcher": "~3.2.0", "karma-coverage-istanbul-reporter": "~3.0.3", "karma-jasmine": "~4.0.0", "karma-jasmine-html-reporter": "^1.5.0", "karma-mocha-reporter": "2.2.5", - "ngx-mask": "^13.1.7", + "ng-mocks": "^14.13.2", + "ngx-mask": "14.2.4", "nodemon": "^2.0.22", - "postcss": "^8.4", - "postcss-apply": "0.12.0", + "postcss": "^8.5", "postcss-import": "^14.0.0", "postcss-loader": "^4.0.3", "postcss-preset-env": "^7.4.2", - "postcss-responsive-type": "1.0.0", - "react": "^16.14.0", - "react-dom": "^16.14.0", "rimraf": "^3.0.2", - "rxjs-spy": "^8.0.2", - "sass": "~1.62.0", + "sass": "~1.83.4", "sass-loader": "^12.6.0", "sass-resources-loader": "^2.2.5", "ts-node": "^8.10.2", - "typescript": "~4.8.4", - "webpack": "5.76.1", - "webpack-bundle-analyzer": "^4.8.0", - "webpack-cli": "^4.2.0", - "webpack-dev-server": "^4.13.3" + "typescript": "~5.4.5", + "webpack": "5.97.1", + "webpack-cli": "^5.1.4", + "webpack-dev-server": "^4.15.1" } } diff --git a/postcss.config.js b/postcss.config.js index df092d1d39..f8b9666b31 100644 --- a/postcss.config.js +++ b/postcss.config.js @@ -1,8 +1,6 @@ module.exports = { plugins: [ require('postcss-import')(), - require('postcss-preset-env')(), - require('postcss-apply')(), - require('postcss-responsive-type')() + require('postcss-preset-env')() ] }; diff --git a/scripts/merge-i18n-files.ts b/scripts/merge-i18n-files.ts index e790828c0d..64442f5788 100644 --- a/scripts/merge-i18n-files.ts +++ b/scripts/merge-i18n-files.ts @@ -38,7 +38,7 @@ parseCliInput(); function parseCliInput() { program .option('-d, --output-dir ', 'output dir when running script on all language files', projectRoot(LANGUAGE_FILES_LOCATION)) - .option('-s, --source-dir ', 'source dir of transalations to be merged') + .option('-s, --source-dir ', 'source dir of translations to be merged') .usage('(-s [-d ])') .parse(process.argv); diff --git a/scripts/sync-i18n-files.ts b/scripts/sync-i18n-files.ts index 96ba0d4010..170266b6a2 100644 --- a/scripts/sync-i18n-files.ts +++ b/scripts/sync-i18n-files.ts @@ -275,7 +275,9 @@ function readFileIfExists(pathToFile) { try { return fs.readFileSync(pathToFile, 'utf8'); } catch (e) { - console.error('Error:', e.stack); + if (e instanceof Error) { + console.error('Error:', e.stack); + } } } return null; diff --git a/server.ts b/server.ts index da085f372f..1276621e9d 100644 --- a/server.ts +++ b/server.ts @@ -17,7 +17,6 @@ import 'zone.js/node'; import 'reflect-metadata'; -import 'rxjs'; /* eslint-disable import/no-namespace */ import * as morgan from 'morgan'; @@ -28,7 +27,7 @@ import * as expressStaticGzip from 'express-static-gzip'; /* eslint-enable import/no-namespace */ import axios from 'axios'; import LRU from 'lru-cache'; -import isbot from 'isbot'; +import { isbot } from 'isbot'; import { createCertificate } from 'pem'; import { createServer } from 'https'; import { json } from 'body-parser'; @@ -39,23 +38,26 @@ import { join } from 'path'; import { enableProdMode } from '@angular/core'; -import { ngExpressEngine } from '@nguniversal/express-engine'; -import { REQUEST, RESPONSE } from '@nguniversal/express-engine/tokens'; import { environment } from './src/environments/environment'; import { createProxyMiddleware } from 'http-proxy-middleware'; -import { hasNoValue, hasValue } from './src/app/shared/empty.util'; - +import { hasValue } from './src/app/shared/empty.util'; import { UIServerConfig } from './src/config/ui-server-config.interface'; - -import { ServerAppModule } from './src/main.server'; - +import bootstrap from './src/main.server'; import { buildAppConfig } from './src/config/config.server'; -import { APP_CONFIG, AppConfig } from './src/config/app-config.interface'; +import { + APP_CONFIG, + AppConfig, +} from './src/config/app-config.interface'; import { extendEnvironmentWithAppConfig } from './src/config/config.util'; import { logStartupMessage } from './startup-message'; import { TOKENITEM } from './src/app/core/auth/models/auth-token-info.model'; - +import { CommonEngine } from '@angular/ssr'; +import { APP_BASE_HREF } from '@angular/common'; +import { + REQUEST, + RESPONSE, +} from './src/express.tokens'; /* * Set path for the browser application's dist folder @@ -97,7 +99,7 @@ export function app() { * If production mode is enabled in the environment file: * - Enable Angular's production mode * - Initialize caching of SSR rendered pages (if enabled in config.yml) - * - Enable compression for SSR reponses. See [compression](https://github.com/expressjs/compression) + * - Enable compression for SSR responses. See [compression](https://github.com/expressjs/compression) */ if (environment.production) { enableProdMode(); @@ -127,27 +129,6 @@ export function app() { */ server.use(json()); - // Our Universal express-engine (found @ https://github.com/angular/universal/tree/master/modules/express-engine) - server.engine('html', (_, options, callback) => - ngExpressEngine({ - bootstrap: ServerAppModule, - providers: [ - { - provide: REQUEST, - useValue: (options as any).req, - }, - { - provide: RESPONSE, - useValue: (options as any).req.res, - }, - { - provide: APP_CONFIG, - useValue: environment - } - ] - })(_, (options as any), callback) - ); - server.engine('ejs', ejs.renderFile); /* @@ -162,7 +143,7 @@ export function app() { server.get('/robots.txt', (req, res) => { res.setHeader('content-type', 'text/plain'); res.render('assets/robots.txt.ejs', { - 'origin': req.protocol + '://' + req.headers.host + 'origin': req.protocol + '://' + req.headers.host, }); }); @@ -177,7 +158,7 @@ export function app() { router.use('/sitemap**', createProxyMiddleware({ target: `${environment.rest.baseUrl}/sitemaps`, pathRewrite: path => path.replace(environment.ui.nameSpace, '/'), - changeOrigin: true + changeOrigin: true, })); /** @@ -186,7 +167,7 @@ export function app() { router.use('/signposting**', createProxyMiddleware({ target: `${environment.rest.baseUrl}`, pathRewrite: path => path.replace(environment.ui.nameSpace, '/'), - changeOrigin: true + changeOrigin: true, })); /** @@ -197,7 +178,7 @@ export function app() { const RateLimit = require('express-rate-limit'); const limiter = new RateLimit({ windowMs: (environment.ui as UIServerConfig).rateLimiter.windowMs, - max: (environment.ui as UIServerConfig).rateLimiter.max + max: (environment.ui as UIServerConfig).rateLimiter.max, }); server.use(limiter); } @@ -236,10 +217,10 @@ export function app() { /* * The callback function to serve server side angular */ -function ngApp(req, res) { - if (environment.universal.preboot) { +function ngApp(req, res, next) { + if (environment.ssr.enabled && req.method === 'GET' && (req.path === '/' || environment.ssr.paths.some(pathPrefix => req.path.startsWith(pathPrefix)))) { // Render the page to user via SSR (server side rendering) - serverSideRender(req, res); + serverSideRender(req, res, next); } else { // If preboot is disabled, just serve the client console.log('Universal off, serving for direct client-side rendering (CSR)'); @@ -252,45 +233,66 @@ function ngApp(req, res) { * returned to the user. * @param req current request * @param res current response + * @param next the next function * @param sendToUser if true (default), send the rendered content to the user. * If false, then only save this rendered content to the in-memory cache (to refresh cache). */ -function serverSideRender(req, res, sendToUser: boolean = true) { +function serverSideRender(req, res, next, sendToUser: boolean = true) { + const { protocol, originalUrl, baseUrl, headers } = req; + const commonEngine = new CommonEngine({ enablePerformanceProfiler: environment.ssr.enablePerformanceProfiler }); // Render the page via SSR (server side rendering) - res.render(indexHtml, { - req, - res, - preboot: environment.universal.preboot, - async: environment.universal.async, - time: environment.universal.time, - baseUrl: environment.ui.nameSpace, - originUrl: environment.ui.baseUrl, - requestUrl: req.originalUrl, - }, (err, data) => { - if (hasNoValue(err) && hasValue(data)) { - // save server side rendered page to cache (if any are enabled) - saveToCache(req, data); - if (sendToUser) { - res.locals.ssr = true; // mark response as SSR (enables text compression) - // send rendered page to user - res.send(data); + commonEngine + .render({ + bootstrap, + documentFilePath: indexHtml, + inlineCriticalCss: environment.ssr.inlineCriticalCss, + url: `${protocol}://${headers.host}${originalUrl}`, + publicPath: DIST_FOLDER, + providers: [ + { provide: APP_BASE_HREF, useValue: baseUrl }, + { + provide: REQUEST, + useValue: req, + }, + { + provide: RESPONSE, + useValue: res, + }, + { + provide: APP_CONFIG, + useValue: environment, + }, + ], + }) + .then((html) => { + if (hasValue(html)) { + // save server side rendered page to cache (if any are enabled) + saveToCache(req, html); + if (sendToUser) { + res.locals.ssr = true; // mark response as SSR (enables text compression) + // send rendered page to user + res.send(html); + } } - } else if (hasValue(err) && err.code === 'ERR_HTTP_HEADERS_SENT') { - // When this error occurs we can't fall back to CSR because the response has already been - // sent. These errors occur for various reasons in universal, not all of which are in our - // control to solve. - console.warn('Warning [ERR_HTTP_HEADERS_SENT]: Tried to set headers after they were sent to the client'); - } else { - console.warn('Error in server-side rendering (SSR)'); - if (hasValue(err)) { - console.warn('Error details : ', err); + }) + .catch((err) => { + if (hasValue(err) && err.code === 'ERR_HTTP_HEADERS_SENT') { + // When this error occurs we can't fall back to CSR because the response has already been + // sent. These errors occur for various reasons in universal, not all of which are in our + // control to solve. + console.warn('Warning [ERR_HTTP_HEADERS_SENT]: Tried to set headers after they were sent to the client'); + } else { + console.warn('Error in server-side rendering (SSR)'); + if (hasValue(err)) { + console.warn('Error details : ', err); + } + if (sendToUser) { + console.warn('Falling back to serving direct client-side rendering (CSR).'); + clientSideRender(req, res); + } } - if (sendToUser) { - console.warn('Falling back to serving direct client-side rendering (CSR).'); - clientSideRender(req, res); - } - } - }); + next(err); + }); } /** @@ -325,7 +327,7 @@ function initCache() { botCache = new LRU( { max: environment.cache.serverSide.botCache.max, ttl: environment.cache.serverSide.botCache.timeToLive, - allowStale: environment.cache.serverSide.botCache.allowStale + allowStale: environment.cache.serverSide.botCache.allowStale, }); } @@ -337,7 +339,7 @@ function initCache() { anonymousCache = new LRU( { max: environment.cache.serverSide.anonymousCache.max, ttl: environment.cache.serverSide.anonymousCache.timeToLive, - allowStale: environment.cache.serverSide.anonymousCache.allowStale + allowStale: environment.cache.serverSide.anonymousCache.allowStale, }); } } @@ -348,7 +350,7 @@ function initCache() { function botCacheEnabled(): boolean { // Caching is only enabled if SSR is enabled AND // "max" pages to cache is greater than zero - return environment.universal.preboot && environment.cache.serverSide.botCache.max && (environment.cache.serverSide.botCache.max > 0); + return environment.ssr.enabled && environment.cache.serverSide.botCache.max && (environment.cache.serverSide.botCache.max > 0); } /** @@ -357,7 +359,7 @@ function botCacheEnabled(): boolean { function anonymousCacheEnabled(): boolean { // Caching is only enabled if SSR is enabled AND // "max" pages to cache is greater than zero - return environment.universal.preboot && environment.cache.serverSide.anonymousCache.max && (environment.cache.serverSide.anonymousCache.max > 0); + return environment.ssr.enabled && environment.cache.serverSide.anonymousCache.max && (environment.cache.serverSide.anonymousCache.max > 0); } /** @@ -370,9 +372,9 @@ function cacheCheck(req, res, next) { // If the bot cache is enabled and this request looks like a bot, check the bot cache for a cached page. if (botCacheEnabled() && isbot(req.get('user-agent'))) { - cachedCopy = checkCacheForRequest('bot', botCache, req, res); + cachedCopy = checkCacheForRequest('bot', botCache, req, res, next); } else if (anonymousCacheEnabled() && !isUserAuthenticated(req)) { - cachedCopy = checkCacheForRequest('anonymous', anonymousCache, req, res); + cachedCopy = checkCacheForRequest('anonymous', anonymousCache, req, res, next); } // If cached copy exists, return it to the user. @@ -408,14 +410,15 @@ function cacheCheck(req, res, next) { * @param cache LRU cache to check * @param req current request to look for in the cache * @param res current response + * @param next the next function * @returns cached copy (if found) or undefined (if not found) */ -function checkCacheForRequest(cacheName: string, cache: LRU, req, res): any { +function checkCacheForRequest(cacheName: string, cache: LRU, req, res, next): any { // Get the cache key for this request const key = getCacheKey(req); // Check if this page is in our cache - let cachedCopy = cache.get(key); + const cachedCopy = cache.get(key); if (cachedCopy) { if (environment.cache.serverSide.debug) { console.log(`CACHE HIT FOR ${key} in ${cacheName} cache`); } @@ -425,8 +428,8 @@ function checkCacheForRequest(cacheName: string, cache: LRU, req, r if (environment.cache.serverSide.debug) { console.log(`CACHE EXPIRED FOR ${key} in ${cacheName} cache. Re-rendering...`); } // Update cached copy by rerendering server-side // NOTE: In this scenario the currently cached copy will be returned to the current user. - // This re-render is peformed behind the scenes to update cached copy for next user. - serverSideRender(req, res, false); + // This re-render is performed behind the scenes to update cached copy for next user. + serverSideRender(req, res, next, false); } } else { if (environment.cache.serverSide.debug) { console.log(`CACHE MISS FOR ${key} in ${cacheName} cache.`); } @@ -529,20 +532,20 @@ function serverStarted() { function createHttpsServer(keys) { const listener = createServer({ key: keys.serviceKey, - cert: keys.certificate - }, app).listen(environment.ui.port, environment.ui.host, () => { + cert: keys.certificate, + }, app()).listen(environment.ui.port, environment.ui.host, () => { serverStarted(); }); // Graceful shutdown when signalled - const terminator = createHttpTerminator({server: listener}); + const terminator = createHttpTerminator({ server: listener }); process.on('SIGINT', () => { - void (async ()=> { - console.debug('Closing HTTPS server on signal'); - await terminator.terminate().catch(e => { console.error(e); }); - console.debug('HTTPS server closed'); - })(); - }); + void (async ()=> { + console.debug('Closing HTTPS server on signal'); + await terminator.terminate().catch(e => { console.error(e); }); + console.debug('HTTPS server closed'); + })(); + }); } /** @@ -559,14 +562,14 @@ function run() { }); // Graceful shutdown when signalled - const terminator = createHttpTerminator({server: listener}); + const terminator = createHttpTerminator({ server: listener }); process.on('SIGINT', () => { - void (async () => { - console.debug('Closing HTTP server on signal'); - await terminator.terminate().catch(e => { console.error(e); }); - console.debug('HTTP server closed.');return undefined; - })(); - }); + void (async () => { + console.debug('Closing HTTP server on signal'); + await terminator.terminate().catch(e => { console.error(e); }); + console.debug('HTTP server closed.');return undefined; + })(); + }); } function start() { @@ -597,7 +600,7 @@ function start() { if (serviceKey && certificate) { createHttpsServer({ serviceKey: serviceKey, - certificate: certificate + certificate: certificate, }); } else { console.warn('Disabling certificate validation and proceeding with a self-signed certificate. If this is a production server, it is recommended that you configure a valid certificate instead.'); @@ -606,7 +609,7 @@ function start() { createCertificate({ days: 1, - selfSigned: true + selfSigned: true, }, (error, keys) => { createHttpsServer(keys); }); @@ -627,7 +630,7 @@ function healthCheck(req, res) { }) .catch((error) => { res.status(error.response.status).send({ - error: error.message + error: error.message, }); }); } diff --git a/src/app/access-control/access-control-routes.ts b/src/app/access-control/access-control-routes.ts new file mode 100644 index 0000000000..07b6f6c4ff --- /dev/null +++ b/src/app/access-control/access-control-routes.ts @@ -0,0 +1,114 @@ +import { AbstractControl } from '@angular/forms'; +import { Route } from '@angular/router'; +import { + DYNAMIC_ERROR_MESSAGES_MATCHER, + DynamicErrorMessagesMatcher, +} from '@ng-dynamic-forms/core'; + +import { i18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver'; +import { groupAdministratorGuard } from '../core/data/feature-authorization/feature-authorization-guard/group-administrator.guard'; +import { siteAdministratorGuard } from '../core/data/feature-authorization/feature-authorization-guard/site-administrator.guard'; +import { + EPERSON_PATH, + GROUP_PATH, +} from './access-control-routing-paths'; +import { BulkAccessComponent } from './bulk-access/bulk-access.component'; +import { EPeopleRegistryComponent } from './epeople-registry/epeople-registry.component'; +import { EPersonFormComponent } from './epeople-registry/eperson-form/eperson-form.component'; +import { EPersonResolver } from './epeople-registry/eperson-resolver.service'; +import { GroupFormComponent } from './group-registry/group-form/group-form.component'; +import { groupPageGuard } from './group-registry/group-page.guard'; +import { GroupsRegistryComponent } from './group-registry/groups-registry.component'; + +/** + * Condition for displaying error messages on email form field + */ +export const ValidateEmailErrorStateMatcher: DynamicErrorMessagesMatcher = + (control: AbstractControl, model: any, hasFocus: boolean) => { + return ( control.touched && !hasFocus ) || ( control.errors?.emailTaken && hasFocus ); + }; + +const providers = [ + { + provide: DYNAMIC_ERROR_MESSAGES_MATCHER, + useValue: ValidateEmailErrorStateMatcher, + }, +]; +export const ROUTES: Route[] = [ + { + path: EPERSON_PATH, + component: EPeopleRegistryComponent, + resolve: { + breadcrumb: i18nBreadcrumbResolver, + }, + providers, + data: { title: 'admin.access-control.epeople.title', breadcrumbKey: 'admin.access-control.epeople' }, + canActivate: [siteAdministratorGuard], + }, + { + path: `${EPERSON_PATH}/create`, + component: EPersonFormComponent, + resolve: { + breadcrumb: i18nBreadcrumbResolver, + }, + providers, + data: { title: 'admin.access-control.epeople.add.title', breadcrumbKey: 'admin.access-control.epeople.add' }, + canActivate: [siteAdministratorGuard], + }, + { + path: `${EPERSON_PATH}/:id/edit`, + component: EPersonFormComponent, + resolve: { + breadcrumb: i18nBreadcrumbResolver, + ePerson: EPersonResolver, + }, + providers, + data: { title: 'admin.access-control.epeople.edit.title', breadcrumbKey: 'admin.access-control.epeople.edit' }, + canActivate: [siteAdministratorGuard], + }, + { + path: GROUP_PATH, + component: GroupsRegistryComponent, + resolve: { + breadcrumb: i18nBreadcrumbResolver, + }, + providers, + data: { title: 'admin.access-control.groups.title', breadcrumbKey: 'admin.access-control.groups' }, + canActivate: [groupAdministratorGuard], + }, + { + path: `${GROUP_PATH}/create`, + component: GroupFormComponent, + resolve: { + breadcrumb: i18nBreadcrumbResolver, + }, + providers, + data: { + title: 'admin.access-control.groups.title.addGroup', + breadcrumbKey: 'admin.access-control.groups.addGroup', + }, + canActivate: [groupAdministratorGuard], + }, + { + path: `${GROUP_PATH}/:groupId/edit`, + component: GroupFormComponent, + resolve: { + breadcrumb: i18nBreadcrumbResolver, + }, + providers, + data: { + title: 'admin.access-control.groups.title.singleGroup', + breadcrumbKey: 'admin.access-control.groups.singleGroup', + }, + canActivate: [groupPageGuard], + }, + { + path: 'bulk-access', + component: BulkAccessComponent, + resolve: { + breadcrumb: i18nBreadcrumbResolver, + }, + data: { title: 'admin.access-control.bulk-access.title', breadcrumbKey: 'admin.access-control.bulk-access' }, + canActivate: [siteAdministratorGuard], + }, +]; diff --git a/src/app/access-control/access-control-routing.module.ts b/src/app/access-control/access-control-routing.module.ts deleted file mode 100644 index e85961fd13..0000000000 --- a/src/app/access-control/access-control-routing.module.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { NgModule } from '@angular/core'; -import { RouterModule } from '@angular/router'; - -import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver'; -import { GroupAdministratorGuard } from '../core/data/feature-authorization/feature-authorization-guard/group-administrator.guard'; -import { SiteAdministratorGuard } from '../core/data/feature-authorization/feature-authorization-guard/site-administrator.guard'; -import { - EPERSON_PATH, - GROUP_PATH, -} from './access-control-routing-paths'; -import { BulkAccessComponent } from './bulk-access/bulk-access.component'; -import { EPeopleRegistryComponent } from './epeople-registry/epeople-registry.component'; -import { EPersonFormComponent } from './epeople-registry/eperson-form/eperson-form.component'; -import { EPersonResolver } from './epeople-registry/eperson-resolver.service'; -import { GroupFormComponent } from './group-registry/group-form/group-form.component'; -import { GroupPageGuard } from './group-registry/group-page.guard'; -import { GroupsRegistryComponent } from './group-registry/groups-registry.component'; - -@NgModule({ - imports: [ - RouterModule.forChild([ - { - path: EPERSON_PATH, - component: EPeopleRegistryComponent, - resolve: { - breadcrumb: I18nBreadcrumbResolver, - }, - data: { title: 'admin.access-control.epeople.title', breadcrumbKey: 'admin.access-control.epeople' }, - canActivate: [SiteAdministratorGuard], - }, - { - path: `${EPERSON_PATH}/create`, - component: EPersonFormComponent, - resolve: { - breadcrumb: I18nBreadcrumbResolver, - }, - data: { title: 'admin.access-control.epeople.add.title', breadcrumbKey: 'admin.access-control.epeople.add' }, - canActivate: [SiteAdministratorGuard], - }, - { - path: `${EPERSON_PATH}/:id/edit`, - component: EPersonFormComponent, - resolve: { - breadcrumb: I18nBreadcrumbResolver, - ePerson: EPersonResolver, - }, - data: { title: 'admin.access-control.epeople.edit.title', breadcrumbKey: 'admin.access-control.epeople.edit' }, - canActivate: [SiteAdministratorGuard], - }, - { - path: GROUP_PATH, - component: GroupsRegistryComponent, - resolve: { - breadcrumb: I18nBreadcrumbResolver, - }, - data: { title: 'admin.access-control.groups.title', breadcrumbKey: 'admin.access-control.groups' }, - canActivate: [GroupAdministratorGuard], - }, - { - path: `${GROUP_PATH}/create`, - component: GroupFormComponent, - resolve: { - breadcrumb: I18nBreadcrumbResolver, - }, - data: { title: 'admin.access-control.groups.title.addGroup', breadcrumbKey: 'admin.access-control.groups.addGroup' }, - canActivate: [GroupAdministratorGuard], - }, - { - path: `${GROUP_PATH}/:groupId/edit`, - component: GroupFormComponent, - resolve: { - breadcrumb: I18nBreadcrumbResolver, - }, - data: { title: 'admin.access-control.groups.title.singleGroup', breadcrumbKey: 'admin.access-control.groups.singleGroup' }, - canActivate: [GroupPageGuard], - }, - { - path: 'bulk-access', - component: BulkAccessComponent, - resolve: { - breadcrumb: I18nBreadcrumbResolver, - }, - data: { title: 'admin.access-control.bulk-access.title', breadcrumbKey: 'admin.access-control.bulk-access' }, - canActivate: [SiteAdministratorGuard], - }, - ]), - ], -}) -/** - * Routing module for the AccessControl section of the admin sidebar - */ -export class AccessControlRoutingModule { - -} diff --git a/src/app/access-control/access-control.module.ts b/src/app/access-control/access-control.module.ts deleted file mode 100644 index 87737987e0..0000000000 --- a/src/app/access-control/access-control.module.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { CommonModule } from '@angular/common'; -import { NgModule } from '@angular/core'; -import { AbstractControl } from '@angular/forms'; -import { RouterModule } from '@angular/router'; -import { NgbAccordionModule } from '@ng-bootstrap/ng-bootstrap'; -import { - DYNAMIC_ERROR_MESSAGES_MATCHER, - DynamicErrorMessagesMatcher, -} from '@ng-dynamic-forms/core'; - -import { AccessControlFormModule } from '../shared/access-control-form-container/access-control-form.module'; -import { FormModule } from '../shared/form/form.module'; -import { SearchModule } from '../shared/search/search.module'; -import { SharedModule } from '../shared/shared.module'; -import { AccessControlRoutingModule } from './access-control-routing.module'; -import { BulkAccessBrowseComponent } from './bulk-access/browse/bulk-access-browse.component'; -import { BulkAccessComponent } from './bulk-access/bulk-access.component'; -import { BulkAccessSettingsComponent } from './bulk-access/settings/bulk-access-settings.component'; -import { EPeopleRegistryComponent } from './epeople-registry/epeople-registry.component'; -import { EPersonFormComponent } from './epeople-registry/eperson-form/eperson-form.component'; -import { GroupFormComponent } from './group-registry/group-form/group-form.component'; -import { MembersListComponent } from './group-registry/group-form/members-list/members-list.component'; -import { SubgroupsListComponent } from './group-registry/group-form/subgroup-list/subgroups-list.component'; -import { GroupsRegistryComponent } from './group-registry/groups-registry.component'; - -/** - * Condition for displaying error messages on email form field - */ -export const ValidateEmailErrorStateMatcher: DynamicErrorMessagesMatcher = - (control: AbstractControl, model: any, hasFocus: boolean) => { - return (control.touched && !hasFocus) || (control.errors?.emailTaken && hasFocus); - }; - -@NgModule({ - imports: [ - CommonModule, - SharedModule, - RouterModule, - AccessControlRoutingModule, - FormModule, - NgbAccordionModule, - SearchModule, - AccessControlFormModule, - ], - exports: [ - MembersListComponent, - ], - declarations: [ - EPeopleRegistryComponent, - EPersonFormComponent, - GroupsRegistryComponent, - GroupFormComponent, - SubgroupsListComponent, - MembersListComponent, - BulkAccessComponent, - BulkAccessBrowseComponent, - BulkAccessSettingsComponent, - ], - providers: [ - { - provide: DYNAMIC_ERROR_MESSAGES_MATCHER, - useValue: ValidateEmailErrorStateMatcher, - }, - ], -}) -/** - * This module handles all components related to the access control pages - */ -export class AccessControlModule { - -} diff --git a/src/app/access-control/bulk-access/browse/bulk-access-browse.component.html b/src/app/access-control/bulk-access/browse/bulk-access-browse.component.html index 6e967b53b5..f96ddf4a23 100644 --- a/src/app/access-control/bulk-access/browse/bulk-access-browse.component.html +++ b/src/app/access-control/bulk-access/browse/bulk-access-browse.component.html @@ -23,10 +23,10 @@ {{'admin.access-control.bulk-access-browse.search.header' | translate}}
- + [showThumbnails]="false">
@@ -37,7 +37,6 @@ { @@ -29,7 +35,7 @@ describe('BulkAccessBrowseComponent', () => { const selected1 = new SelectableObject(value1); const selected2 = new SelectableObject(value2); - const testSelection = { id: listID1, selection: [selected1, selected2] } ; + const testSelection = { id: listID1, selection: [selected1, selected2] }; const selectableListService = jasmine.createSpyObj('SelectableListService', ['getSelectableList', 'deselectAll']); beforeEach(waitForAsync(() => { @@ -38,13 +44,27 @@ describe('BulkAccessBrowseComponent', () => { NgbAccordionModule, NgbNavModule, TranslateModule.forRoot(), + BulkAccessBrowseComponent, + ], + providers: [ + { provide: SelectableListService, useValue: selectableListService }, + { provide: ThemeService, useValue: getMockThemeService() }, ], - declarations: [BulkAccessBrowseComponent], - providers: [ { provide: SelectableListService, useValue: selectableListService } ], schemas: [ NO_ERRORS_SCHEMA, ], - }).compileComponents(); + }) + .overrideComponent(BulkAccessBrowseComponent, { + remove: { + imports: [ + PaginationComponent, + ThemedSearchComponent, + SelectableListItemControlComponent, + ListableObjectComponentLoaderComponent, + ], + }, + }) + .compileComponents(); })); beforeEach(() => { @@ -79,7 +99,7 @@ describe('BulkAccessBrowseComponent', () => { 'totalElements': 2, 'totalPages': 1, 'currentPage': 1, - }), [selected1, selected2]) ; + }), [selected1, selected2]); const rd = createSuccessfulRemoteDataObject(list); expect(component.objectsSelected$.value).toEqual(rd); diff --git a/src/app/access-control/bulk-access/browse/bulk-access-browse.component.ts b/src/app/access-control/bulk-access/browse/bulk-access-browse.component.ts index 6b221f107e..a400742f01 100644 --- a/src/app/access-control/bulk-access/browse/bulk-access-browse.component.ts +++ b/src/app/access-control/bulk-access/browse/bulk-access-browse.component.ts @@ -1,9 +1,20 @@ +import { + AsyncPipe, + NgForOf, + NgIf, +} from '@angular/common'; import { Component, Input, OnDestroy, OnInit, } from '@angular/core'; +import { + NgbAccordionModule, + NgbNavModule, +} from '@ng-bootstrap/ng-bootstrap'; +import { TranslateModule } from '@ngx-translate/core'; +import { NgxPaginationModule } from 'ngx-pagination'; import { BehaviorSubject, Subscription, @@ -20,13 +31,18 @@ import { import { RemoteData } from '../../../core/data/remote-data'; import { PageInfo } from '../../../core/shared/page-info.model'; import { SearchConfigurationService } from '../../../core/shared/search/search-configuration.service'; -import { SEARCH_CONFIG_SERVICE } from '../../../my-dspace-page/my-dspace-page.component'; +import { SEARCH_CONFIG_SERVICE } from '../../../my-dspace-page/my-dspace-configuration.service'; import { hasValue } from '../../../shared/empty.util'; import { ListableObject } from '../../../shared/object-collection/shared/listable-object.model'; +import { ListableObjectComponentLoaderComponent } from '../../../shared/object-collection/shared/listable-object/listable-object-component-loader.component'; +import { SelectableListItemControlComponent } from '../../../shared/object-collection/shared/selectable-list-item-control/selectable-list-item-control.component'; import { SelectableListState } from '../../../shared/object-list/selectable-list/selectable-list.reducer'; import { SelectableListService } from '../../../shared/object-list/selectable-list/selectable-list.service'; +import { PaginationComponent } from '../../../shared/pagination/pagination.component'; import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model'; import { createSuccessfulRemoteDataObject } from '../../../shared/remote-data.utils'; +import { ThemedSearchComponent } from '../../../shared/search/themed-search.component'; +import { BrowserOnlyPipe } from '../../../shared/utils/browser-only.pipe'; @Component({ selector: 'ds-bulk-access-browse', @@ -38,6 +54,21 @@ import { createSuccessfulRemoteDataObject } from '../../../shared/remote-data.ut useClass: SearchConfigurationService, }, ], + imports: [ + PaginationComponent, + AsyncPipe, + NgbAccordionModule, + TranslateModule, + NgIf, + NgbNavModule, + ThemedSearchComponent, + BrowserOnlyPipe, + NgForOf, + NgxPaginationModule, + SelectableListItemControlComponent, + ListableObjectComponentLoaderComponent, + ], + standalone: true, }) export class BulkAccessBrowseComponent implements OnInit, OnDestroy { diff --git a/src/app/access-control/bulk-access/bulk-access.component.spec.ts b/src/app/access-control/bulk-access/bulk-access.component.spec.ts index e7ec28c132..8bfbe1fe5d 100644 --- a/src/app/access-control/bulk-access/bulk-access.component.spec.ts +++ b/src/app/access-control/bulk-access/bulk-access.component.spec.ts @@ -9,12 +9,15 @@ import { of } from 'rxjs'; import { Process } from '../../process-page/processes/process.model'; import { BulkAccessControlService } from '../../shared/access-control-form-container/bulk-access-control.service'; +import { getMockThemeService } from '../../shared/mocks/theme-service.mock'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { SelectableListState } from '../../shared/object-list/selectable-list/selectable-list.reducer'; import { SelectableListService } from '../../shared/object-list/selectable-list/selectable-list.service'; import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub'; +import { ThemeService } from '../../shared/theme-support/theme.service'; import { BulkAccessComponent } from './bulk-access.component'; +import { BulkAccessSettingsComponent } from './settings/bulk-access-settings.component'; describe('BulkAccessComponent', () => { let component: BulkAccessComponent; @@ -74,15 +77,23 @@ describe('BulkAccessComponent', () => { imports: [ RouterTestingModule, TranslateModule.forRoot(), + BulkAccessComponent, ], - declarations: [ BulkAccessComponent ], providers: [ { provide: BulkAccessControlService, useValue: bulkAccessControlServiceMock }, { provide: NotificationsService, useValue: NotificationsServiceStub }, { provide: SelectableListService, useValue: selectableListServiceMock }, + { provide: ThemeService, useValue: getMockThemeService() }, ], schemas: [NO_ERRORS_SCHEMA], }) + .overrideComponent(BulkAccessComponent, { + remove: { + imports: [ + BulkAccessSettingsComponent, + ], + }, + }) .compileComponents(); }); diff --git a/src/app/access-control/bulk-access/bulk-access.component.ts b/src/app/access-control/bulk-access/bulk-access.component.ts index 52faa97dbc..bd8e893b59 100644 --- a/src/app/access-control/bulk-access/bulk-access.component.ts +++ b/src/app/access-control/bulk-access/bulk-access.component.ts @@ -3,6 +3,7 @@ import { OnInit, ViewChild, } from '@angular/core'; +import { TranslateModule } from '@ngx-translate/core'; import { BehaviorSubject, Subscription, @@ -15,12 +16,19 @@ import { import { BulkAccessControlService } from '../../shared/access-control-form-container/bulk-access-control.service'; import { SelectableListState } from '../../shared/object-list/selectable-list/selectable-list.reducer'; import { SelectableListService } from '../../shared/object-list/selectable-list/selectable-list.service'; +import { BulkAccessBrowseComponent } from './browse/bulk-access-browse.component'; import { BulkAccessSettingsComponent } from './settings/bulk-access-settings.component'; @Component({ selector: 'ds-bulk-access', templateUrl: './bulk-access.component.html', styleUrls: ['./bulk-access.component.scss'], + imports: [ + TranslateModule, + BulkAccessSettingsComponent, + BulkAccessBrowseComponent, + ], + standalone: true, }) export class BulkAccessComponent implements OnInit { diff --git a/src/app/access-control/bulk-access/settings/bulk-access-settings.component.spec.ts b/src/app/access-control/bulk-access/settings/bulk-access-settings.component.spec.ts index f3c1cad04a..880e1f2472 100644 --- a/src/app/access-control/bulk-access/settings/bulk-access-settings.component.spec.ts +++ b/src/app/access-control/bulk-access/settings/bulk-access-settings.component.spec.ts @@ -6,6 +6,7 @@ import { import { NgbAccordionModule } from '@ng-bootstrap/ng-bootstrap'; import { TranslateModule } from '@ngx-translate/core'; +import { AccessControlFormContainerComponent } from '../../../shared/access-control-form-container/access-control-form-container.component'; import { BulkAccessSettingsComponent } from './bulk-access-settings.component'; describe('BulkAccessSettingsComponent', () => { @@ -45,10 +46,13 @@ describe('BulkAccessSettingsComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [NgbAccordionModule, TranslateModule.forRoot()], - declarations: [BulkAccessSettingsComponent], + imports: [NgbAccordionModule, TranslateModule.forRoot(), BulkAccessSettingsComponent], schemas: [NO_ERRORS_SCHEMA], - }).compileComponents(); + }) + .overrideComponent(BulkAccessSettingsComponent, { + remove: { imports: [AccessControlFormContainerComponent] }, + }) + .compileComponents(); }); beforeEach(() => { diff --git a/src/app/access-control/bulk-access/settings/bulk-access-settings.component.ts b/src/app/access-control/bulk-access/settings/bulk-access-settings.component.ts index d0f95377eb..264cefc708 100644 --- a/src/app/access-control/bulk-access/settings/bulk-access-settings.component.ts +++ b/src/app/access-control/bulk-access/settings/bulk-access-settings.component.ts @@ -1,7 +1,10 @@ +import { NgIf } from '@angular/common'; import { Component, ViewChild, } from '@angular/core'; +import { NgbAccordionModule } from '@ng-bootstrap/ng-bootstrap'; +import { TranslateModule } from '@ngx-translate/core'; import { AccessControlFormContainerComponent } from '../../../shared/access-control-form-container/access-control-form-container.component'; @@ -10,6 +13,13 @@ import { AccessControlFormContainerComponent } from '../../../shared/access-cont templateUrl: 'bulk-access-settings.component.html', styleUrls: ['./bulk-access-settings.component.scss'], exportAs: 'dsBulkSettings', + imports: [ + NgbAccordionModule, + TranslateModule, + NgIf, + AccessControlFormContainerComponent, + ], + standalone: true, }) export class BulkAccessSettingsComponent { diff --git a/src/app/access-control/epeople-registry/epeople-registry.component.html b/src/app/access-control/epeople-registry/epeople-registry.component.html index 92968d2e28..b5a26533cf 100644 --- a/src/app/access-control/epeople-registry/epeople-registry.component.html +++ b/src/app/access-control/epeople-registry/epeople-registry.component.html @@ -41,11 +41,10 @@ - + @@ -62,7 +61,7 @@ + [ngClass]="{'table-primary' : (activeEPerson$ | async) === epersonDto.eperson}"> {{epersonDto.eperson.id}} {{ dsoNameService.getName(epersonDto.eperson) }} {{epersonDto.eperson.email}} diff --git a/src/app/access-control/epeople-registry/epeople-registry.component.spec.ts b/src/app/access-control/epeople-registry/epeople-registry.component.spec.ts index ee4d5fa603..c636b72d56 100644 --- a/src/app/access-control/epeople-registry/epeople-registry.component.spec.ts +++ b/src/app/access-control/epeople-registry/epeople-registry.component.spec.ts @@ -19,6 +19,7 @@ import { By, } from '@angular/platform-browser'; import { Router } from '@angular/router'; +import { RouterTestingModule } from '@angular/router/testing'; import { NgbModal, NgbModule, @@ -42,8 +43,11 @@ import { EPerson } from '../../core/eperson/models/eperson.model'; import { PaginationService } from '../../core/pagination/pagination.service'; import { PageInfo } from '../../core/shared/page-info.model'; import { FormBuilderService } from '../../shared/form/builder/form-builder.service'; +import { ThemedLoadingComponent } from '../../shared/loading/themed-loading.component'; import { getMockFormBuilderService } from '../../shared/mocks/form-builder-service.mock'; +import { RouterMock } from '../../shared/mocks/router.mock'; import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { PaginationComponent } from '../../shared/pagination/pagination.component'; import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; import { EPersonMock, @@ -51,8 +55,8 @@ import { } from '../../shared/testing/eperson.mock'; import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub'; import { PaginationServiceStub } from '../../shared/testing/pagination-service.stub'; -import { RouterStub } from '../../shared/testing/router.stub'; import { EPeopleRegistryComponent } from './epeople-registry.component'; +import { EPersonFormComponent } from './eperson-form/eperson-form.component'; describe('EPeopleRegistryComponent', () => { let component: EPeopleRegistryComponent; @@ -145,22 +149,30 @@ describe('EPeopleRegistryComponent', () => { builderService = getMockFormBuilderService(); paginationService = new PaginationServiceStub(); - await TestBed.configureTestingModule({ - imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule, - TranslateModule.forRoot(), - ], - declarations: [EPeopleRegistryComponent], + TestBed.configureTestingModule({ + imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule, RouterTestingModule.withRoutes([]), + TranslateModule.forRoot(), EPeopleRegistryComponent], providers: [ { provide: EPersonDataService, useValue: ePersonDataServiceStub }, { provide: NotificationsService, useValue: new NotificationsServiceStub() }, { provide: AuthorizationDataService, useValue: authorizationService }, { provide: FormBuilderService, useValue: builderService }, - { provide: Router, useValue: new RouterStub() }, + { provide: Router, useValue: new RouterMock() }, { provide: RequestService, useValue: jasmine.createSpyObj('requestService', ['removeByHrefSubstring']) }, { provide: PaginationService, useValue: paginationService }, ], schemas: [NO_ERRORS_SCHEMA], - }).compileComponents(); + }) + .overrideComponent(EPeopleRegistryComponent, { + remove: { + imports: [ + EPersonFormComponent, + ThemedLoadingComponent, + PaginationComponent, + ], + }, + }) + .compileComponents(); })); beforeEach(() => { diff --git a/src/app/access-control/epeople-registry/epeople-registry.component.ts b/src/app/access-control/epeople-registry/epeople-registry.component.ts index ddf5fe7bfb..6b62a13ecf 100644 --- a/src/app/access-control/epeople-registry/epeople-registry.component.ts +++ b/src/app/access-control/epeople-registry/epeople-registry.component.ts @@ -1,12 +1,27 @@ +import { + AsyncPipe, + NgClass, + NgForOf, + NgIf, +} from '@angular/common'; import { Component, OnDestroy, OnInit, } from '@angular/core'; -import { UntypedFormBuilder } from '@angular/forms'; -import { Router } from '@angular/router'; +import { + ReactiveFormsModule, + UntypedFormBuilder, +} from '@angular/forms'; +import { + Router, + RouterModule, +} from '@angular/router'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { TranslateService } from '@ngx-translate/core'; +import { + TranslateModule, + TranslateService, +} from '@ngx-translate/core'; import { BehaviorSubject, combineLatest, @@ -40,16 +55,32 @@ import { import { PageInfo } from '../../core/shared/page-info.model'; import { ConfirmationModalComponent } from '../../shared/confirmation-modal/confirmation-modal.component'; import { hasValue } from '../../shared/empty.util'; +import { ThemedLoadingComponent } from '../../shared/loading/themed-loading.component'; import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { PaginationComponent } from '../../shared/pagination/pagination.component'; import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; import { getEPersonEditRoute, getEPersonsRoute, } from '../access-control-routing-paths'; +import { EPersonFormComponent } from './eperson-form/eperson-form.component'; @Component({ selector: 'ds-epeople-registry', templateUrl: './epeople-registry.component.html', + imports: [ + TranslateModule, + RouterModule, + AsyncPipe, + NgIf, + EPersonFormComponent, + ReactiveFormsModule, + ThemedLoadingComponent, + PaginationComponent, + NgClass, + NgForOf, + ], + standalone: true, }) /** * A component used for managing all existing epeople within the repository. @@ -69,6 +100,8 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy { */ ePeopleDto$: BehaviorSubject> = new BehaviorSubject>({} as any); + activeEPerson$: Observable; + /** * An observable for the pageInfo, needed to pass to the pagination component */ @@ -134,6 +167,7 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy { initialisePage() { this.searching$.next(true); this.search({ scope: this.currentSearchScope, query: this.currentSearchQuery }); + this.activeEPerson$ = this.epersonService.getActiveEPerson(); this.subs.push(this.ePeople$.pipe( switchMap((epeople: PaginatedList) => { if (epeople.pageInfo.totalElements > 0) { @@ -201,23 +235,6 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy { ); } - /** - * Checks whether the given EPerson is active (being edited) - * @param eperson - */ - isActive(eperson: EPerson): Observable { - return this.getActiveEPerson().pipe( - map((activeEPerson) => eperson === activeEPerson), - ); - } - - /** - * Gets the active eperson (being edited) - */ - getActiveEPerson(): Observable { - return this.epersonService.getActiveEPerson(); - } - /** * Deletes EPerson, show notification on success/failure & updates EPeople list */ diff --git a/src/app/access-control/epeople-registry/eperson-form/eperson-form.component.html b/src/app/access-control/epeople-registry/eperson-form/eperson-form.component.html index 9168bbaf8e..ae1046be45 100644 --- a/src/app/access-control/epeople-registry/eperson-form/eperson-form.component.html +++ b/src/app/access-control/epeople-registry/eperson-form/eperson-form.component.html @@ -2,7 +2,7 @@
-
+

{{messagePrefix + '.create' | translate}}

@@ -42,17 +42,16 @@ - + -
+

{{messagePrefix + '.groupsEPersonIsMemberOf' | translate}}

- + - {{ dsoNameService.getName(undefined) }} + + {{ dsoNameService.getName((group.object | async)?.payload) }} + diff --git a/src/app/access-control/epeople-registry/eperson-form/eperson-form.component.spec.ts b/src/app/access-control/epeople-registry/eperson-form/eperson-form.component.spec.ts index 5196eee631..e61f95d6e5 100644 --- a/src/app/access-control/epeople-registry/eperson-form/eperson-form.component.spec.ts +++ b/src/app/access-control/epeople-registry/eperson-form/eperson-form.component.spec.ts @@ -1,5 +1,5 @@ import { CommonModule } from '@angular/common'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; import { ComponentFixture, TestBed, @@ -19,12 +19,10 @@ import { import { ActivatedRoute, Router, + RouterModule, } from '@angular/router'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; -import { - TranslateLoader, - TranslateModule, -} from '@ngx-translate/core'; +import { TranslateModule } from '@ngx-translate/core'; import { Observable, of as observableOf, @@ -46,9 +44,11 @@ import { EPerson } from '../../../core/eperson/models/eperson.model'; import { PaginationService } from '../../../core/pagination/pagination.service'; import { PageInfo } from '../../../core/shared/page-info.model'; import { FormBuilderService } from '../../../shared/form/builder/form-builder.service'; +import { FormComponent } from '../../../shared/form/form.component'; +import { ThemedLoadingComponent } from '../../../shared/loading/themed-loading.component'; import { getMockFormBuilderService } from '../../../shared/mocks/form-builder-service.mock'; -import { TranslateLoaderMock } from '../../../shared/mocks/translate-loader.mock'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; +import { PaginationComponent } from '../../../shared/pagination/pagination.component'; import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; import { ActivatedRouteStub } from '../../../shared/testing/active-router.stub'; import { AuthServiceStub } from '../../../shared/testing/auth-service.stub'; @@ -89,9 +89,6 @@ describe('EPersonFormComponent', () => { ePersonDataServiceStub = { activeEPerson: null, allEpeople: mockEPeople, - getEPeople(): Observable>> { - return createSuccessfulRemoteDataObject$(buildPaginatedList(null, this.allEpeople)); - }, getActiveEPerson(): Observable { return observableOf(this.activeEPerson); }, @@ -225,14 +222,8 @@ describe('EPersonFormComponent', () => { router = new RouterStub(); TestBed.configureTestingModule({ imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule, - TranslateModule.forRoot({ - loader: { - provide: TranslateLoader, - useClass: TranslateLoaderMock, - }, - }), - ], - declarations: [ + RouterModule.forRoot([]), + TranslateModule.forRoot(), EPersonFormComponent, HasNoValuePipe, ], @@ -250,8 +241,12 @@ describe('EPersonFormComponent', () => { { provide: Router, useValue: router }, EPeopleRegistryComponent, ], - schemas: [NO_ERRORS_SCHEMA], - }).compileComponents(); + schemas: [CUSTOM_ELEMENTS_SCHEMA], + }) + .overrideComponent(EPersonFormComponent, { + remove: { imports: [ ThemedLoadingComponent, PaginationComponent,FormComponent] }, + }) + .compileComponents(); })); epersonRegistrationService = jasmine.createSpyObj('epersonRegistrationService', { @@ -269,37 +264,13 @@ describe('EPersonFormComponent', () => { }); describe('check form validation', () => { - let firstName; - let lastName; - let email; - let canLogIn; - let requireCertificate; + let canLogIn: boolean; + let requireCertificate: boolean; - let expected; beforeEach(() => { - firstName = 'testName'; - lastName = 'testLastName'; - email = 'testEmail@test.com'; canLogIn = false; requireCertificate = false; - expected = Object.assign(new EPerson(), { - metadata: { - 'eperson.firstname': [ - { - value: firstName, - }, - ], - 'eperson.lastname': [ - { - value: lastName, - }, - ], - }, - email: email, - canLogIn: canLogIn, - requireCertificate: requireCertificate, - }); spyOn(component.submitForm, 'emit'); component.canLogIn.value = canLogIn; component.requireCertificate.value = requireCertificate; @@ -373,15 +344,13 @@ describe('EPersonFormComponent', () => { expect(component.formGroup.controls.email.errors.emailTaken).toBeTruthy(); }); }); - - - }); + describe('when submitting the form', () => { let firstName; let lastName; let email; - let canLogIn; + let canLogIn: boolean; let requireCertificate; let expected; @@ -410,6 +379,7 @@ describe('EPersonFormComponent', () => { requireCertificate: requireCertificate, }); spyOn(component.submitForm, 'emit'); + component.ngOnInit(); component.firstName.value = firstName; component.lastName.value = lastName; component.email.value = email; @@ -449,9 +419,17 @@ describe('EPersonFormComponent', () => { email: email, canLogIn: canLogIn, requireCertificate: requireCertificate, - _links: undefined, + _links: { + groups: { + href: '', + }, + self: { + href: '', + }, + }, }); spyOn(ePersonDataServiceStub, 'getActiveEPerson').and.returnValue(observableOf(expectedWithId)); + component.ngOnInit(); component.onSubmit(); fixture.detectChanges(); }); @@ -499,22 +477,19 @@ describe('EPersonFormComponent', () => { }); describe('delete', () => { - - let ePersonId; let eperson: EPerson; let modalService; beforeEach(() => { spyOn(authService, 'impersonate').and.callThrough(); - ePersonId = 'testEPersonId'; eperson = EPersonMock; component.epersonInitial = eperson; component.canDelete$ = observableOf(true); spyOn(component.epersonService, 'getActiveEPerson').and.returnValue(observableOf(eperson)); modalService = (component as any).modalService; spyOn(modalService, 'open').and.returnValue(Object.assign({ componentInstance: Object.assign({ response: observableOf(true) }) })); + component.ngOnInit(); fixture.detectChanges(); - }); it('the delete button should be visible if the ePerson can be deleted', () => { diff --git a/src/app/access-control/epeople-registry/eperson-form/eperson-form.component.ts b/src/app/access-control/epeople-registry/eperson-form/eperson-form.component.ts index 7eb743f1ab..942481c4fd 100644 --- a/src/app/access-control/epeople-registry/eperson-form/eperson-form.component.ts +++ b/src/app/access-control/epeople-registry/eperson-form/eperson-form.component.ts @@ -1,3 +1,9 @@ +import { + AsyncPipe, + NgClass, + NgFor, + NgIf, +} from '@angular/common'; import { ChangeDetectorRef, Component, @@ -10,6 +16,7 @@ import { UntypedFormGroup } from '@angular/forms'; import { ActivatedRoute, Router, + RouterLink, } from '@angular/router'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { @@ -18,7 +25,10 @@ import { DynamicFormLayout, DynamicInputModel, } from '@ng-dynamic-forms/core'; -import { TranslateService } from '@ngx-translate/core'; +import { + TranslateModule, + TranslateService, +} from '@ngx-translate/core'; import { combineLatest as observableCombineLatest, Observable, @@ -58,15 +68,32 @@ import { TYPE_REQUEST_FORGOT } from '../../../register-email-form/register-email import { ConfirmationModalComponent } from '../../../shared/confirmation-modal/confirmation-modal.component'; import { hasValue } from '../../../shared/empty.util'; import { FormBuilderService } from '../../../shared/form/builder/form-builder.service'; +import { FormComponent } from '../../../shared/form/form.component'; +import { ThemedLoadingComponent } from '../../../shared/loading/themed-loading.component'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; +import { PaginationComponent } from '../../../shared/pagination/pagination.component'; import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model'; import { followLink } from '../../../shared/utils/follow-link-config.model'; +import { HasNoValuePipe } from '../../../shared/utils/has-no-value.pipe'; import { getEPersonsRoute } from '../../access-control-routing-paths'; import { ValidateEmailNotTaken } from './validators/email-taken.validator'; @Component({ selector: 'ds-eperson-form', templateUrl: './eperson-form.component.html', + imports: [ + FormComponent, + NgIf, + NgFor, + AsyncPipe, + TranslateModule, + NgClass, + ThemedLoadingComponent, + PaginationComponent, + RouterLink, + HasNoValuePipe, + ], + standalone: true, }) /** * A form used for creating and editing EPeople @@ -162,6 +189,11 @@ export class EPersonFormComponent implements OnInit, OnDestroy { */ canImpersonate$: Observable; + /** + * The current {@link EPerson} + */ + activeEPerson$: Observable; + /** * List of subscriptions */ @@ -227,7 +259,11 @@ export class EPersonFormComponent implements OnInit, OnDestroy { protected route: ActivatedRoute, protected router: Router, ) { - this.subs.push(this.epersonService.getActiveEPerson().subscribe((eperson: EPerson) => { + } + + ngOnInit() { + this.activeEPerson$ = this.epersonService.getActiveEPerson(); + this.subs.push(this.activeEPerson$.subscribe((eperson: EPerson) => { this.epersonInitial = eperson; if (hasValue(eperson)) { this.isImpersonated = this.authService.isImpersonatingUser(eperson.id); @@ -235,9 +271,6 @@ export class EPersonFormComponent implements OnInit, OnDestroy { this.submitLabel = 'form.submit'; } })); - } - - ngOnInit() { this.initialisePage(); } @@ -245,130 +278,121 @@ export class EPersonFormComponent implements OnInit, OnDestroy { * This method will initialise the page */ initialisePage() { - this.subs.push(this.epersonService.findById(this.route.snapshot.params.id).subscribe((ePersonRD: RemoteData) => { - this.epersonService.editEPerson(ePersonRD.payload); - })); - observableCombineLatest([ - this.translateService.get(`${this.messagePrefix}.firstName`), - this.translateService.get(`${this.messagePrefix}.lastName`), - this.translateService.get(`${this.messagePrefix}.email`), - this.translateService.get(`${this.messagePrefix}.canLogIn`), - this.translateService.get(`${this.messagePrefix}.requireCertificate`), - this.translateService.get(`${this.messagePrefix}.emailHint`), - ]).subscribe(([firstName, lastName, email, canLogIn, requireCertificate, emailHint]) => { - this.firstName = new DynamicInputModel({ - id: 'firstName', - label: firstName, - name: 'firstName', - validators: { - required: null, - }, - required: true, - }); - this.lastName = new DynamicInputModel({ - id: 'lastName', - label: lastName, - name: 'lastName', - validators: { - required: null, - }, - required: true, - }); - this.email = new DynamicInputModel({ - id: 'email', - label: email, - name: 'email', - validators: { - required: null, - pattern: '^[a-z0-9._%+-]+@[a-z0-9.-]+\\.[a-z]{2,4}$', - }, - required: true, - errorMessages: { - emailTaken: 'error.validation.emailTaken', - pattern: 'error.validation.NotValidEmail', - }, - hint: emailHint, - }); - this.canLogIn = new DynamicCheckboxModel( - { - id: 'canLogIn', - label: canLogIn, - name: 'canLogIn', - value: (this.epersonInitial != null ? this.epersonInitial.canLogIn : true), - }); - this.requireCertificate = new DynamicCheckboxModel( - { - id: 'requireCertificate', - label: requireCertificate, - name: 'requireCertificate', - value: (this.epersonInitial != null ? this.epersonInitial.requireCertificate : false), - }); - this.formModel = [ - this.firstName, - this.lastName, - this.email, - this.canLogIn, - this.requireCertificate, - ]; - this.formGroup = this.formBuilderService.createFormGroup(this.formModel); - this.subs.push(this.epersonService.getActiveEPerson().subscribe((eperson: EPerson) => { - if (eperson != null) { - this.groups$ = this.groupsDataService.findListByHref(eperson._links.groups.href, { - currentPage: 1, - elementsPerPage: this.config.pageSize, - }); - } - this.formGroup.patchValue({ - firstName: eperson != null ? eperson.firstMetadataValue('eperson.firstname') : '', - lastName: eperson != null ? eperson.firstMetadataValue('eperson.lastname') : '', - email: eperson != null ? eperson.email : '', - canLogIn: eperson != null ? eperson.canLogIn : true, - requireCertificate: eperson != null ? eperson.requireCertificate : false, - }); - - if (eperson === null && !!this.formGroup.controls.email) { - this.formGroup.controls.email.setAsyncValidators(ValidateEmailNotTaken.createValidator(this.epersonService)); - this.emailValueChangeSubscribe = this.email.valueChanges.pipe(debounceTime(300)).subscribe(() => { - this.changeDetectorRef.detectChanges(); - }); - } + if (this.route.snapshot.params.id) { + this.subs.push(this.epersonService.findById(this.route.snapshot.params.id).subscribe((ePersonRD: RemoteData) => { + this.epersonService.editEPerson(ePersonRD.payload); })); - - const activeEPerson$ = this.epersonService.getActiveEPerson(); - - this.groups$ = activeEPerson$.pipe( - switchMap((eperson) => { - return observableCombineLatest([observableOf(eperson), this.paginationService.getFindListOptions(this.config.id, { - currentPage: 1, - elementsPerPage: this.config.pageSize, - })]); - }), - switchMap(([eperson, findListOptions]) => { - if (eperson != null) { - return this.groupsDataService.findListByHref(eperson._links.groups.href, findListOptions, true, true, followLink('object')); - } - return observableOf(undefined); - }), - ); - - this.groupsPageInfoState$ = this.groups$.pipe( - map(groupsRD => groupsRD.payload.pageInfo), - ); - - this.canImpersonate$ = activeEPerson$.pipe( - switchMap((eperson) => { - if (hasValue(eperson)) { - return this.authorizationService.isAuthorized(FeatureID.LoginOnBehalfOf, eperson.self); - } else { - return observableOf(false); - } - }), - ); - this.canDelete$ = activeEPerson$.pipe( - switchMap((eperson) => this.authorizationService.isAuthorized(FeatureID.CanDelete, hasValue(eperson) ? eperson.self : undefined)), - ); - this.canReset$ = observableOf(true); + } + this.firstName = new DynamicInputModel({ + id: 'firstName', + label: this.translateService.instant(`${this.messagePrefix}.firstName`), + name: 'firstName', + validators: { + required: null, + }, + required: true, }); + this.lastName = new DynamicInputModel({ + id: 'lastName', + label: this.translateService.instant(`${this.messagePrefix}.lastName`), + name: 'lastName', + validators: { + required: null, + }, + required: true, + }); + this.email = new DynamicInputModel({ + id: 'email', + label: this.translateService.instant(`${this.messagePrefix}.email`), + name: 'email', + validators: { + required: null, + pattern: '^[a-z0-9._%+-]+@[a-z0-9.-]+\\.[a-z]{2,4}$', + }, + required: true, + errorMessages: { + emailTaken: 'error.validation.emailTaken', + pattern: 'error.validation.NotValidEmail', + }, + hint: this.translateService.instant(`${this.messagePrefix}.emailHint`), + }); + this.canLogIn = new DynamicCheckboxModel( + { + id: 'canLogIn', + label: this.translateService.instant(`${this.messagePrefix}.canLogIn`), + name: 'canLogIn', + value: (this.epersonInitial != null ? this.epersonInitial.canLogIn : true), + }); + this.requireCertificate = new DynamicCheckboxModel( + { + id: 'requireCertificate', + label: this.translateService.instant(`${this.messagePrefix}.requireCertificate`), + name: 'requireCertificate', + value: (this.epersonInitial != null ? this.epersonInitial.requireCertificate : false), + }); + this.formModel = [ + this.firstName, + this.lastName, + this.email, + this.canLogIn, + this.requireCertificate, + ]; + this.formGroup = this.formBuilderService.createFormGroup(this.formModel); + this.subs.push(this.activeEPerson$.subscribe((eperson: EPerson) => { + if (eperson != null) { + this.groups$ = this.groupsDataService.findListByHref(eperson._links.groups.href, { + currentPage: 1, + elementsPerPage: this.config.pageSize, + }, undefined, undefined, followLink('object')); + } + this.formGroup.patchValue({ + firstName: eperson != null ? eperson.firstMetadataValue('eperson.firstname') : '', + lastName: eperson != null ? eperson.firstMetadataValue('eperson.lastname') : '', + email: eperson != null ? eperson.email : '', + canLogIn: eperson != null ? eperson.canLogIn : true, + requireCertificate: eperson != null ? eperson.requireCertificate : false, + }); + + if (eperson === null && !!this.formGroup.controls.email) { + this.formGroup.controls.email.setAsyncValidators(ValidateEmailNotTaken.createValidator(this.epersonService)); + this.emailValueChangeSubscribe = this.email.valueChanges.pipe(debounceTime(300)).subscribe(() => { + this.changeDetectorRef.detectChanges(); + }); + } + })); + + this.groups$ = this.activeEPerson$.pipe( + switchMap((eperson) => { + return observableCombineLatest([observableOf(eperson), this.paginationService.getFindListOptions(this.config.id, { + currentPage: 1, + elementsPerPage: this.config.pageSize, + })]); + }), + switchMap(([eperson, findListOptions]) => { + if (eperson != null) { + return this.groupsDataService.findListByHref(eperson._links.groups.href, findListOptions, true, true, followLink('object')); + } + return observableOf(undefined); + }), + ); + + this.groupsPageInfoState$ = this.groups$.pipe( + map(groupsRD => groupsRD.payload.pageInfo), + ); + + this.canImpersonate$ = this.activeEPerson$.pipe( + switchMap((eperson) => { + if (hasValue(eperson)) { + return this.authorizationService.isAuthorized(FeatureID.LoginOnBehalfOf, eperson.self); + } else { + return observableOf(false); + } + }), + ); + this.canDelete$ = this.activeEPerson$.pipe( + switchMap((eperson) => this.authorizationService.isAuthorized(FeatureID.CanDelete, hasValue(eperson) ? eperson.self : undefined)), + ); + this.canReset$ = observableOf(true); } /** @@ -387,7 +411,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy { * Emit the updated/created eperson using the EventEmitter submitForm */ onSubmit() { - this.epersonService.getActiveEPerson().pipe(take(1)).subscribe( + this.activeEPerson$.pipe(take(1)).subscribe( (ePerson: EPerson) => { const values = { metadata: { @@ -506,7 +530,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy { * It'll either show a success or error message depending on whether the delete was successful or not. */ delete(): void { - this.epersonService.getActiveEPerson().pipe( + this.activeEPerson$.pipe( take(1), switchMap((eperson: EPerson) => { const modalRef = this.modalService.open(ConfirmationModalComponent); @@ -610,7 +634,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy { * Update the list of groups by fetching it from the rest api or cache */ private updateGroups(options) { - this.subs.push(this.epersonService.getActiveEPerson().subscribe((eperson: EPerson) => { + this.subs.push(this.activeEPerson$.subscribe((eperson: EPerson) => { this.groups$ = this.groupsDataService.findListByHref(eperson._links.groups.href, options); })); } diff --git a/src/app/access-control/epeople-registry/eperson-resolver.service.ts b/src/app/access-control/epeople-registry/eperson-resolver.service.ts index 33233ba7e8..6c9d7347f7 100644 --- a/src/app/access-control/epeople-registry/eperson-resolver.service.ts +++ b/src/app/access-control/epeople-registry/eperson-resolver.service.ts @@ -1,7 +1,6 @@ import { Injectable } from '@angular/core'; import { ActivatedRouteSnapshot, - Resolve, RouterStateSnapshot, } from '@angular/router'; import { Store } from '@ngrx/store'; @@ -27,7 +26,7 @@ export const EPERSON_EDIT_FOLLOW_LINKS: FollowLinkConfig[] = [ @Injectable({ providedIn: 'root', }) -export class EPersonResolver implements Resolve> { +export class EPersonResolver { constructor( protected ePersonService: EPersonDataService, diff --git a/src/app/access-control/group-registry/group-form/group-form.component.html b/src/app/access-control/group-registry/group-form/group-form.component.html index dc477352e5..7e8c1ed1b4 100644 --- a/src/app/access-control/group-registry/group-form/group-form.component.html +++ b/src/app/access-control/group-registry/group-form/group-form.component.html @@ -2,7 +2,7 @@
-
+

{{messagePrefix + '.head.create' | translate}}

@@ -23,11 +23,15 @@
- - - + + + + + + + {{messagePrefix + '.return' | translate}}
-
+
-
- -
- - - - + +
+ +
+ +
diff --git a/src/app/access-control/group-registry/group-form/group-form.component.spec.ts b/src/app/access-control/group-registry/group-form/group-form.component.spec.ts index aca6da2a74..b7e6a35d4e 100644 --- a/src/app/access-control/group-registry/group-form/group-form.component.spec.ts +++ b/src/app/access-control/group-registry/group-form/group-form.component.spec.ts @@ -1,6 +1,6 @@ import { CommonModule } from '@angular/common'; import { HttpClient } from '@angular/common/http'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; import { ComponentFixture, TestBed, @@ -23,11 +23,7 @@ import { } from '@angular/router'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { Store } from '@ngrx/store'; -import { - TranslateLoader, - TranslateModule, - TranslateService, -} from '@ngx-translate/core'; +import { TranslateModule } from '@ngx-translate/core'; import { Operation } from 'fast-json-patch'; import { Observable, @@ -53,38 +49,43 @@ import { HALEndpointService } from '../../../core/shared/hal-endpoint.service'; import { NoContent } from '../../../core/shared/NoContent.model'; import { PageInfo } from '../../../core/shared/page-info.model'; import { UUIDService } from '../../../core/shared/uuid.service'; +import { XSRFService } from '../../../core/xsrf/xsrf.service'; +import { AlertComponent } from '../../../shared/alert/alert.component'; +import { ContextHelpDirective } from '../../../shared/context-help.directive'; import { FormBuilderService } from '../../../shared/form/builder/form-builder.service'; +import { FormComponent } from '../../../shared/form/form.component'; import { DSONameServiceMock } from '../../../shared/mocks/dso-name.service.mock'; import { getMockFormBuilderService } from '../../../shared/mocks/form-builder-service.mock'; import { RouterMock } from '../../../shared/mocks/router.mock'; -import { getMockTranslateService } from '../../../shared/mocks/translate.service.mock'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; +import { ActivatedRouteStub } from '../../../shared/testing/active-router.stub'; import { GroupMock, GroupMock2, } from '../../../shared/testing/group-mock'; import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub'; -import { TranslateLoaderMock } from '../../../shared/testing/translate-loader.mock'; import { GroupFormComponent } from './group-form.component'; +import { MembersListComponent } from './members-list/members-list.component'; +import { SubgroupsListComponent } from './subgroup-list/subgroups-list.component'; import { ValidateGroupExists } from './validators/group-exists.validator'; describe('GroupFormComponent', () => { let component: GroupFormComponent; let fixture: ComponentFixture; - let translateService: TranslateService; let builderService: FormBuilderService; let ePersonDataServiceStub: any; let groupsDataServiceStub: any; let dsoDataServiceStub: any; let authorizationService: AuthorizationDataService; let notificationService: NotificationsServiceStub; - let router; + let router: RouterMock; + let route: ActivatedRouteStub; - let groups; - let groupName; - let groupDescription; - let expected; + let groups: Group[]; + let groupName: string; + let groupDescription: string; + let expected: Group; beforeEach(waitForAsync(() => { groups = [GroupMock, GroupMock2]; @@ -99,6 +100,15 @@ describe('GroupFormComponent', () => { }, ], }, + object: createSuccessfulRemoteDataObject$(undefined), + _links: { + self: { + href: 'group-selflink', + }, + object: { + href: 'group-objectlink', + }, + }, }); ePersonDataServiceStub = {}; groupsDataServiceStub = { @@ -135,7 +145,14 @@ describe('GroupFormComponent', () => { create(group: Group): Observable> { this.allGroups = [...this.allGroups, group]; this.createdGroup = Object.assign({}, group, { - _links: { self: { href: 'group-selflink' } }, + _links: { + self: { + href: 'group-selflink', + }, + object: { + href: 'group-objectlink', + }, + }, }); return createSuccessfulRemoteDataObject$(this.createdGroup); }, @@ -217,19 +234,15 @@ describe('GroupFormComponent', () => { return typeof value === 'object' && value !== null; }, }); - translateService = getMockTranslateService(); router = new RouterMock(); + route = new ActivatedRouteStub(); notificationService = new NotificationsServiceStub(); + return TestBed.configureTestingModule({ imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule, - TranslateModule.forRoot({ - loader: { - provide: TranslateLoader, - useClass: TranslateLoaderMock, - }, - }), + TranslateModule.forRoot(), + GroupFormComponent, ], - declarations: [GroupFormComponent], providers: [ { provide: DSONameService, useValue: new DSONameServiceMock() }, { provide: EPersonDataService, useValue: ePersonDataServiceStub }, @@ -241,18 +254,26 @@ describe('GroupFormComponent', () => { { provide: HttpClient, useValue: {} }, { provide: ObjectCacheService, useValue: {} }, { provide: UUIDService, useValue: {} }, + { provide: XSRFService, useValue: {} }, { provide: Store, useValue: {} }, { provide: RemoteDataBuildService, useValue: {} }, { provide: HALEndpointService, useValue: {} }, - { - provide: ActivatedRoute, - useValue: { data: observableOf({ dso: { payload: {} } }), params: observableOf({}) }, - }, + { provide: ActivatedRoute, useValue: route }, { provide: Router, useValue: router }, { provide: AuthorizationDataService, useValue: authorizationService }, ], - schemas: [NO_ERRORS_SCHEMA], - }).compileComponents(); + schemas: [CUSTOM_ELEMENTS_SCHEMA], + }) + .overrideComponent(GroupFormComponent, { + remove: { imports: [ + FormComponent, + AlertComponent, + ContextHelpDirective, + MembersListComponent, + SubgroupsListComponent, + ] }, + }) + .compileComponents(); })); beforeEach(() => { @@ -264,8 +285,8 @@ describe('GroupFormComponent', () => { describe('when submitting the form', () => { beforeEach(() => { spyOn(component.submitForm, 'emit'); - component.groupName.value = groupName; - component.groupDescription.value = groupDescription; + component.groupName.setValue(groupName); + component.groupDescription.setValue(groupDescription); }); describe('without active Group', () => { beforeEach(() => { @@ -273,14 +294,22 @@ describe('GroupFormComponent', () => { fixture.detectChanges(); }); - it('should emit a new group using the correct values', (async () => { - await fixture.whenStable().then(() => { - expect(component.submitForm.emit).toHaveBeenCalledWith(expected); - }); + it('should emit a new group using the correct values', (() => { + expect(component.submitForm.emit).toHaveBeenCalledWith(jasmine.objectContaining({ + name: groupName, + metadata: { + 'dc.description': [ + { + value: groupDescription, + }, + ], + }, + })); })); }); + describe('with active Group', () => { - let expected2; + let expected2: Group; beforeEach(() => { expected2 = Object.assign(new Group(), { name: 'newGroupName', @@ -291,15 +320,24 @@ describe('GroupFormComponent', () => { }, ], }, + object: createSuccessfulRemoteDataObject$(undefined), + _links: { + self: { + href: 'group-selflink', + }, + object: { + href: 'group-objectlink', + }, + }, }); spyOn(groupsDataServiceStub, 'getActiveGroup').and.returnValue(observableOf(expected)); spyOn(groupsDataServiceStub, 'patch').and.returnValue(createSuccessfulRemoteDataObject$(expected2)); - component.groupName.value = 'newGroupName'; - component.onSubmit(); - fixture.detectChanges(); + component.ngOnInit(); }); it('should edit with name and description operations', () => { + component.groupName.setValue('newGroupName'); + component.onSubmit(); const operations = [{ op: 'add', path: '/metadata/dc.description', @@ -313,9 +351,8 @@ describe('GroupFormComponent', () => { }); it('should edit with description operations', () => { - component.groupName.value = null; + component.groupName.setValue(null); component.onSubmit(); - fixture.detectChanges(); const operations = [{ op: 'add', path: '/metadata/dc.description', @@ -325,9 +362,9 @@ describe('GroupFormComponent', () => { }); it('should edit with name operations', () => { - component.groupDescription.value = null; + component.groupName.setValue('newGroupName'); + component.groupDescription.setValue(null); component.onSubmit(); - fixture.detectChanges(); const operations = [{ op: 'replace', path: '/name', @@ -336,12 +373,13 @@ describe('GroupFormComponent', () => { expect(groupsDataServiceStub.patch).toHaveBeenCalledWith(expected, operations); }); - it('should emit the existing group using the correct new values', (async () => { - await fixture.whenStable().then(() => { - expect(component.submitForm.emit).toHaveBeenCalledWith(expected2); - }); - })); + it('should emit the existing group using the correct new values', () => { + component.onSubmit(); + expect(component.submitForm.emit).toHaveBeenCalledWith(expected2); + }); + it('should emit success notification', () => { + component.onSubmit(); expect(notificationService.success).toHaveBeenCalled(); }); }); @@ -356,11 +394,8 @@ describe('GroupFormComponent', () => { describe('check form validation', () => { - let groupCommunity; - beforeEach(() => { groupName = 'testName'; - groupCommunity = 'testgroupCommunity'; groupDescription = 'testgroupDescription'; expected = Object.assign(new Group(), { @@ -372,8 +407,17 @@ describe('GroupFormComponent', () => { }, ], }, + _links: { + self: { + href: 'group-selflink', + }, + object: { + href: 'group-objectlink', + }, + }, }); spyOn(component.submitForm, 'emit'); + spyOn(dsoDataServiceStub, 'findByHref').and.returnValue(observableOf(expected)); fixture.detectChanges(); component.initialisePage(); @@ -423,21 +467,20 @@ describe('GroupFormComponent', () => { }); describe('delete', () => { - let deleteButton; + let deleteButton: HTMLButtonElement; - beforeEach(() => { - component.initialisePage(); - - component.canEdit$ = observableOf(true); - component.groupBeingEdited = { + beforeEach(async () => { + spyOn(groupsDataServiceStub, 'delete').and.callThrough(); + component.activeGroup$ = observableOf({ + id: 'active-group', permanent: false, - } as Group; + } as Group); + component.canEdit$ = observableOf(true); + + component.initialisePage(); fixture.detectChanges(); deleteButton = fixture.debugElement.query(By.css('.delete-button')).nativeElement; - - spyOn(groupsDataServiceStub, 'delete').and.callThrough(); - spyOn(groupsDataServiceStub, 'getActiveGroup').and.returnValue(observableOf({ id: 'active-group' })); }); describe('if confirmed via modal', () => { diff --git a/src/app/access-control/group-registry/group-form/group-form.component.ts b/src/app/access-control/group-registry/group-form/group-form.component.ts index 89bb4c395f..d2ddb3266b 100644 --- a/src/app/access-control/group-registry/group-form/group-form.component.ts +++ b/src/app/access-control/group-registry/group-form/group-form.component.ts @@ -1,3 +1,7 @@ +import { + AsyncPipe, + NgIf, +} from '@angular/common'; import { ChangeDetectorRef, Component, @@ -7,7 +11,10 @@ import { OnInit, Output, } from '@angular/core'; -import { UntypedFormGroup } from '@angular/forms'; +import { + AbstractControl, + UntypedFormGroup, +} from '@angular/forms'; import { ActivatedRoute, Router, @@ -19,18 +26,18 @@ import { DynamicInputModel, DynamicTextAreaModel, } from '@ng-dynamic-forms/core'; -import { TranslateService } from '@ngx-translate/core'; +import { + TranslateModule, + TranslateService, +} from '@ngx-translate/core'; import { Operation } from 'fast-json-patch'; import { combineLatest as observableCombineLatest, Observable, - of as observableOf, Subscription, } from 'rxjs'; import { - catchError, debounceTime, - filter, map, switchMap, take, @@ -46,7 +53,6 @@ import { FeatureID } from '../../../core/data/feature-authorization/feature-id'; import { PaginatedList } from '../../../core/data/paginated-list.model'; import { RemoteData } from '../../../core/data/remote-data'; import { RequestService } from '../../../core/data/request.service'; -import { EPersonDataService } from '../../../core/eperson/eperson-data.service'; import { GroupDataService } from '../../../core/eperson/group-data.service'; import { Group } from '../../../core/eperson/models/group.model'; import { Collection } from '../../../core/shared/collection.model'; @@ -54,30 +60,46 @@ import { Community } from '../../../core/shared/community.model'; import { DSpaceObject } from '../../../core/shared/dspace-object.model'; import { NoContent } from '../../../core/shared/NoContent.model'; import { + getAllCompletedRemoteData, getFirstCompletedRemoteData, getFirstSucceededRemoteData, - getFirstSucceededRemoteDataPayload, getRemoteDataPayload, } from '../../../core/shared/operators'; +import { AlertComponent } from '../../../shared/alert/alert.component'; import { AlertType } from '../../../shared/alert/alert-type'; import { ConfirmationModalComponent } from '../../../shared/confirmation-modal/confirmation-modal.component'; +import { ContextHelpDirective } from '../../../shared/context-help.directive'; import { hasValue, hasValueOperator, isNotEmpty, } from '../../../shared/empty.util'; import { FormBuilderService } from '../../../shared/form/builder/form-builder.service'; +import { FormComponent } from '../../../shared/form/form.component'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { followLink } from '../../../shared/utils/follow-link-config.model'; import { getGroupEditRoute, getGroupsRoute, } from '../../access-control-routing-paths'; +import { MembersListComponent } from './members-list/members-list.component'; +import { SubgroupsListComponent } from './subgroup-list/subgroups-list.component'; import { ValidateGroupExists } from './validators/group-exists.validator'; @Component({ selector: 'ds-group-form', templateUrl: './group-form.component.html', + imports: [ + FormComponent, + AlertComponent, + NgIf, + AsyncPipe, + TranslateModule, + ContextHelpDirective, + MembersListComponent, + SubgroupsListComponent, + ], + standalone: true, }) /** * A form used for creating and editing groups @@ -94,9 +116,9 @@ export class GroupFormComponent implements OnInit, OnDestroy { /** * Dynamic models for the inputs of form */ - groupName: DynamicInputModel; - groupCommunity: DynamicInputModel; - groupDescription: DynamicTextAreaModel; + groupName: AbstractControl; + groupCommunity: AbstractControl; + groupDescription: AbstractControl; /** * A list of all dynamic input models @@ -139,21 +161,30 @@ export class GroupFormComponent implements OnInit, OnDestroy { */ subs: Subscription[] = []; - /** - * Group currently being edited - */ - groupBeingEdited: Group; - /** * Observable whether or not the logged in user is allowed to delete the Group & doesn't have a linked object (community / collection linked to workspace group */ canEdit$: Observable; /** - * The AlertType enumeration - * @type {AlertType} + * The current {@link Group} */ - public AlertTypeEnum = AlertType; + activeGroup$: Observable; + + /** + * The current {@link Group}'s linked {@link Community}/{@link Collection} + */ + activeGroupLinkedDSO$: Observable; + + /** + * Link to the current {@link Group}'s {@link Community}/{@link Collection} edit role tab + */ + linkedEditRolesRoute$: Observable; + + /** + * The AlertType enumeration + */ + public readonly AlertType = AlertType; /** * Subscription to email field value change @@ -163,126 +194,121 @@ export class GroupFormComponent implements OnInit, OnDestroy { constructor( public groupDataService: GroupDataService, - private ePersonDataService: EPersonDataService, - private dSpaceObjectDataService: DSpaceObjectDataService, - private formBuilderService: FormBuilderService, - private translateService: TranslateService, - private notificationsService: NotificationsService, - private route: ActivatedRoute, + protected dSpaceObjectDataService: DSpaceObjectDataService, + protected formBuilderService: FormBuilderService, + protected translateService: TranslateService, + protected notificationsService: NotificationsService, + protected route: ActivatedRoute, protected router: Router, - private authorizationService: AuthorizationDataService, - private modalService: NgbModal, + protected authorizationService: AuthorizationDataService, + protected modalService: NgbModal, public requestService: RequestService, protected changeDetectorRef: ChangeDetectorRef, public dsoNameService: DSONameService, ) { } - ngOnInit() { + ngOnInit(): void { + if (this.route.snapshot.params.groupId !== 'newGroup') { + this.setActiveGroup(this.route.snapshot.params.groupId); + } + this.activeGroup$ = this.groupDataService.getActiveGroup(); + this.activeGroupLinkedDSO$ = this.getActiveGroupLinkedDSO(); + this.linkedEditRolesRoute$ = this.getLinkedEditRolesRoute(); + this.canEdit$ = this.activeGroupLinkedDSO$.pipe( + switchMap((dso: DSpaceObject) => { + if (hasValue(dso)) { + return [false]; + } else { + return this.activeGroup$.pipe( + hasValueOperator(), + switchMap((group: Group) => this.authorizationService.isAuthorized(FeatureID.CanDelete, group.self)), + ); + } + }), + ); this.initialisePage(); } initialisePage() { - this.subs.push(this.route.params.subscribe((params) => { - if (params.groupId !== 'newGroup') { - this.setActiveGroup(params.groupId); - } - })); - this.canEdit$ = this.groupDataService.getActiveGroup().pipe( - hasValueOperator(), - switchMap((group: Group) => { - return observableCombineLatest([ - this.authorizationService.isAuthorized(FeatureID.CanDelete, isNotEmpty(group) ? group.self : undefined), - this.hasLinkedDSO(group), - ]).pipe( - map(([isAuthorized, hasLinkedDSO]: [boolean, boolean]) => isAuthorized && !hasLinkedDSO), - ); + const groupNameModel = new DynamicInputModel({ + id: 'groupName', + label: this.translateService.instant(`${this.messagePrefix}.groupName`), + name: 'groupName', + validators: { + required: null, + }, + required: true, + }); + const groupCommunityModel = new DynamicInputModel({ + id: 'groupCommunity', + label: this.translateService.instant(`${this.messagePrefix}.groupCommunity`), + name: 'groupCommunity', + required: false, + readOnly: true, + }); + const groupDescriptionModel = new DynamicTextAreaModel({ + id: 'groupDescription', + label: this.translateService.instant(`${this.messagePrefix}.groupDescription`), + name: 'groupDescription', + required: false, + spellCheck: environment.form.spellCheck, + }); + this.formModel = [ + groupNameModel, + groupDescriptionModel, + ]; + this.formGroup = this.formBuilderService.createFormGroup(this.formModel); + this.groupName = this.formGroup.get('groupName'); + this.groupDescription = this.formGroup.get('groupDescription'); + + if (hasValue(this.groupName)) { + this.groupName.setAsyncValidators(ValidateGroupExists.createValidator(this.groupDataService)); + this.groupNameValueChangeSubscribe = this.groupName.valueChanges.pipe(debounceTime(300)).subscribe(() => { + this.changeDetectorRef.detectChanges(); + }); + } + + this.subs.push( + observableCombineLatest([ + this.activeGroup$, + this.canEdit$, + this.activeGroupLinkedDSO$, + ]).subscribe(([activeGroup, canEdit, linkedObject]) => { + + if (activeGroup != null) { + + // Disable group name exists validator + this.formGroup.controls.groupName.clearAsyncValidators(); + + if (isNotEmpty(linkedObject?.name)) { + if (!this.formGroup.controls.groupCommunity) { + this.formBuilderService.insertFormGroupControl(1, this.formGroup, this.formModel, groupCommunityModel); + this.groupDescription = this.formGroup.get('groupCommunity'); + } + this.formGroup.patchValue({ + groupName: activeGroup.name, + groupCommunity: linkedObject?.name ?? '', + groupDescription: activeGroup.firstMetadataValue('dc.description'), + }); + } else { + this.formModel = [ + groupNameModel, + groupDescriptionModel, + ]; + this.formGroup.patchValue({ + groupName: activeGroup.name, + groupDescription: activeGroup.firstMetadataValue('dc.description'), + }); + } + if (!canEdit || activeGroup.permanent) { + this.formGroup.disable(); + } else { + this.formGroup.enable(); + } + } }), ); - observableCombineLatest([ - this.translateService.get(`${this.messagePrefix}.groupName`), - this.translateService.get(`${this.messagePrefix}.groupCommunity`), - this.translateService.get(`${this.messagePrefix}.groupDescription`), - ]).subscribe(([groupName, groupCommunity, groupDescription]) => { - this.groupName = new DynamicInputModel({ - id: 'groupName', - label: groupName, - name: 'groupName', - validators: { - required: null, - }, - required: true, - }); - this.groupCommunity = new DynamicInputModel({ - id: 'groupCommunity', - label: groupCommunity, - name: 'groupCommunity', - required: false, - readOnly: true, - }); - this.groupDescription = new DynamicTextAreaModel({ - id: 'groupDescription', - label: groupDescription, - name: 'groupDescription', - required: false, - spellCheck: environment.form.spellCheck, - }); - this.formModel = [ - this.groupName, - this.groupDescription, - ]; - this.formGroup = this.formBuilderService.createFormGroup(this.formModel); - - if (this.formGroup.controls.groupName) { - this.formGroup.controls.groupName.setAsyncValidators(ValidateGroupExists.createValidator(this.groupDataService)); - this.groupNameValueChangeSubscribe = this.groupName.valueChanges.pipe(debounceTime(300)).subscribe(() => { - this.changeDetectorRef.detectChanges(); - }); - } - - this.subs.push( - observableCombineLatest([ - this.groupDataService.getActiveGroup(), - this.canEdit$, - this.groupDataService.getActiveGroup() - .pipe(filter((activeGroup) => hasValue(activeGroup)),switchMap((activeGroup) => this.getLinkedDSO(activeGroup).pipe(getFirstSucceededRemoteDataPayload()))), - ]).subscribe(([activeGroup, canEdit, linkedObject]) => { - - if (activeGroup != null) { - - // Disable group name exists validator - this.formGroup.controls.groupName.clearAsyncValidators(); - - this.groupBeingEdited = activeGroup; - - if (linkedObject?.name) { - if (!this.formGroup.controls.groupCommunity) { - this.formBuilderService.insertFormGroupControl(1, this.formGroup, this.formModel, this.groupCommunity); - this.formGroup.patchValue({ - groupName: activeGroup.name, - groupCommunity: linkedObject?.name ?? '', - groupDescription: activeGroup.firstMetadataValue('dc.description'), - }); - } - } else { - this.formModel = [ - this.groupName, - this.groupDescription, - ]; - this.formGroup.patchValue({ - groupName: activeGroup.name, - groupDescription: activeGroup.firstMetadataValue('dc.description'), - }); - } - setTimeout(() => { - if (!canEdit || activeGroup.permanent) { - this.formGroup.disable(); - } - }, 200); - } - }), - ); - }); } /** @@ -301,9 +327,9 @@ export class GroupFormComponent implements OnInit, OnDestroy { * Emit the updated/created eperson using the EventEmitter submitForm */ onSubmit() { - this.groupDataService.getActiveGroup().pipe(take(1)).subscribe( - (group: Group) => { - const values = { + this.activeGroup$.pipe(take(1)).subscribe((group: Group) => { + if (group === null) { + this.createNewGroup({ name: this.groupName.value, metadata: { 'dc.description': [ @@ -312,14 +338,11 @@ export class GroupFormComponent implements OnInit, OnDestroy { }, ], }, - }; - if (group === null) { - this.createNewGroup(values); - } else { - this.editGroup(group); - } - }, - ); + }); + } else { + this.editGroup(group); + } + }); } /** @@ -425,7 +448,7 @@ export class GroupFormComponent implements OnInit, OnDestroy { * @param groupSelfLink SelfLink of group to set as active */ setActiveGroupWithLink(groupSelfLink: string) { - this.groupDataService.getActiveGroup().pipe(take(1)).subscribe((activeGroup: Group) => { + this.activeGroup$.pipe(take(1)).subscribe((activeGroup: Group) => { if (activeGroup === null) { this.groupDataService.cancelEditGroup(); this.groupDataService.findByHref(groupSelfLink, false, false, followLink('subgroups'), followLink('epersons'), followLink('object')) @@ -444,7 +467,7 @@ export class GroupFormComponent implements OnInit, OnDestroy { * It'll either show a success or error message depending on whether the delete was successful or not. */ delete() { - this.groupDataService.getActiveGroup().pipe(take(1)).subscribe((group: Group) => { + this.activeGroup$.pipe(take(1)).subscribe((group: Group) => { const modalRef = this.modalService.open(ConfirmationModalComponent); modalRef.componentInstance.name = this.dsoNameService.getName(group); modalRef.componentInstance.headerLabel = this.messagePrefix + '.delete-group.modal.header'; @@ -488,52 +511,38 @@ export class GroupFormComponent implements OnInit, OnDestroy { } /** - * Check if group has a linked object (community or collection linked to a workflow group) - * @param group + * Get the active {@link Group}'s linked object if it has one ({@link Community} or {@link Collection} linked to a + * workflow group) */ - hasLinkedDSO(group: Group): Observable { - if (hasValue(group) && hasValue(group._links.object.href)) { - return this.getLinkedDSO(group).pipe( - map((rd: RemoteData) => { - return hasValue(rd) && hasValue(rd.payload); - }), - catchError(() => observableOf(false)), - ); - } + getActiveGroupLinkedDSO(): Observable { + return this.activeGroup$.pipe( + hasValueOperator(), + switchMap((group: Group) => { + if (group.object === undefined) { + return this.dSpaceObjectDataService.findByHref(group._links.object.href); + } + return group.object; + }), + getAllCompletedRemoteData(), + getRemoteDataPayload(), + ); } /** - * Get group's linked object if it has one (community or collection linked to a workflow group) - * @param group + * Get the route to the edit roles tab of the active {@link Group}'s linked object (community or collection linked + * to a workflow group) if it has one */ - getLinkedDSO(group: Group): Observable> { - if (hasValue(group) && hasValue(group._links.object.href)) { - if (group.object === undefined) { - return this.dSpaceObjectDataService.findByHref(group._links.object.href); - } - return group.object; - } - } - - /** - * Get the route to the edit roles tab of the group's linked object (community or collection linked to a workflow group) if it has one - * @param group - */ - getLinkedEditRolesRoute(group: Group): Observable { - if (hasValue(group) && hasValue(group._links.object.href)) { - return this.getLinkedDSO(group).pipe( - map((rd: RemoteData) => { - if (hasValue(rd) && hasValue(rd.payload)) { - const dso = rd.payload; - switch ((dso as any).type) { - case Community.type.value: - return getCommunityEditRolesRoute(rd.payload.id); - case Collection.type.value: - return getCollectionEditRolesRoute(rd.payload.id); - } - } - }), - ); - } + getLinkedEditRolesRoute(): Observable { + return this.activeGroupLinkedDSO$.pipe( + hasValueOperator(), + map((dso: DSpaceObject) => { + switch ((dso as any).type) { + case Community.type.value: + return getCommunityEditRolesRoute(dso.id); + case Collection.type.value: + return getCollectionEditRolesRoute(dso.id); + } + }), + ); } } diff --git a/src/app/access-control/group-registry/group-form/members-list/members-list.component.html b/src/app/access-control/group-registry/group-form/members-list/members-list.component.html index 85aa987605..591d80bdd5 100644 --- a/src/app/access-control/group-registry/group-form/members-list/members-list.component.html +++ b/src/app/access-control/group-registry/group-form/members-list/members-list.component.html @@ -5,7 +5,6 @@ @@ -21,25 +20,33 @@ - - {{eperson.id}} + + {{epersonDTO.eperson.id}} - - {{ dsoNameService.getName(eperson) }} + + {{ dsoNameService.getName(epersonDTO.eperson) }} - {{messagePrefix + '.table.email' | translate}}: {{ eperson.email ? eperson.email : '-' }}
- {{messagePrefix + '.table.netid' | translate}}: {{ eperson.netid ? eperson.netid : '-' }} + {{messagePrefix + '.table.email' | translate}}: {{ epersonDTO.eperson.email ? epersonDTO.eperson.email : '-' }}
+ {{messagePrefix + '.table.netid' | translate}}: {{ epersonDTO.eperson.netid ? epersonDTO.eperson.netid : '-' }}
- +
@@ -86,7 +93,6 @@ diff --git a/src/app/access-control/group-registry/group-form/members-list/members-list.component.spec.ts b/src/app/access-control/group-registry/group-form/members-list/members-list.component.spec.ts index c63ff40df8..9a6b2b4f05 100644 --- a/src/app/access-control/group-registry/group-form/members-list/members-list.component.spec.ts +++ b/src/app/access-control/group-registry/group-form/members-list/members-list.component.spec.ts @@ -20,7 +20,10 @@ import { BrowserModule, By, } from '@angular/platform-browser'; -import { Router } from '@angular/router'; +import { + ActivatedRoute, + Router, +} from '@angular/router'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { TranslateLoader, @@ -45,13 +48,16 @@ import { EPerson } from '../../../../core/eperson/models/eperson.model'; import { Group } from '../../../../core/eperson/models/group.model'; import { PaginationService } from '../../../../core/pagination/pagination.service'; import { PageInfo } from '../../../../core/shared/page-info.model'; +import { ContextHelpDirective } from '../../../../shared/context-help.directive'; import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service'; import { DSONameServiceMock } from '../../../../shared/mocks/dso-name.service.mock'; import { getMockFormBuilderService } from '../../../../shared/mocks/form-builder-service.mock'; import { RouterMock } from '../../../../shared/mocks/router.mock'; import { getMockTranslateService } from '../../../../shared/mocks/translate.service.mock'; import { NotificationsService } from '../../../../shared/notifications/notifications.service'; +import { PaginationComponent } from '../../../../shared/pagination/pagination.component'; import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils'; +import { ActivatedRouteStub } from '../../../../shared/testing/active-router.stub'; import { EPersonMock, EPersonMock2, @@ -155,9 +161,7 @@ describe('MembersListComponent', () => { provide: TranslateLoader, useClass: TranslateLoaderMock, }, - }), - ], - declarations: [MembersListComponent], + }), MembersListComponent], providers: [MembersListComponent, { provide: EPersonDataService, useValue: ePersonDataServiceStub }, { provide: GroupDataService, useValue: groupsDataServiceStub }, @@ -166,9 +170,16 @@ describe('MembersListComponent', () => { { provide: Router, useValue: new RouterMock() }, { provide: PaginationService, useValue: paginationService }, { provide: DSONameService, useValue: new DSONameServiceMock() }, + { provide: ActivatedRoute, useValue: new ActivatedRouteStub() }, ], schemas: [NO_ERRORS_SCHEMA], - }).compileComponents(); + }) + .overrideComponent(MembersListComponent, { + remove: { + imports: [PaginationComponent, ContextHelpDirective], + }, + }) + .compileComponents(); })); beforeEach(() => { @@ -211,13 +222,13 @@ describe('MembersListComponent', () => { describe('if first delete button is pressed', () => { beforeEach(() => { + spyOn(component, 'search').and.callThrough(); const deleteButton: DebugElement = fixture.debugElement.query(By.css('#ePeopleMembersOfGroup tbody .fa-trash-alt')); deleteButton.nativeElement.click(); fixture.detectChanges(); }); - it('then no ePerson remains as a member of the active group.', () => { - const epersonsFound = fixture.debugElement.queryAll(By.css('#ePeopleMembersOfGroup tbody tr')); - expect(epersonsFound.length).toEqual(0); + it('should trigger the search to add the user back to the search table', () => { + expect(component.search).toHaveBeenCalled(); }); }); }); @@ -253,13 +264,13 @@ describe('MembersListComponent', () => { describe('if first add button is pressed', () => { beforeEach(() => { + spyOn(component, 'search').and.callThrough(); const addButton: DebugElement = fixture.debugElement.query(By.css('#epersonsSearch tbody .fa-plus')); addButton.nativeElement.click(); fixture.detectChanges(); }); - it('then all (two) ePersons are member of the active group. No non-members left', () => { - epersonsFound = fixture.debugElement.queryAll(By.css('#epersonsSearch tbody tr')); - expect(epersonsFound.length).toEqual(0); + it('should trigger the search to remove the user from the search table', () => { + expect(component.search).toHaveBeenCalled(); }); }); }); diff --git a/src/app/access-control/group-registry/group-form/members-list/members-list.component.ts b/src/app/access-control/group-registry/group-form/members-list/members-list.component.ts index 002e20524c..9b123ae447 100644 --- a/src/app/access-control/group-registry/group-form/members-list/members-list.component.ts +++ b/src/app/access-control/group-registry/group-form/members-list/members-list.component.ts @@ -1,29 +1,52 @@ +import { + AsyncPipe, + NgClass, + NgForOf, + NgIf, +} from '@angular/common'; import { Component, Input, OnDestroy, OnInit, } from '@angular/core'; -import { UntypedFormBuilder } from '@angular/forms'; -import { Router } from '@angular/router'; -import { TranslateService } from '@ngx-translate/core'; +import { + ReactiveFormsModule, + UntypedFormBuilder, +} from '@angular/forms'; +import { + Router, + RouterLink, +} from '@angular/router'; +import { + TranslateModule, + TranslateService, +} from '@ngx-translate/core'; import { BehaviorSubject, + combineLatest as observableCombineLatest, Observable, + ObservedValueOf, + of as observableOf, Subscription, } from 'rxjs'; import { + defaultIfEmpty, map, switchMap, take, } from 'rxjs/operators'; import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service'; -import { PaginatedList } from '../../../../core/data/paginated-list.model'; +import { + buildPaginatedList, + PaginatedList, +} from '../../../../core/data/paginated-list.model'; import { RemoteData } from '../../../../core/data/remote-data'; import { EPersonDataService } from '../../../../core/eperson/eperson-data.service'; import { GroupDataService } from '../../../../core/eperson/group-data.service'; import { EPerson } from '../../../../core/eperson/models/eperson.model'; +import { EpersonDtoModel } from '../../../../core/eperson/models/eperson-dto.model'; import { Group } from '../../../../core/eperson/models/group.model'; import { PaginationService } from '../../../../core/pagination/pagination.service'; import { @@ -31,7 +54,9 @@ import { getFirstCompletedRemoteData, getRemoteDataPayload, } from '../../../../core/shared/operators'; +import { ContextHelpDirective } from '../../../../shared/context-help.directive'; import { NotificationsService } from '../../../../shared/notifications/notifications.service'; +import { PaginationComponent } from '../../../../shared/pagination/pagination.component'; import { PaginationComponentOptions } from '../../../../shared/pagination/pagination-component-options.model'; import { getEPersonEditRoute } from '../../../access-control-routing-paths'; @@ -78,6 +103,18 @@ export interface EPersonListActionConfig { @Component({ selector: 'ds-members-list', templateUrl: './members-list.component.html', + imports: [ + TranslateModule, + ContextHelpDirective, + ReactiveFormsModule, + PaginationComponent, + NgIf, + AsyncPipe, + RouterLink, + NgClass, + NgForOf, + ], + standalone: true, }) /** * The list of members in the edit group page @@ -85,21 +122,21 @@ export interface EPersonListActionConfig { export class MembersListComponent implements OnInit, OnDestroy { @Input() - messagePrefix: string; + messagePrefix: string; @Input() - actionConfig: EPersonListActionConfig = { - add: { - css: 'btn-outline-primary', - disabled: false, - icon: 'fas fa-plus fa-fw', - }, - remove: { - css: 'btn-outline-danger', - disabled: false, - icon: 'fas fa-trash-alt fa-fw', - }, - }; + actionConfig: EPersonListActionConfig = { + add: { + css: 'btn-outline-primary', + disabled: false, + icon: 'fas fa-plus fa-fw', + }, + remove: { + css: 'btn-outline-danger', + disabled: false, + icon: 'fas fa-trash-alt fa-fw', + }, + }; /** * EPeople being displayed in search result, initially all members, after search result of search @@ -108,7 +145,7 @@ export class MembersListComponent implements OnInit, OnDestroy { /** * List of EPeople members of currently active group being edited */ - ePeopleMembersOfGroup: BehaviorSubject> = new BehaviorSubject>(undefined); + ePeopleMembersOfGroup: BehaviorSubject> = new BehaviorSubject(undefined); /** * Pagination config used to display the list of EPeople that are result of EPeople search @@ -197,10 +234,35 @@ export class MembersListComponent implements OnInit, OnDestroy { return rd; } }), - getRemoteDataPayload()) - .subscribe((paginatedListOfEPersons: PaginatedList) => { - this.ePeopleMembersOfGroup.next(paginatedListOfEPersons); - })); + switchMap((epersonListRD: RemoteData>) => { + const dtos$ = observableCombineLatest([...epersonListRD.payload.page.map((member: EPerson) => { + const dto$: Observable = observableCombineLatest( + this.isMemberOfGroup(member), (isMember: ObservedValueOf>) => { + const epersonDtoModel: EpersonDtoModel = new EpersonDtoModel(); + epersonDtoModel.eperson = member; + epersonDtoModel.ableToDelete = isMember; + return epersonDtoModel; + }); + return dto$; + })]); + return dtos$.pipe(defaultIfEmpty([]), map((dtos: EpersonDtoModel[]) => { + return buildPaginatedList(epersonListRD.payload.pageInfo, dtos); + })); + }), + ).subscribe((paginatedListOfDTOs: PaginatedList) => { + this.ePeopleMembersOfGroup.next(paginatedListOfDTOs); + }), + ); + } + + /** + * We always return true since this is only used by the top section (which represents all the users part of the group + * in {@link MembersListComponent}) + * + * @param possibleMember EPerson that is a possible member (being tested) of the group currently being edited + */ + isMemberOfGroup(possibleMember: EPerson): Observable { + return observableOf(true); } /** diff --git a/src/app/access-control/group-registry/group-form/subgroup-list/subgroups-list.component.html b/src/app/access-control/group-registry/group-form/subgroup-list/subgroups-list.component.html index ab7bd84f6b..66404bde0d 100644 --- a/src/app/access-control/group-registry/group-form/subgroup-list/subgroups-list.component.html +++ b/src/app/access-control/group-registry/group-form/subgroup-list/subgroups-list.component.html @@ -5,7 +5,6 @@ @@ -84,7 +83,6 @@ diff --git a/src/app/access-control/group-registry/group-form/subgroup-list/subgroups-list.component.spec.ts b/src/app/access-control/group-registry/group-form/subgroup-list/subgroups-list.component.spec.ts index ca50406aea..5b39102ca8 100644 --- a/src/app/access-control/group-registry/group-form/subgroup-list/subgroups-list.component.spec.ts +++ b/src/app/access-control/group-registry/group-form/subgroup-list/subgroups-list.component.spec.ts @@ -19,7 +19,10 @@ import { BrowserModule, By, } from '@angular/platform-browser'; -import { Router } from '@angular/router'; +import { + ActivatedRoute, + Router, +} from '@angular/router'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { TranslateLoader, @@ -43,13 +46,16 @@ import { GroupDataService } from '../../../../core/eperson/group-data.service'; import { Group } from '../../../../core/eperson/models/group.model'; import { PaginationService } from '../../../../core/pagination/pagination.service'; import { PageInfo } from '../../../../core/shared/page-info.model'; +import { ContextHelpDirective } from '../../../../shared/context-help.directive'; import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service'; import { DSONameServiceMock } from '../../../../shared/mocks/dso-name.service.mock'; import { getMockFormBuilderService } from '../../../../shared/mocks/form-builder-service.mock'; import { RouterMock } from '../../../../shared/mocks/router.mock'; import { getMockTranslateService } from '../../../../shared/mocks/translate.service.mock'; import { NotificationsService } from '../../../../shared/notifications/notifications.service'; +import { PaginationComponent } from '../../../../shared/pagination/pagination.component'; import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils'; +import { ActivatedRouteStub } from '../../../../shared/testing/active-router.stub'; import { GroupMock, GroupMock2, @@ -119,7 +125,9 @@ describe('SubgroupsListComponent', () => { if (query === '') { return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), groupNonMembers)); } - return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), [])); + return createSuccessfulRemoteDataObject$( + buildPaginatedList(new PageInfo(), []), + ); }, addSubGroupToGroup(parentGroup, subgroupToAdd: Group): Observable { // Add group to list of subgroups @@ -153,28 +161,44 @@ describe('SubgroupsListComponent', () => { routerStub = new RouterMock(); builderService = getMockFormBuilderService(); translateService = getMockTranslateService(); - paginationService = new PaginationServiceStub(); return TestBed.configureTestingModule({ - imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule, + imports: [ + CommonModule, + NgbModule, + FormsModule, + ReactiveFormsModule, + BrowserModule, + // ContextHelpDirective, TranslateModule.forRoot({ loader: { provide: TranslateLoader, useClass: TranslateLoaderMock, }, }), + SubgroupsListComponent, ], - declarations: [SubgroupsListComponent], - providers: [SubgroupsListComponent, + providers: [ + SubgroupsListComponent, { provide: DSONameService, useValue: new DSONameServiceMock() }, { provide: GroupDataService, useValue: groupsDataServiceStub }, - { provide: NotificationsService, useValue: new NotificationsServiceStub() }, + { + provide: NotificationsService, + useValue: new NotificationsServiceStub(), + }, { provide: FormBuilderService, useValue: builderService }, { provide: Router, useValue: routerStub }, { provide: PaginationService, useValue: paginationService }, + { provide: ActivatedRoute, useValue: new ActivatedRouteStub() }, ], schemas: [NO_ERRORS_SCHEMA], - }).compileComponents(); + }) + .overrideComponent(SubgroupsListComponent, { + remove: { + imports: [ContextHelpDirective, PaginationComponent], + }, + }) + .compileComponents(); })); beforeEach(() => { diff --git a/src/app/access-control/group-registry/group-form/subgroup-list/subgroups-list.component.ts b/src/app/access-control/group-registry/group-form/subgroup-list/subgroups-list.component.ts index ae677a5558..95c1b7f249 100644 --- a/src/app/access-control/group-registry/group-form/subgroup-list/subgroups-list.component.ts +++ b/src/app/access-control/group-registry/group-form/subgroup-list/subgroups-list.component.ts @@ -1,12 +1,26 @@ +import { + AsyncPipe, + NgForOf, + NgIf, +} from '@angular/common'; import { Component, Input, OnDestroy, OnInit, } from '@angular/core'; -import { UntypedFormBuilder } from '@angular/forms'; -import { Router } from '@angular/router'; -import { TranslateService } from '@ngx-translate/core'; +import { + ReactiveFormsModule, + UntypedFormBuilder, +} from '@angular/forms'; +import { + Router, + RouterLink, +} from '@angular/router'; +import { + TranslateModule, + TranslateService, +} from '@ngx-translate/core'; import { BehaviorSubject, Observable, @@ -30,7 +44,9 @@ import { getFirstCompletedRemoteData, } from '../../../../core/shared/operators'; import { PageInfo } from '../../../../core/shared/page-info.model'; +import { ContextHelpDirective } from '../../../../shared/context-help.directive'; import { NotificationsService } from '../../../../shared/notifications/notifications.service'; +import { PaginationComponent } from '../../../../shared/pagination/pagination.component'; import { PaginationComponentOptions } from '../../../../shared/pagination/pagination-component-options.model'; import { followLink } from '../../../../shared/utils/follow-link-config.model'; @@ -46,6 +62,17 @@ enum SubKey { @Component({ selector: 'ds-subgroups-list', templateUrl: './subgroups-list.component.html', + imports: [ + RouterLink, + AsyncPipe, + NgForOf, + ContextHelpDirective, + TranslateModule, + ReactiveFormsModule, + PaginationComponent, + NgIf, + ], + standalone: true, }) /** * The list of subgroups in the edit group page @@ -53,7 +80,7 @@ enum SubKey { export class SubgroupsListComponent implements OnInit, OnDestroy { @Input() - messagePrefix: string; + messagePrefix: string; /** * Result of search groups, initially all groups diff --git a/src/app/access-control/group-registry/group-page.guard.spec.ts b/src/app/access-control/group-registry/group-page.guard.spec.ts index b1648f59ec..3024e42d64 100644 --- a/src/app/access-control/group-registry/group-page.guard.spec.ts +++ b/src/app/access-control/group-registry/group-page.guard.spec.ts @@ -1,14 +1,24 @@ +import { + TestBed, + waitForAsync, +} from '@angular/core/testing'; import { ActivatedRouteSnapshot, Router, + UrlTree, } from '@angular/router'; -import { of as observableOf } from 'rxjs'; +import { + Observable, + of as observableOf, +} from 'rxjs'; import { AuthService } from '../../core/auth/auth.service'; import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; import { FeatureID } from '../../core/data/feature-authorization/feature-id'; import { HALEndpointService } from '../../core/shared/hal-endpoint.service'; -import { GroupPageGuard } from './group-page.guard'; +import { groupPageGuard } from './group-page.guard'; + +jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000; // Increase timeout to 10 seconds describe('GroupPageGuard', () => { const groupsEndpointUrl = 'https://test.org/api/eperson/groups'; @@ -20,42 +30,54 @@ describe('GroupPageGuard', () => { }, } as unknown as ActivatedRouteSnapshot; - let guard: GroupPageGuard; let halEndpointService: HALEndpointService; let authorizationService: AuthorizationDataService; let router: Router; let authService: AuthService; - beforeEach(() => { + function init() { halEndpointService = jasmine.createSpyObj(['getEndpoint']); - (halEndpointService as any).getEndpoint.and.returnValue(observableOf(groupsEndpointUrl)); + ( halEndpointService as any ).getEndpoint.and.returnValue(observableOf(groupsEndpointUrl)); authorizationService = jasmine.createSpyObj(['isAuthorized']); // NOTE: value is set in beforeEach router = jasmine.createSpyObj(['parseUrl']); - (router as any).parseUrl.and.returnValue = {}; + ( router as any ).parseUrl.and.returnValue = {}; authService = jasmine.createSpyObj(['isAuthenticated']); - (authService as any).isAuthenticated.and.returnValue(observableOf(true)); + ( authService as any ).isAuthenticated.and.returnValue(observableOf(true)); - guard = new GroupPageGuard(halEndpointService, authorizationService, router, authService); - }); + TestBed.configureTestingModule({ + providers: [ + { provide: AuthorizationDataService, useValue: authorizationService }, + { provide: Router, useValue: router }, + { provide: AuthService, useValue: authService }, + { provide: HALEndpointService, useValue: halEndpointService }, + ], + }); + } + + beforeEach(waitForAsync(() => { + init(); + })); it('should be created', () => { - expect(guard).toBeTruthy(); + expect(groupPageGuard).toBeTruthy(); }); describe('canActivate', () => { describe('when the current user can manage the group', () => { beforeEach(() => { - (authorizationService as any).isAuthorized.and.returnValue(observableOf(true)); + ( authorizationService as any ).isAuthorized.and.returnValue(observableOf(true)); }); it('should return true', (done) => { - guard.canActivate( - routeSnapshotWithGroupId, { url: 'current-url' } as any, - ).subscribe((result) => { + const result$ = TestBed.runInInjectionContext(() => { + return groupPageGuard()(routeSnapshotWithGroupId, { url: 'current-url' } as any); + }) as Observable; + + result$.subscribe((result) => { expect(authorizationService.isAuthorized).toHaveBeenCalledWith( FeatureID.CanManageGroup, groupEndpointUrl, undefined, ); @@ -71,15 +93,18 @@ describe('GroupPageGuard', () => { }); it('should not return true', (done) => { - guard.canActivate( - routeSnapshotWithGroupId, { url: 'current-url' } as any, - ).subscribe((result) => { + const result$ = TestBed.runInInjectionContext(() => { + return groupPageGuard()(routeSnapshotWithGroupId, { url: 'current-url' } as any); + }) as Observable; + + result$.subscribe((result) => { expect(authorizationService.isAuthorized).toHaveBeenCalledWith( FeatureID.CanManageGroup, groupEndpointUrl, undefined, ); expect(result).not.toBeTrue(); done(); }); + }); }); }); diff --git a/src/app/access-control/group-registry/group-page.guard.ts b/src/app/access-control/group-registry/group-page.guard.ts index 928271887c..c52bed9c48 100644 --- a/src/app/access-control/group-registry/group-page.guard.ts +++ b/src/app/access-control/group-registry/group-page.guard.ts @@ -1,7 +1,7 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, - Router, + CanActivateFn, RouterStateSnapshot, } from '@angular/router'; import { @@ -10,34 +10,29 @@ import { } from 'rxjs'; import { map } from 'rxjs/operators'; -import { AuthService } from '../../core/auth/auth.service'; -import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; -import { SomeFeatureAuthorizationGuard } from '../../core/data/feature-authorization/feature-authorization-guard/some-feature-authorization.guard'; +import { + someFeatureAuthorizationGuard, + StringGuardParamFn, +} from '../../core/data/feature-authorization/feature-authorization-guard/some-feature-authorization.guard'; import { FeatureID } from '../../core/data/feature-authorization/feature-id'; import { HALEndpointService } from '../../core/shared/hal-endpoint.service'; -@Injectable({ - providedIn: 'root', -}) -export class GroupPageGuard extends SomeFeatureAuthorizationGuard { +const defaultGroupPageGetObjectUrl: StringGuardParamFn = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, +): Observable => { + const halEndpointService = inject(HALEndpointService); + const groupsEndpoint = 'groups'; - protected groupsEndpoint = 'groups'; + return halEndpointService.getEndpoint(groupsEndpoint).pipe( + map(groupsUrl => `${groupsUrl}/${route?.params?.groupId}`), + ); +}; - constructor(protected halEndpointService: HALEndpointService, - protected authorizationService: AuthorizationDataService, - protected router: Router, - protected authService: AuthService) { - super(authorizationService, router, authService); - } - - getFeatureIDs(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { - return observableOf([FeatureID.CanManageGroup]); - } - - getObjectUrl(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { - return this.halEndpointService.getEndpoint(this.groupsEndpoint).pipe( - map(groupsUrl => `${groupsUrl}/${route?.params?.groupId}`), - ); - } - -} +export const groupPageGuard = ( + getObjectUrl = defaultGroupPageGetObjectUrl, + getEPersonUuid?: StringGuardParamFn, +): CanActivateFn => someFeatureAuthorizationGuard( + () => observableOf([FeatureID.CanManageGroup]), + getObjectUrl, + getEPersonUuid); diff --git a/src/app/access-control/group-registry/groups-registry.component.html b/src/app/access-control/group-registry/groups-registry.component.html index 2ef67ddf54..c2d998d954 100644 --- a/src/app/access-control/group-registry/groups-registry.component.html +++ b/src/app/access-control/group-registry/groups-registry.component.html @@ -33,11 +33,10 @@
- + diff --git a/src/app/access-control/group-registry/groups-registry.component.spec.ts b/src/app/access-control/group-registry/groups-registry.component.spec.ts index 73bb2fe80a..43c72843e5 100644 --- a/src/app/access-control/group-registry/groups-registry.component.spec.ts +++ b/src/app/access-control/group-registry/groups-registry.component.spec.ts @@ -16,18 +16,22 @@ import { BrowserModule, By, } from '@angular/platform-browser'; -import { Router } from '@angular/router'; -import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { - TranslateLoader, - TranslateModule, -} from '@ngx-translate/core'; + ActivatedRoute, + Router, +} from '@angular/router'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { provideMockStore } from '@ngrx/store/testing'; +import { TranslateModule } from '@ngx-translate/core'; import { Observable, of as observableOf, + of, } from 'rxjs'; +import { APP_DATA_SERVICES_MAP } from '../../../config/app-config.interface'; import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; +import { ConfigurationDataService } from '../../core/data/configuration-data.service'; import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service'; import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; import { FeatureID } from '../../core/data/feature-authorization/feature-id'; @@ -52,7 +56,9 @@ import { } from '../../shared/mocks/dso-name.service.mock'; import { RouterMock } from '../../shared/mocks/router.mock'; import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { PaginationComponent } from '../../shared/pagination/pagination.component'; import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; +import { ActivatedRouteStub } from '../../shared/testing/active-router.stub'; import { EPersonMock, EPersonMock2, @@ -64,7 +70,6 @@ import { import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub'; import { PaginationServiceStub } from '../../shared/testing/pagination-service.stub'; import { routeServiceStub } from '../../shared/testing/route-service.stub'; -import { TranslateLoaderMock } from '../../shared/testing/translate-loader.mock'; import { GroupsRegistryComponent } from './groups-registry.component'; describe('GroupsRegistryComponent', () => { @@ -74,6 +79,7 @@ describe('GroupsRegistryComponent', () => { let groupsDataServiceStub: any; let dsoDataServiceStub: any; let authorizationService: AuthorizationDataService; + let configurationDataService: jasmine.SpyObj; let mockGroups; let mockEPeople; @@ -191,32 +197,41 @@ describe('GroupsRegistryComponent', () => { }, }; + configurationDataService = jasmine.createSpyObj('ConfigurationDataService', { + findByPropertyName: of({ payload: { value: 'test' } }), + }); + authorizationService = jasmine.createSpyObj('authorizationService', ['isAuthorized']); setIsAuthorized(true, true); paginationService = new PaginationServiceStub(); return TestBed.configureTestingModule({ imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule, - TranslateModule.forRoot({ - loader: { - provide: TranslateLoader, - useClass: TranslateLoaderMock, - }, - }), + TranslateModule.forRoot(), + GroupsRegistryComponent, ], - declarations: [GroupsRegistryComponent], providers: [GroupsRegistryComponent, { provide: DSONameService, useValue: new DSONameServiceMock() }, { provide: EPersonDataService, useValue: ePersonDataServiceStub }, { provide: GroupDataService, useValue: groupsDataServiceStub }, { provide: DSpaceObjectDataService, useValue: dsoDataServiceStub }, { provide: NotificationsService, useValue: new NotificationsServiceStub() }, + { provide: ConfigurationDataService, useValue: configurationDataService }, { provide: RouteService, useValue: routeServiceStub }, + { provide: ActivatedRoute, useValue: new ActivatedRouteStub() }, { provide: Router, useValue: new RouterMock() }, { provide: AuthorizationDataService, useValue: authorizationService }, { provide: PaginationService, useValue: paginationService }, { provide: RequestService, useValue: jasmine.createSpyObj('requestService', ['removeByHrefSubstring']) }, + { provide: APP_DATA_SERVICES_MAP, useValue: {} }, + provideMockStore(), ], schemas: [NO_ERRORS_SCHEMA], + }).overrideComponent(GroupsRegistryComponent, { + remove: { + imports: [ + PaginationComponent, + ], + }, }).compileComponents(); })); diff --git a/src/app/access-control/group-registry/groups-registry.component.ts b/src/app/access-control/group-registry/groups-registry.component.ts index 24d6f9e4a9..dec3dc955d 100644 --- a/src/app/access-control/group-registry/groups-registry.component.ts +++ b/src/app/access-control/group-registry/groups-registry.component.ts @@ -1,11 +1,25 @@ +import { + AsyncPipe, + NgForOf, + NgIf, + NgSwitch, + NgSwitchCase, +} from '@angular/common'; import { Component, OnDestroy, OnInit, } from '@angular/core'; -import { UntypedFormBuilder } from '@angular/forms'; -import { Router } from '@angular/router'; -import { TranslateService } from '@ngx-translate/core'; +import { + ReactiveFormsModule, + UntypedFormBuilder, +} from '@angular/forms'; +import { RouterLink } from '@angular/router'; +import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'; +import { + TranslateModule, + TranslateService, +} from '@ngx-translate/core'; import { BehaviorSubject, combineLatest as observableCombineLatest, @@ -49,13 +63,29 @@ import { } from '../../core/shared/operators'; import { PageInfo } from '../../core/shared/page-info.model'; import { hasValue } from '../../shared/empty.util'; +import { ThemedLoadingComponent } from '../../shared/loading/themed-loading.component'; import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { PaginationComponent } from '../../shared/pagination/pagination.component'; import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; import { followLink } from '../../shared/utils/follow-link-config.model'; @Component({ selector: 'ds-groups-registry', templateUrl: './groups-registry.component.html', + imports: [ + ThemedLoadingComponent, + TranslateModule, + RouterLink, + ReactiveFormsModule, + AsyncPipe, + NgIf, + PaginationComponent, + NgSwitch, + NgSwitchCase, + NgbTooltipModule, + NgForOf, + ], + standalone: true, }) /** * A component used for managing all existing groups within the repository. @@ -116,7 +146,6 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy { private notificationsService: NotificationsService, private formBuilder: UntypedFormBuilder, protected routeService: RouteService, - private router: Router, private authorizationService: AuthorizationDataService, private paginationService: PaginationService, public requestService: RequestService, diff --git a/src/app/admin/admin-curation-tasks/admin-curation-tasks.component.spec.ts b/src/app/admin/admin-curation-tasks/admin-curation-tasks.component.spec.ts index 5aabe0fda0..c2981f1fb6 100644 --- a/src/app/admin/admin-curation-tasks/admin-curation-tasks.component.spec.ts +++ b/src/app/admin/admin-curation-tasks/admin-curation-tasks.component.spec.ts @@ -6,6 +6,7 @@ import { } from '@angular/core/testing'; import { TranslateModule } from '@ngx-translate/core'; +import { CurationFormComponent } from '../../curation-form/curation-form.component'; import { AdminCurationTasksComponent } from './admin-curation-tasks.component'; describe('AdminCurationTasksComponent', () => { @@ -14,10 +15,15 @@ describe('AdminCurationTasksComponent', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [TranslateModule.forRoot()], - declarations: [AdminCurationTasksComponent], + imports: [TranslateModule.forRoot(), AdminCurationTasksComponent], schemas: [CUSTOM_ELEMENTS_SCHEMA], - }).compileComponents(); + }) + .overrideComponent(AdminCurationTasksComponent, { + remove: { + imports: [CurationFormComponent], + }, + }) + .compileComponents(); })); beforeEach(() => { diff --git a/src/app/admin/admin-curation-tasks/admin-curation-tasks.component.ts b/src/app/admin/admin-curation-tasks/admin-curation-tasks.component.ts index 9a80f341b9..657f972468 100644 --- a/src/app/admin/admin-curation-tasks/admin-curation-tasks.component.ts +++ b/src/app/admin/admin-curation-tasks/admin-curation-tasks.component.ts @@ -1,4 +1,7 @@ import { Component } from '@angular/core'; +import { TranslateModule } from '@ngx-translate/core'; + +import { CurationFormComponent } from '../../curation-form/curation-form.component'; /** * Component responsible for rendering the system wide Curation Task UI @@ -6,6 +9,11 @@ import { Component } from '@angular/core'; @Component({ selector: 'ds-admin-curation-task', templateUrl: './admin-curation-tasks.component.html', + imports: [ + CurationFormComponent, + TranslateModule, + ], + standalone: true, }) export class AdminCurationTasksComponent { diff --git a/src/app/admin/admin-import-batch-page/batch-import-page.component.spec.ts b/src/app/admin/admin-import-batch-page/batch-import-page.component.spec.ts index 752bd52e4d..20c53f58ed 100644 --- a/src/app/admin/admin-import-batch-page/batch-import-page.component.spec.ts +++ b/src/app/admin/admin-import-batch-page/batch-import-page.component.spec.ts @@ -23,6 +23,7 @@ import { createSuccessfulRemoteDataObject$, } from '../../shared/remote-data.utils'; import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub'; +import { FileDropzoneNoUploaderComponent } from '../../shared/upload/file-dropzone-no-uploader/file-dropzone-no-uploader.component'; import { FileValueAccessorDirective } from '../../shared/utils/file-value-accessor.directive'; import { FileValidator } from '../../shared/utils/require-file.validator'; import { BatchImportPageComponent } from './batch-import-page.component'; @@ -58,8 +59,8 @@ describe('BatchImportPageComponent', () => { FormsModule, TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), + BatchImportPageComponent, FileValueAccessorDirective, FileValidator, ], - declarations: [BatchImportPageComponent, FileValueAccessorDirective, FileValidator], providers: [ { provide: NotificationsService, useValue: notificationService }, { provide: ScriptDataService, useValue: scriptService }, @@ -67,7 +68,13 @@ describe('BatchImportPageComponent', () => { { provide: Location, useValue: locationStub }, ], schemas: [NO_ERRORS_SCHEMA], - }).compileComponents(); + }) + .overrideComponent(BatchImportPageComponent, { + remove: { + imports: [FileDropzoneNoUploaderComponent], + }, + }) + .compileComponents(); })); beforeEach(() => { diff --git a/src/app/admin/admin-import-batch-page/batch-import-page.component.ts b/src/app/admin/admin-import-batch-page/batch-import-page.component.ts index 2712610fd1..1f54b801c8 100644 --- a/src/app/admin/admin-import-batch-page/batch-import-page.component.ts +++ b/src/app/admin/admin-import-batch-page/batch-import-page.component.ts @@ -1,8 +1,16 @@ -import { Location } from '@angular/common'; +import { + Location, + NgIf, +} from '@angular/common'; import { Component } from '@angular/core'; +import { FormsModule } from '@angular/forms'; import { Router } from '@angular/router'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { TranslateService } from '@ngx-translate/core'; +import { + TranslateModule, + TranslateService, +} from '@ngx-translate/core'; +import { UiSwitchModule } from 'ngx-ui-switch'; import { take } from 'rxjs/operators'; import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; @@ -22,10 +30,19 @@ import { isNotEmpty, } from '../../shared/empty.util'; import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { FileDropzoneNoUploaderComponent } from '../../shared/upload/file-dropzone-no-uploader/file-dropzone-no-uploader.component'; @Component({ selector: 'ds-batch-import-page', templateUrl: './batch-import-page.component.html', + imports: [ + NgIf, + TranslateModule, + FormsModule, + UiSwitchModule, + FileDropzoneNoUploaderComponent, + ], + standalone: true, }) export class BatchImportPageComponent { /** diff --git a/src/app/admin/admin-import-metadata-page/metadata-import-page.component.spec.ts b/src/app/admin/admin-import-metadata-page/metadata-import-page.component.spec.ts index 131b4561c1..b345da2c06 100644 --- a/src/app/admin/admin-import-metadata-page/metadata-import-page.component.spec.ts +++ b/src/app/admin/admin-import-metadata-page/metadata-import-page.component.spec.ts @@ -23,6 +23,7 @@ import { createSuccessfulRemoteDataObject$, } from '../../shared/remote-data.utils'; import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub'; +import { FileDropzoneNoUploaderComponent } from '../../shared/upload/file-dropzone-no-uploader/file-dropzone-no-uploader.component'; import { FileValueAccessorDirective } from '../../shared/utils/file-value-accessor.directive'; import { FileValidator } from '../../shared/utils/require-file.validator'; import { MetadataImportPageComponent } from './metadata-import-page.component'; @@ -58,8 +59,8 @@ describe('MetadataImportPageComponent', () => { FormsModule, TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), + MetadataImportPageComponent, FileValueAccessorDirective, FileValidator, ], - declarations: [MetadataImportPageComponent, FileValueAccessorDirective, FileValidator], providers: [ { provide: NotificationsService, useValue: notificationService }, { provide: ScriptDataService, useValue: scriptService }, @@ -67,7 +68,13 @@ describe('MetadataImportPageComponent', () => { { provide: Location, useValue: locationStub }, ], schemas: [NO_ERRORS_SCHEMA], - }).compileComponents(); + }) + .overrideComponent(MetadataImportPageComponent, { + remove: { + imports: [FileDropzoneNoUploaderComponent], + }, + }) + .compileComponents(); })); beforeEach(() => { diff --git a/src/app/admin/admin-import-metadata-page/metadata-import-page.component.ts b/src/app/admin/admin-import-metadata-page/metadata-import-page.component.ts index e554459286..56d504d57d 100644 --- a/src/app/admin/admin-import-metadata-page/metadata-import-page.component.ts +++ b/src/app/admin/admin-import-metadata-page/metadata-import-page.component.ts @@ -1,7 +1,11 @@ import { Location } from '@angular/common'; import { Component } from '@angular/core'; +import { FormsModule } from '@angular/forms'; import { Router } from '@angular/router'; -import { TranslateService } from '@ngx-translate/core'; +import { + TranslateModule, + TranslateService, +} from '@ngx-translate/core'; import { METADATA_IMPORT_SCRIPT_NAME, @@ -14,10 +18,17 @@ import { Process } from '../../process-page/processes/process.model'; import { ProcessParameter } from '../../process-page/processes/process-parameter.model'; import { isNotEmpty } from '../../shared/empty.util'; import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { FileDropzoneNoUploaderComponent } from '../../shared/upload/file-dropzone-no-uploader/file-dropzone-no-uploader.component'; @Component({ - selector: 'ds-metadata-import-page', + selector: 'ds-base-metadata-import-page', templateUrl: './metadata-import-page.component.html', + imports: [ + FileDropzoneNoUploaderComponent, + FormsModule, + TranslateModule, + ], + standalone: true, }) /** diff --git a/src/app/admin/admin-import-metadata-page/themed-metadata-import-page.component.ts b/src/app/admin/admin-import-metadata-page/themed-metadata-import-page.component.ts new file mode 100644 index 0000000000..2562541dfc --- /dev/null +++ b/src/app/admin/admin-import-metadata-page/themed-metadata-import-page.component.ts @@ -0,0 +1,27 @@ +import { Component } from '@angular/core'; + +import { ThemedComponent } from '../../shared/theme-support/themed.component'; +import { MetadataImportPageComponent } from './metadata-import-page.component'; + +/** + * Themed wrapper for {@link MetadataImportPageComponent}. + */ +@Component({ + selector: 'ds-metadata-import-page', + templateUrl: '../../shared/theme-support/themed.component.html', + standalone: true, + imports: [MetadataImportPageComponent], +}) +export class ThemedMetadataImportPageComponent extends ThemedComponent { + protected getComponentName(): string { + return 'MetadataImportPageComponent'; + } + + protected importThemedComponent(themeName: string): Promise { + return import(`../../../themes/${themeName}/app/admin/admin-import-metadata-page/metadata-import-page.component`); + } + + protected importUnthemedComponent(): Promise { + return import('./metadata-import-page.component'); + } +} diff --git a/src/app/admin/admin-ldn-services/admin-ldn-services-routes.ts b/src/app/admin/admin-ldn-services/admin-ldn-services-routes.ts new file mode 100644 index 0000000000..66420f7a7b --- /dev/null +++ b/src/app/admin/admin-ldn-services/admin-ldn-services-routes.ts @@ -0,0 +1,38 @@ +import { Routes } from '@angular/router'; + +import { i18nBreadcrumbResolver } from '../../core/breadcrumbs/i18n-breadcrumb.resolver'; +import { navigationBreadcrumbResolver } from '../../core/breadcrumbs/navigation-breadcrumb.resolver'; +import { LdnServiceFormComponent } from './ldn-service-form/ldn-service-form.component'; +import { LdnServicesOverviewComponent } from './ldn-services-directory/ldn-services-directory.component'; + +const moduleRoutes: Routes = [ + { + path: '', + pathMatch: 'full', + component: LdnServicesOverviewComponent, + resolve: { breadcrumb: i18nBreadcrumbResolver }, + data: { title: 'ldn-registered-services.title', breadcrumbKey: 'ldn-registered-services.new' }, + }, + { + path: 'new', + resolve: { breadcrumb: navigationBreadcrumbResolver }, + component: LdnServiceFormComponent, + data: { title: 'ldn-register-new-service.title', breadcrumbKey: 'ldn-register-new-service' }, + }, + { + path: 'edit/:serviceId', + resolve: { breadcrumb: navigationBreadcrumbResolver }, + component: LdnServiceFormComponent, + data: { title: 'ldn-edit-service.title', breadcrumbKey: 'ldn-edit-service' }, + }, +]; + +export const ROUTES = moduleRoutes.map(route => { + return { ...route, data: { + ...route.data, + relatedRoutes: moduleRoutes.filter(relatedRoute => relatedRoute.path !== route.path) + .map((relatedRoute) => { + return { path: relatedRoute.path, data: relatedRoute.data }; + }), + } }; +}); diff --git a/src/app/admin/admin-ldn-services/admin-ldn-services-routing.module.ts b/src/app/admin/admin-ldn-services/admin-ldn-services-routing.module.ts deleted file mode 100644 index c1bdff9818..0000000000 --- a/src/app/admin/admin-ldn-services/admin-ldn-services-routing.module.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { NgModule } from '@angular/core'; -import { - RouterModule, - Routes, -} from '@angular/router'; - -import { I18nBreadcrumbResolver } from '../../core/breadcrumbs/i18n-breadcrumb.resolver'; -import { NavigationBreadcrumbResolver } from '../../core/breadcrumbs/navigation-breadcrumb.resolver'; -import { LdnServiceFormComponent } from './ldn-service-form/ldn-service-form.component'; -import { LdnServicesOverviewComponent } from './ldn-services-directory/ldn-services-directory.component'; - -const moduleRoutes: Routes = [ - { - path: '', - pathMatch: 'full', - component: LdnServicesOverviewComponent, - resolve: { breadcrumb: I18nBreadcrumbResolver }, - data: { title: 'ldn-registered-services.title', breadcrumbKey: 'ldn-registered-services.new' }, - }, - { - path: 'new', - resolve: { breadcrumb: NavigationBreadcrumbResolver }, - component: LdnServiceFormComponent, - data: { title: 'ldn-register-new-service.title', breadcrumbKey: 'ldn-register-new-service' }, - }, - { - path: 'edit/:serviceId', - resolve: { breadcrumb: NavigationBreadcrumbResolver }, - component: LdnServiceFormComponent, - data: { title: 'ldn-edit-service.title', breadcrumbKey: 'ldn-edit-service' }, - }, -]; - - -@NgModule({ - imports: [ - RouterModule.forChild(moduleRoutes.map(route => { - return { ...route, data: { - ...route.data, - relatedRoutes: moduleRoutes.filter(relatedRoute => relatedRoute.path !== route.path) - .map((relatedRoute) => { - return { path: relatedRoute.path, data: relatedRoute.data }; - }), - } }; - })), - ], -}) -export class AdminLdnServicesRoutingModule { - -} diff --git a/src/app/admin/admin-ldn-services/admin-ldn-services.module.ts b/src/app/admin/admin-ldn-services/admin-ldn-services.module.ts deleted file mode 100644 index 59875158ef..0000000000 --- a/src/app/admin/admin-ldn-services/admin-ldn-services.module.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { CommonModule } from '@angular/common'; -import { NgModule } from '@angular/core'; -import { FormsModule } from '@angular/forms'; - -import { SharedModule } from '../../shared/shared.module'; -import { AdminLdnServicesRoutingModule } from './admin-ldn-services-routing.module'; -import { LdnServiceFormComponent } from './ldn-service-form/ldn-service-form.component'; -import { LdnItemfiltersService } from './ldn-services-data/ldn-itemfilters-data.service'; -import { LdnServicesOverviewComponent } from './ldn-services-directory/ldn-services-directory.component'; - -@NgModule({ - imports: [ - CommonModule, - SharedModule, - AdminLdnServicesRoutingModule, - FormsModule, - ], - declarations: [ - LdnServicesOverviewComponent, - LdnServiceFormComponent, - ], - providers: [LdnItemfiltersService], -}) -export class AdminLdnServicesModule { -} diff --git a/src/app/admin/admin-ldn-services/ldn-service-form/ldn-service-form.component.html b/src/app/admin/admin-ldn-services/ldn-service-form/ldn-service-form.component.html index 0a7bc39fa3..a980182882 100644 --- a/src/app/admin/admin-ldn-services/ldn-service-form/ldn-service-form.component.html +++ b/src/app/admin/admin-ldn-services/ldn-service-form/ldn-service-form.component.html @@ -212,7 +212,7 @@ {{'ldn-service.control-constaint-select-none' | translate}} + -
@@ -294,7 +298,7 @@ diff --git a/src/app/admin/admin-registries/bitstream-formats/bitstream-formats.component.spec.ts b/src/app/admin/admin-registries/bitstream-formats/bitstream-formats.component.spec.ts index b63a6213eb..c6d619b611 100644 --- a/src/app/admin/admin-registries/bitstream-formats/bitstream-formats.component.spec.ts +++ b/src/app/admin/admin-registries/bitstream-formats/bitstream-formats.component.spec.ts @@ -1,26 +1,20 @@ -import { CommonModule } from '@angular/common'; +import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; import { ComponentFixture, TestBed, waitForAsync, } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; -import { RouterTestingModule } from '@angular/router/testing'; -import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { RouterModule } from '@angular/router'; +import { provideMockStore } from '@ngrx/store/testing'; import { TranslateModule } from '@ngx-translate/core'; -import { - cold, - getTestScheduler, - hot, -} from 'jasmine-marbles'; +import { hot } from 'jasmine-marbles'; import { of as observableOf } from 'rxjs'; -import { TestScheduler } from 'rxjs/testing'; import { BitstreamFormatDataService } from '../../../core/data/bitstream-format-data.service'; import { PaginationService } from '../../../core/pagination/pagination.service'; import { BitstreamFormat } from '../../../core/shared/bitstream-format.model'; import { BitstreamFormatSupportLevel } from '../../../core/shared/bitstream-format-support-level'; -import { HostWindowService } from '../../../shared/host-window.service'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { PaginationComponent } from '../../../shared/pagination/pagination.component'; import { @@ -29,7 +23,6 @@ import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$, } from '../../../shared/remote-data.utils'; -import { HostWindowServiceStub } from '../../../shared/testing/host-window-service.stub'; import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub'; import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub'; import { createPaginatedList } from '../../../shared/testing/utils.test'; @@ -40,7 +33,6 @@ describe('BitstreamFormatsComponent', () => { let comp: BitstreamFormatsComponent; let fixture: ComponentFixture; let bitstreamFormatService; - let scheduler: TestScheduler; let notificationsServiceStub; let paginationService; @@ -95,8 +87,6 @@ describe('BitstreamFormatsComponent', () => { const initAsync = () => { notificationsServiceStub = new NotificationsServiceStub(); - scheduler = getTestScheduler(); - bitstreamFormatService = jasmine.createSpyObj('bitstreamFormatService', { findAll: observableOf(mockFormatsRD), find: createSuccessfulRemoteDataObject$(mockFormatsList[0]), @@ -111,14 +101,25 @@ describe('BitstreamFormatsComponent', () => { paginationService = new PaginationServiceStub(); TestBed.configureTestingModule({ - imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule], - declarations: [BitstreamFormatsComponent, PaginationComponent, EnumKeysPipe], + imports: [ + BitstreamFormatsComponent, + EnumKeysPipe, + RouterModule.forRoot([]), + TranslateModule.forRoot(), + ], providers: [ + provideMockStore(), { provide: BitstreamFormatDataService, useValue: bitstreamFormatService }, - { provide: HostWindowService, useValue: new HostWindowServiceStub(0) }, { provide: NotificationsService, useValue: notificationsServiceStub }, { provide: PaginationService, useValue: paginationService }, ], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + }).overrideComponent(BitstreamFormatsComponent, { + remove: { + imports: [ + PaginationComponent, + ], + }, }).compileComponents(); }; @@ -186,18 +187,18 @@ describe('BitstreamFormatsComponent', () => { describe('isSelected', () => { beforeEach(waitForAsync(initAsync)); beforeEach(initBeforeEach); - it('should return an observable of true if the provided bistream is in the list returned by the service', () => { - const result = comp.isSelected(bitstreamFormat1); - - expect(result).toBeObservable(cold('b', { b: true })); + it('should return an observable of true if the provided bitstream is in the list returned by the service', () => { + comp.selectedBitstreamFormatIDs().subscribe((selectedBitstreamFormatIDs: string[]) => { + expect(selectedBitstreamFormatIDs).toContain(bitstreamFormat1.id); + }); }); - it('should return an observable of false if the provided bistream is not in the list returned by the service', () => { + it('should return an observable of false if the provided bitstream is not in the list returned by the service', () => { const format = new BitstreamFormat(); format.uuid = 'new'; - const result = comp.isSelected(format); - - expect(result).toBeObservable(cold('b', { b: false })); + comp.selectedBitstreamFormatIDs().subscribe((selectedBitstreamFormatIDs: string[]) => { + expect(selectedBitstreamFormatIDs).not.toContain(format.id); + }); }); }); @@ -223,8 +224,6 @@ describe('BitstreamFormatsComponent', () => { beforeEach(waitForAsync(() => { notificationsServiceStub = new NotificationsServiceStub(); - scheduler = getTestScheduler(); - bitstreamFormatService = jasmine.createSpyObj('bitstreamFormatService', { findAll: observableOf(mockFormatsRD), find: createSuccessfulRemoteDataObject$(mockFormatsList[0]), @@ -239,15 +238,25 @@ describe('BitstreamFormatsComponent', () => { paginationService = new PaginationServiceStub(); TestBed.configureTestingModule({ - imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule], - declarations: [BitstreamFormatsComponent, PaginationComponent, EnumKeysPipe], + imports: [ + BitstreamFormatsComponent, + EnumKeysPipe, + PaginationComponent, + RouterModule.forRoot([]), + TranslateModule.forRoot(), + ], providers: [ + provideMockStore(), { provide: BitstreamFormatDataService, useValue: bitstreamFormatService }, - { provide: HostWindowService, useValue: new HostWindowServiceStub(0) }, { provide: NotificationsService, useValue: notificationsServiceStub }, { provide: PaginationService, useValue: paginationService }, ], - }).compileComponents(); + schemas: [CUSTOM_ELEMENTS_SCHEMA], + }) + .overrideComponent(BitstreamFormatsComponent, { + remove: { imports: [PaginationComponent] }, + }) + .compileComponents(); }, )); @@ -272,8 +281,6 @@ describe('BitstreamFormatsComponent', () => { beforeEach(waitForAsync(() => { notificationsServiceStub = new NotificationsServiceStub(); - scheduler = getTestScheduler(); - bitstreamFormatService = jasmine.createSpyObj('bitstreamFormatService', { findAll: observableOf(mockFormatsRD), find: createSuccessfulRemoteDataObject$(mockFormatsList[0]), @@ -288,15 +295,23 @@ describe('BitstreamFormatsComponent', () => { paginationService = new PaginationServiceStub(); TestBed.configureTestingModule({ - imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule], - declarations: [BitstreamFormatsComponent, PaginationComponent, EnumKeysPipe], + imports: [ + BitstreamFormatsComponent, + EnumKeysPipe, + RouterModule.forRoot([]), + TranslateModule.forRoot(), + ], providers: [ { provide: BitstreamFormatDataService, useValue: bitstreamFormatService }, - { provide: HostWindowService, useValue: new HostWindowServiceStub(0) }, { provide: NotificationsService, useValue: notificationsServiceStub }, { provide: PaginationService, useValue: paginationService }, ], - }).compileComponents(); + schemas: [CUSTOM_ELEMENTS_SCHEMA], + }) + .overrideComponent(BitstreamFormatsComponent, { + remove: { imports: [PaginationComponent] }, + }) + .compileComponents(); }, )); diff --git a/src/app/admin/admin-registries/bitstream-formats/bitstream-formats.component.ts b/src/app/admin/admin-registries/bitstream-formats/bitstream-formats.component.ts index 522f466735..74869670c5 100644 --- a/src/app/admin/admin-registries/bitstream-formats/bitstream-formats.component.ts +++ b/src/app/admin/admin-registries/bitstream-formats/bitstream-formats.component.ts @@ -1,14 +1,19 @@ +import { + AsyncPipe, + NgForOf, + NgIf, +} from '@angular/common'; import { Component, OnDestroy, OnInit, } from '@angular/core'; -import { Router } from '@angular/router'; -import { TranslateService } from '@ngx-translate/core'; +import { RouterLink } from '@angular/router'; import { - combineLatest as observableCombineLatest, - Observable, -} from 'rxjs'; + TranslateModule, + TranslateService, +} from '@ngx-translate/core'; +import { Observable } from 'rxjs'; import { map, mergeMap, @@ -26,6 +31,7 @@ import { BitstreamFormat } from '../../../core/shared/bitstream-format.model'; import { NoContent } from '../../../core/shared/NoContent.model'; import { getFirstCompletedRemoteData } from '../../../core/shared/operators'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; +import { PaginationComponent } from '../../../shared/pagination/pagination.component'; import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model'; /** @@ -34,13 +40,27 @@ import { PaginationComponentOptions } from '../../../shared/pagination/paginatio @Component({ selector: 'ds-bitstream-formats', templateUrl: './bitstream-formats.component.html', + imports: [ + NgIf, + AsyncPipe, + RouterLink, + TranslateModule, + PaginationComponent, + NgForOf, + ], + standalone: true, }) export class BitstreamFormatsComponent implements OnInit, OnDestroy { /** * A paginated list of bitstream formats to be shown on the page */ - bitstreamFormats: Observable>>; + bitstreamFormats$: Observable>>; + + /** + * The currently selected {@link BitstreamFormat} IDs + */ + selectedBitstreamFormatIDs$: Observable; /** * The current pagination configuration for the page @@ -53,7 +73,6 @@ export class BitstreamFormatsComponent implements OnInit, OnDestroy { }); constructor(private notificationsService: NotificationsService, - private router: Router, private translateService: TranslateService, private bitstreamFormatService: BitstreamFormatDataService, private paginationService: PaginationService, @@ -101,21 +120,18 @@ export class BitstreamFormatsComponent implements OnInit, OnDestroy { } /** - * Deselects all selecetd bitstream formats + * Deselects all selected bitstream formats */ deselectAll() { this.bitstreamFormatService.deselectAllBitstreamFormats(); } /** - * Checks whether a given bitstream format is selected in the list (checkbox) - * @param bitstreamFormat + * Returns the list of all the bitstream formats that are selected in the list (checkbox) */ - isSelected(bitstreamFormat: BitstreamFormat): Observable { + selectedBitstreamFormatIDs(): Observable { return this.bitstreamFormatService.getSelectedBitstreamFormats().pipe( - map((bitstreamFormats: BitstreamFormat[]) => { - return bitstreamFormats.find((selectedFormat) => selectedFormat.id === bitstreamFormat.id) != null; - }), + map((bitstreamFormats: BitstreamFormat[]) => bitstreamFormats.map((selectedFormat) => selectedFormat.id)), ); } @@ -139,27 +155,23 @@ export class BitstreamFormatsComponent implements OnInit, OnDestroy { const prefix = 'admin.registries.bitstream-formats.delete'; const suffix = success ? 'success' : 'failure'; - const messages = observableCombineLatest( - this.translateService.get(`${prefix}.${suffix}.head`), - this.translateService.get(`${prefix}.${suffix}.amount`, { amount: amount }), - ); - messages.subscribe(([head, content]) => { + const head: string = this.translateService.instant(`${prefix}.${suffix}.head`); + const content: string = this.translateService.instant(`${prefix}.${suffix}.amount`, { amount: amount }); - if (success) { - this.notificationsService.success(head, content); - } else { - this.notificationsService.error(head, content); - } - }); + if (success) { + this.notificationsService.success(head, content); + } else { + this.notificationsService.error(head, content); + } } ngOnInit(): void { - - this.bitstreamFormats = this.paginationService.getFindListOptions(this.pageConfig.id, this.pageConfig).pipe( + this.bitstreamFormats$ = this.paginationService.getFindListOptions(this.pageConfig.id, this.pageConfig).pipe( switchMap((findListOptions: FindListOptions) => { return this.bitstreamFormatService.findAll(findListOptions); }), ); + this.selectedBitstreamFormatIDs$ = this.selectedBitstreamFormatIDs(); } diff --git a/src/app/admin/admin-registries/bitstream-formats/bitstream-formats.module.ts b/src/app/admin/admin-registries/bitstream-formats/bitstream-formats.module.ts deleted file mode 100644 index 4c8b3284b0..0000000000 --- a/src/app/admin/admin-registries/bitstream-formats/bitstream-formats.module.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { CommonModule } from '@angular/common'; -import { NgModule } from '@angular/core'; -import { RouterModule } from '@angular/router'; - -import { FormModule } from '../../../shared/form/form.module'; -import { SharedModule } from '../../../shared/shared.module'; -import { AddBitstreamFormatComponent } from './add-bitstream-format/add-bitstream-format.component'; -import { BitstreamFormatsComponent } from './bitstream-formats.component'; -import { BitstreamFormatsRoutingModule } from './bitstream-formats-routing.module'; -import { EditBitstreamFormatComponent } from './edit-bitstream-format/edit-bitstream-format.component'; -import { FormatFormComponent } from './format-form/format-form.component'; - -@NgModule({ - imports: [ - CommonModule, - SharedModule, - RouterModule, - BitstreamFormatsRoutingModule, - FormModule, - ], - declarations: [ - BitstreamFormatsComponent, - EditBitstreamFormatComponent, - AddBitstreamFormatComponent, - FormatFormComponent, - ], -}) -export class BitstreamFormatsModule { - -} diff --git a/src/app/admin/admin-registries/bitstream-formats/bitstream-formats.resolver.ts b/src/app/admin/admin-registries/bitstream-formats/bitstream-formats.resolver.ts index 4ab186cdc6..366f5a682b 100644 --- a/src/app/admin/admin-registries/bitstream-formats/bitstream-formats.resolver.ts +++ b/src/app/admin/admin-registries/bitstream-formats/bitstream-formats.resolver.ts @@ -1,7 +1,7 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, - Resolve, + ResolveFn, RouterStateSnapshot, } from '@angular/router'; import { Observable } from 'rxjs'; @@ -12,24 +12,20 @@ import { BitstreamFormat } from '../../../core/shared/bitstream-format.model'; import { getFirstCompletedRemoteData } from '../../../core/shared/operators'; /** - * This class represents a resolver that requests a specific bitstreamFormat before the route is activated + * Method for resolving an bitstreamFormat based on the parameters in the current route + * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot + * @param {RouterStateSnapshot} state + * @param {BitstreamFormatDataService} bitstreamFormatDataService The BitstreamFormatDataService + * @returns Observable<> Emits the found bitstreamFormat based on the parameters in the current route, + * or an error if something went wrong */ -@Injectable() -export class BitstreamFormatsResolver implements Resolve> { - constructor(private bitstreamFormatDataService: BitstreamFormatDataService) { - } - - /** - * Method for resolving an bitstreamFormat based on the parameters in the current route - * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot - * @param {RouterStateSnapshot} state The current RouterStateSnapshot - * @returns Observable<> Emits the found bitstreamFormat based on the parameters in the current route, - * or an error if something went wrong - */ - resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable> { - return this.bitstreamFormatDataService.findById(route.params.id) - .pipe( - getFirstCompletedRemoteData(), - ); - } -} +export const bitstreamFormatsResolver: ResolveFn> = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + bitstreamFormatDataService: BitstreamFormatDataService = inject(BitstreamFormatDataService), +): Observable> => { + return bitstreamFormatDataService.findById(route.params.id) + .pipe( + getFirstCompletedRemoteData(), + ); +}; diff --git a/src/app/admin/admin-registries/bitstream-formats/edit-bitstream-format/edit-bitstream-format.component.spec.ts b/src/app/admin/admin-registries/bitstream-formats/edit-bitstream-format/edit-bitstream-format.component.spec.ts index 5df6f74c13..17276ef2ac 100644 --- a/src/app/admin/admin-registries/bitstream-formats/edit-bitstream-format/edit-bitstream-format.component.spec.ts +++ b/src/app/admin/admin-registries/bitstream-formats/edit-bitstream-format/edit-bitstream-format.component.spec.ts @@ -26,6 +26,7 @@ import { } from '../../../../shared/remote-data.utils'; import { NotificationsServiceStub } from '../../../../shared/testing/notifications-service.stub'; import { RouterStub } from '../../../../shared/testing/router.stub'; +import { FormatFormComponent } from '../format-form/format-form.component'; import { EditBitstreamFormatComponent } from './edit-bitstream-format.component'; describe('EditBitstreamFormatComponent', () => { @@ -60,16 +61,22 @@ describe('EditBitstreamFormatComponent', () => { }); TestBed.configureTestingModule({ - imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule], - declarations: [EditBitstreamFormatComponent], + imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule, EditBitstreamFormatComponent], providers: [ { provide: ActivatedRoute, useValue: routeStub }, { provide: Router, useValue: router }, { provide: NotificationsService, useValue: notificationService }, { provide: BitstreamFormatDataService, useValue: bitstreamFormatDataService }, + ], schemas: [CUSTOM_ELEMENTS_SCHEMA], - }).compileComponents(); + }) + .overrideComponent(EditBitstreamFormatComponent, { + remove: { + imports: [FormatFormComponent], + }, + }) + .compileComponents(); }; const initBeforeEach = () => { @@ -111,8 +118,7 @@ describe('EditBitstreamFormatComponent', () => { }); TestBed.configureTestingModule({ - imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule], - declarations: [EditBitstreamFormatComponent], + imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule, EditBitstreamFormatComponent], providers: [ { provide: ActivatedRoute, useValue: routeStub }, { provide: Router, useValue: router }, @@ -120,7 +126,13 @@ describe('EditBitstreamFormatComponent', () => { { provide: BitstreamFormatDataService, useValue: bitstreamFormatDataService }, ], schemas: [CUSTOM_ELEMENTS_SCHEMA], - }).compileComponents(); + }) + .overrideComponent(EditBitstreamFormatComponent, { + remove: { + imports: [FormatFormComponent], + }, + }) + .compileComponents(); })); beforeEach(initBeforeEach); it('should send the updated form to the service, show a notification and navigate to ', () => { diff --git a/src/app/admin/admin-registries/bitstream-formats/edit-bitstream-format/edit-bitstream-format.component.ts b/src/app/admin/admin-registries/bitstream-formats/edit-bitstream-format/edit-bitstream-format.component.ts index e7d1d501af..f932d83277 100644 --- a/src/app/admin/admin-registries/bitstream-formats/edit-bitstream-format/edit-bitstream-format.component.ts +++ b/src/app/admin/admin-registries/bitstream-formats/edit-bitstream-format/edit-bitstream-format.component.ts @@ -1,3 +1,4 @@ +import { AsyncPipe } from '@angular/common'; import { Component, OnInit, @@ -6,7 +7,10 @@ import { ActivatedRoute, Router, } from '@angular/router'; -import { TranslateService } from '@ngx-translate/core'; +import { + TranslateModule, + TranslateService, +} from '@ngx-translate/core'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; @@ -16,6 +20,7 @@ import { BitstreamFormat } from '../../../../core/shared/bitstream-format.model' import { getFirstCompletedRemoteData } from '../../../../core/shared/operators'; import { NotificationsService } from '../../../../shared/notifications/notifications.service'; import { getBitstreamFormatsModuleRoute } from '../../admin-registries-routing-paths'; +import { FormatFormComponent } from '../format-form/format-form.component'; /** * This component renders the edit page of a bitstream format. @@ -24,6 +29,12 @@ import { getBitstreamFormatsModuleRoute } from '../../admin-registries-routing-p @Component({ selector: 'ds-edit-bitstream-format', templateUrl: './edit-bitstream-format.component.html', + imports: [ + FormatFormComponent, + TranslateModule, + AsyncPipe, + ], + standalone: true, }) export class EditBitstreamFormatComponent implements OnInit { diff --git a/src/app/admin/admin-registries/bitstream-formats/format-form/format-form.component.spec.ts b/src/app/admin/admin-registries/bitstream-formats/format-form/format-form.component.spec.ts index ceeb313a9d..2b9f5034fe 100644 --- a/src/app/admin/admin-registries/bitstream-formats/format-form/format-form.component.spec.ts +++ b/src/app/admin/admin-registries/bitstream-formats/format-form/format-form.component.spec.ts @@ -22,6 +22,7 @@ import { TranslateModule } from '@ngx-translate/core'; import { BitstreamFormat } from '../../../../core/shared/bitstream-format.model'; import { BitstreamFormatSupportLevel } from '../../../../core/shared/bitstream-format-support-level'; import { isEmpty } from '../../../../shared/empty.util'; +import { FormComponent } from '../../../../shared/form/form.component'; import { RouterStub } from '../../../../shared/testing/router.stub'; import { FormatFormComponent } from './format-form.component'; @@ -52,13 +53,24 @@ describe('FormatFormComponent', () => { const initAsync = () => { TestBed.configureTestingModule({ - imports: [CommonModule, RouterTestingModule.withRoutes([]), ReactiveFormsModule, FormsModule, TranslateModule.forRoot(), NgbModule], - declarations: [FormatFormComponent], - providers: [ - { provide: Router, useValue: router }, + imports: [ + CommonModule, + RouterTestingModule.withRoutes([]), + ReactiveFormsModule, + FormsModule, + TranslateModule.forRoot(), + NgbModule, + FormatFormComponent, ], + providers: [{ provide: Router, useValue: router }], schemas: [CUSTOM_ELEMENTS_SCHEMA], - }).compileComponents(); + }) + .overrideComponent(FormatFormComponent, { + remove: { + imports: [FormComponent], + }, + }) + .compileComponents(); }; const initBeforeEach = () => { diff --git a/src/app/admin/admin-registries/bitstream-formats/format-form/format-form.component.ts b/src/app/admin/admin-registries/bitstream-formats/format-form/format-form.component.ts index 42a1e4baff..37ae0d1dc0 100644 --- a/src/app/admin/admin-registries/bitstream-formats/format-form/format-form.component.ts +++ b/src/app/admin/admin-registries/bitstream-formats/format-form/format-form.component.ts @@ -1,3 +1,4 @@ +import { NgIf } from '@angular/common'; import { Component, EventEmitter, @@ -11,12 +12,10 @@ import { DynamicFormArrayModel, DynamicFormControlLayout, DynamicFormControlModel, - DynamicFormService, DynamicInputModel, DynamicSelectModel, DynamicTextAreaModel, } from '@ng-dynamic-forms/core'; -import { TranslateService } from '@ngx-translate/core'; import { environment } from '../../../../../environments/environment'; import { BitstreamFormat } from '../../../../core/shared/bitstream-format.model'; @@ -25,6 +24,7 @@ import { hasValue, isEmpty, } from '../../../../shared/empty.util'; +import { FormComponent } from '../../../../shared/form/form.component'; import { getBitstreamFormatsModuleRoute } from '../../admin-registries-routing-paths'; /** @@ -33,6 +33,11 @@ import { getBitstreamFormatsModuleRoute } from '../../admin-registries-routing-p @Component({ selector: 'ds-bitstream-format-form', templateUrl: './format-form.component.html', + imports: [ + FormComponent, + NgIf, + ], + standalone: true, }) export class FormatFormComponent implements OnInit { @@ -132,9 +137,7 @@ export class FormatFormComponent implements OnInit { }, this.arrayElementLayout), ]; - constructor(private dynamicFormService: DynamicFormService, - private translateService: TranslateService, - private router: Router) { + constructor(private router: Router) { } @@ -151,12 +154,12 @@ export class FormatFormComponent implements OnInit { (fieldModel: DynamicFormControlModel) => { if (fieldModel.name === 'extensions') { if (hasValue(this.bitstreamFormat.extensions)) { - const extenstions = this.bitstreamFormat.extensions; + const extensions = this.bitstreamFormat.extensions; const formArray = (fieldModel as DynamicFormArrayModel); - for (let i = 0; i < extenstions.length; i++) { + for (let i = 0; i < extensions.length; i++) { formArray.insertGroup(i).group[0] = new DynamicInputModel({ id: `extension-${i}`, - value: extenstions[i], + value: extensions[i], }, this.arrayInputElementLayout); } } @@ -169,7 +172,7 @@ export class FormatFormComponent implements OnInit { } /** - * Creates an updated bistream format based on the current values in the form + * Creates an updated bitstream format based on the current values in the form * Emits the updated bitstream format trouhg the updatedFormat emitter */ onSubmit() { diff --git a/src/app/admin/admin-registries/metadata-registry/metadata-registry.component.html b/src/app/admin/admin-registries/metadata-registry/metadata-registry.component.html index 63242b4795..8b3c6fe972 100644 --- a/src/app/admin/admin-registries/metadata-registry/metadata-registry.component.html +++ b/src/app/admin/admin-registries/metadata-registry/metadata-registry.component.html @@ -27,14 +27,14 @@ + [ngClass]="{'table-primary' : (activeMetadataSchema$ | async)?.id === schema.id}"> {{schema.id}} diff --git a/src/app/admin/admin-registries/metadata-registry/metadata-registry.component.spec.ts b/src/app/admin/admin-registries/metadata-registry/metadata-registry.component.spec.ts index 63acf0f258..4ee61c35d5 100644 --- a/src/app/admin/admin-registries/metadata-registry/metadata-registry.component.spec.ts +++ b/src/app/admin/admin-registries/metadata-registry/metadata-registry.component.spec.ts @@ -10,32 +10,45 @@ import { waitForAsync, } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; +import { RouterLink } from '@angular/router'; import { RouterTestingModule } from '@angular/router/testing'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { TranslateModule } from '@ngx-translate/core'; import { of as observableOf } from 'rxjs'; +import { FormBuilderService } from 'src/app/shared/form/builder/form-builder.service'; -import { RestResponse } from '../../../core/cache/response.models'; -import { buildPaginatedList } from '../../../core/data/paginated-list.model'; +import { ConfigurationDataService } from '../../../core/data/configuration-data.service'; +import { GroupDataService } from '../../../core/eperson/group-data.service'; import { MetadataSchema } from '../../../core/metadata/metadata-schema.model'; import { PaginationService } from '../../../core/pagination/pagination.service'; import { RegistryService } from '../../../core/registry/registry.service'; +import { ConfigurationProperty } from '../../../core/shared/configuration-property.model'; +import { SearchConfigurationService } from '../../../core/shared/search/search-configuration.service'; +import { FormService } from '../../../shared/form/form.service'; import { HostWindowService } from '../../../shared/host-window.service'; +import { getMockFormBuilderService } from '../../../shared/mocks/form-builder-service.mock'; +import { getMockFormService } from '../../../shared/mocks/form-service.mock'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { PaginationComponent } from '../../../shared/pagination/pagination.component'; import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; import { HostWindowServiceStub } from '../../../shared/testing/host-window-service.stub'; import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub'; import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub'; +import { RegistryServiceStub } from '../../../shared/testing/registry.service.stub'; +import { SearchConfigurationServiceStub } from '../../../shared/testing/search-configuration-service.stub'; +import { createPaginatedList } from '../../../shared/testing/utils.test'; import { EnumKeysPipe } from '../../../shared/utils/enum-keys-pipe'; import { MetadataRegistryComponent } from './metadata-registry.component'; +import { MetadataSchemaFormComponent } from './metadata-schema-form/metadata-schema-form.component'; describe('MetadataRegistryComponent', () => { let comp: MetadataRegistryComponent; let fixture: ComponentFixture; - let registryService: RegistryService; - let paginationService; - const mockSchemasList = [ + + let paginationService: PaginationServiceStub; + let registryService: RegistryServiceStub; + + const mockSchemasList: MetadataSchema[] = [ { id: 1, _links: { @@ -56,40 +69,73 @@ describe('MetadataRegistryComponent', () => { prefix: 'mock', namespace: 'http://dspace.org/mockschema', }, - ]; - const mockSchemas = createSuccessfulRemoteDataObject$(buildPaginatedList(null, mockSchemasList)); - /* eslint-disable no-empty,@typescript-eslint/no-empty-function */ - const registryServiceStub = { - getMetadataSchemas: () => mockSchemas, - getActiveMetadataSchema: () => observableOf(undefined), - getSelectedMetadataSchemas: () => observableOf([]), - editMetadataSchema: (schema) => { - }, - cancelEditMetadataSchema: () => { - }, - deleteMetadataSchema: () => observableOf(new RestResponse(true, 200, 'OK')), - deselectAllMetadataSchema: () => { - }, - clearMetadataSchemaRequests: () => observableOf(undefined), - }; - /* eslint-enable no-empty, @typescript-eslint/no-empty-function */ + ] as MetadataSchema[]; - paginationService = new PaginationServiceStub(); + const configurationDataService = jasmine.createSpyObj('configurationDataService', { + findByPropertyName: createSuccessfulRemoteDataObject$(Object.assign(new ConfigurationProperty(), { + name: 'test', + values: [ + 'org.dspace.ctask.general.ProfileFormats = test', + ], + })), + }); + + const mockGroupService = jasmine.createSpyObj('groupService', + { + // findByHref: jasmine.createSpy('findByHref'), + // findAll: jasmine.createSpy('findAll'), + // searchGroups: jasmine.createSpy('searchGroups'), + getUUIDFromString: jasmine.createSpy('getUUIDFromString'), + }, + { + linkPath: 'groups', + }, + ); beforeEach(waitForAsync(() => { + paginationService = new PaginationServiceStub(); + registryService = new RegistryServiceStub(); + spyOn(registryService, 'getMetadataSchemas').and.returnValue(createSuccessfulRemoteDataObject$(createPaginatedList(mockSchemasList))); + TestBed.configureTestingModule({ - imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule], - declarations: [MetadataRegistryComponent, PaginationComponent, EnumKeysPipe], + imports: [ + CommonModule, + RouterTestingModule.withRoutes([]), + TranslateModule.forRoot(), + NgbModule, + MetadataRegistryComponent, + PaginationComponent, + EnumKeysPipe, + ], providers: [ - { provide: RegistryService, useValue: registryServiceStub }, + { provide: RegistryService, useValue: registryService }, { provide: HostWindowService, useValue: new HostWindowServiceStub(0) }, { provide: PaginationService, useValue: paginationService }, - { provide: NotificationsService, useValue: new NotificationsServiceStub() }, + { + provide: NotificationsService, + useValue: new NotificationsServiceStub(), + }, + { provide: FormService, useValue: getMockFormService() }, + { provide: GroupDataService, useValue: mockGroupService }, + { + provide: ConfigurationDataService, + useValue: configurationDataService, + }, + { + provide: SearchConfigurationService, + useValue: new SearchConfigurationServiceStub(), + }, + { provide: FormBuilderService, useValue: getMockFormBuilderService() }, ], schemas: [NO_ERRORS_SCHEMA], - }).overrideComponent(MetadataRegistryComponent, { - set: { changeDetection: ChangeDetectionStrategy.Default }, - }).compileComponents(); + }) + .overrideComponent(MetadataRegistryComponent, { + remove: { + imports: [MetadataSchemaFormComponent, RouterLink], + }, + add: { changeDetection: ChangeDetectionStrategy.Default }, + }) + .compileComponents(); })); beforeEach(() => { @@ -132,7 +178,7 @@ describe('MetadataRegistryComponent', () => { })); it('should cancel editing the selected schema when clicked again', waitForAsync(() => { - spyOn(registryService, 'getActiveMetadataSchema').and.returnValue(observableOf(mockSchemasList[0] as MetadataSchema)); + comp.activeMetadataSchema$ = observableOf(mockSchemasList[0] as MetadataSchema); spyOn(registryService, 'cancelEditMetadataSchema'); row.click(); fixture.detectChanges(); @@ -147,7 +193,7 @@ describe('MetadataRegistryComponent', () => { beforeEach(() => { spyOn(registryService, 'deleteMetadataSchema').and.callThrough(); - spyOn(registryService, 'getSelectedMetadataSchemas').and.returnValue(observableOf(selectedSchemas as MetadataSchema[])); + comp.selectedMetadataSchemaIDs$ = observableOf(selectedSchemas.map((selectedSchema: MetadataSchema) => selectedSchema.id)); comp.deleteSchemas(); fixture.detectChanges(); }); diff --git a/src/app/admin/admin-registries/metadata-registry/metadata-registry.component.ts b/src/app/admin/admin-registries/metadata-registry/metadata-registry.component.ts index 976ab572cc..be1239ab95 100644 --- a/src/app/admin/admin-registries/metadata-registry/metadata-registry.component.ts +++ b/src/app/admin/admin-registries/metadata-registry/metadata-registry.component.ts @@ -1,10 +1,23 @@ -import { Component } from '@angular/core'; -import { Router } from '@angular/router'; -import { TranslateService } from '@ngx-translate/core'; +import { + AsyncPipe, + NgClass, + NgForOf, + NgIf, +} from '@angular/common'; +import { + Component, + OnDestroy, + OnInit, +} from '@angular/core'; +import { RouterLink } from '@angular/router'; +import { + TranslateModule, + TranslateService, +} from '@ngx-translate/core'; import { BehaviorSubject, - combineLatest as observableCombineLatest, Observable, + Subscription, zip, } from 'rxjs'; import { @@ -21,27 +34,49 @@ import { PaginationService } from '../../../core/pagination/pagination.service'; import { RegistryService } from '../../../core/registry/registry.service'; import { NoContent } from '../../../core/shared/NoContent.model'; import { getFirstCompletedRemoteData } from '../../../core/shared/operators'; -import { hasValue } from '../../../shared/empty.util'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; +import { PaginationComponent } from '../../../shared/pagination/pagination.component'; import { toFindListOptions } from '../../../shared/pagination/pagination.utils'; import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model'; +import { MetadataSchemaFormComponent } from './metadata-schema-form/metadata-schema-form.component'; @Component({ selector: 'ds-metadata-registry', templateUrl: './metadata-registry.component.html', styleUrls: ['./metadata-registry.component.scss'], + imports: [ + MetadataSchemaFormComponent, + TranslateModule, + AsyncPipe, + PaginationComponent, + NgIf, + NgForOf, + NgClass, + RouterLink, + ], + standalone: true, }) /** * A component used for managing all existing metadata schemas within the repository. * The admin can create, edit or delete metadata schemas here. */ -export class MetadataRegistryComponent { +export class MetadataRegistryComponent implements OnDestroy, OnInit { /** * A list of all the current metadata schemas within the repository */ metadataSchemas: Observable>>; + /** + * The {@link MetadataSchema}that is being edited + */ + activeMetadataSchema$: Observable; + + /** + * The selected {@link MetadataSchema} IDs + */ + selectedMetadataSchemaIDs$: Observable; + /** * Pagination config used to display the list of metadata schemas */ @@ -51,15 +86,25 @@ export class MetadataRegistryComponent { }); /** - * Whether or not the list of MetadataSchemas needs an update + * Whether the list of MetadataSchemas needs an update */ needsUpdate$: BehaviorSubject = new BehaviorSubject(true); - constructor(private registryService: RegistryService, - private notificationsService: NotificationsService, - private router: Router, - private paginationService: PaginationService, - private translateService: TranslateService) { + subscriptions: Subscription[] = []; + + constructor( + protected registryService: RegistryService, + protected notificationsService: NotificationsService, + protected paginationService: PaginationService, + protected translateService: TranslateService, + ) { + } + + ngOnInit(): void { + this.activeMetadataSchema$ = this.registryService.getActiveMetadataSchema(); + this.selectedMetadataSchemaIDs$ = this.registryService.getSelectedMetadataSchemas().pipe( + map((schemas: MetadataSchema[]) => schemas.map((schema: MetadataSchema) => schema.id)), + ); this.updateSchemas(); } @@ -88,30 +133,13 @@ export class MetadataRegistryComponent { * @param schema */ editSchema(schema: MetadataSchema) { - this.getActiveSchema().pipe(take(1)).subscribe((activeSchema) => { + this.subscriptions.push(this.activeMetadataSchema$.pipe(take(1)).subscribe((activeSchema: MetadataSchema) => { if (schema === activeSchema) { this.registryService.cancelEditMetadataSchema(); } else { this.registryService.editMetadataSchema(schema); } - }); - } - - /** - * Checks whether the given metadata schema is active (being edited) - * @param schema - */ - isActive(schema: MetadataSchema): Observable { - return this.getActiveSchema().pipe( - map((activeSchema) => schema === activeSchema), - ); - } - - /** - * Gets the active metadata schema (being edited) - */ - getActiveSchema(): Observable { - return this.registryService.getActiveMetadataSchema(); + })); } /** @@ -125,42 +153,25 @@ export class MetadataRegistryComponent { this.registryService.deselectMetadataSchema(schema); } - /** - * Checks whether a given metadata schema is selected in the list (checkbox) - * @param schema - */ - isSelected(schema: MetadataSchema): Observable { - return this.registryService.getSelectedMetadataSchemas().pipe( - map((schemas) => schemas.find((selectedSchema) => selectedSchema === schema) != null), - ); - } - /** * Delete all the selected metadata schemas */ deleteSchemas() { - this.registryService.getSelectedMetadataSchemas().pipe(take(1)).subscribe( - (schemas) => { - const tasks$ = []; - for (const schema of schemas) { - if (hasValue(schema.id)) { - tasks$.push(this.registryService.deleteMetadataSchema(schema.id).pipe(getFirstCompletedRemoteData())); - } - } - zip(...tasks$).subscribe((responses: RemoteData[]) => { - const successResponses = responses.filter((response: RemoteData) => response.hasSucceeded); - const failedResponses = responses.filter((response: RemoteData) => response.hasFailed); - if (successResponses.length > 0) { - this.showNotification(true, successResponses.length); - } - if (failedResponses.length > 0) { - this.showNotification(false, failedResponses.length); - } - this.registryService.deselectAllMetadataSchema(); - this.registryService.cancelEditMetadataSchema(); - }); - }, - ); + this.subscriptions.push(this.selectedMetadataSchemaIDs$.pipe( + take(1), + switchMap((schemaIDs: number[]) => zip(schemaIDs.map((schemaID: number) => this.registryService.deleteMetadataSchema(schemaID).pipe(getFirstCompletedRemoteData())))), + ).subscribe((responses: RemoteData[]) => { + const successResponses: RemoteData[] = responses.filter((response: RemoteData) => response.hasSucceeded); + const failedResponses: RemoteData[] = responses.filter((response: RemoteData) => response.hasFailed); + if (successResponses.length > 0) { + this.showNotification(true, successResponses.length); + } + if (failedResponses.length > 0) { + this.showNotification(false, failedResponses.length); + } + this.registryService.deselectAllMetadataSchema(); + this.registryService.cancelEditMetadataSchema(); + })); } /** @@ -171,20 +182,20 @@ export class MetadataRegistryComponent { showNotification(success: boolean, amount: number) { const prefix = 'admin.registries.schema.notification'; const suffix = success ? 'success' : 'failure'; - const messages = observableCombineLatest( - this.translateService.get(success ? `${prefix}.${suffix}` : `${prefix}.${suffix}`), - this.translateService.get(`${prefix}.deleted.${suffix}`, { amount: amount }), - ); - messages.subscribe(([head, content]) => { - if (success) { - this.notificationsService.success(head, content); - } else { - this.notificationsService.error(head, content); - } - }); + + const head: string = this.translateService.instant(success ? `${prefix}.${suffix}` : `${prefix}.${suffix}`); + const content: string = this.translateService.instant(`${prefix}.deleted.${suffix}`, { amount: amount }); + + if (success) { + this.notificationsService.success(head, content); + } else { + this.notificationsService.error(head, content); + } } + ngOnDestroy(): void { this.paginationService.clearPagination(this.config.id); + this.subscriptions.map((subscription: Subscription) => subscription.unsubscribe()); } } diff --git a/src/app/admin/admin-registries/metadata-registry/metadata-schema-form/metadata-schema-form.component.html b/src/app/admin/admin-registries/metadata-registry/metadata-schema-form/metadata-schema-form.component.html index 85d1e90692..15cc81d9ca 100644 --- a/src/app/admin/admin-registries/metadata-registry/metadata-schema-form/metadata-schema-form.component.html +++ b/src/app/admin/admin-registries/metadata-registry/metadata-schema-form/metadata-schema-form.component.html @@ -1,4 +1,4 @@ -
+

{{messagePrefix + '.create' | translate}}

diff --git a/src/app/admin/admin-registries/metadata-registry/metadata-schema-form/metadata-schema-form.component.spec.ts b/src/app/admin/admin-registries/metadata-registry/metadata-schema-form/metadata-schema-form.component.spec.ts index 17fdb6cd3d..f98e274324 100644 --- a/src/app/admin/admin-registries/metadata-registry/metadata-schema-form/metadata-schema-form.component.spec.ts +++ b/src/app/admin/admin-registries/metadata-registry/metadata-schema-form/metadata-schema-form.component.spec.ts @@ -1,5 +1,5 @@ import { CommonModule } from '@angular/common'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; import { ComponentFixture, inject, @@ -14,44 +14,35 @@ import { of as observableOf } from 'rxjs'; import { MetadataSchema } from '../../../../core/metadata/metadata-schema.model'; import { RegistryService } from '../../../../core/registry/registry.service'; import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service'; +import { FormComponent } from '../../../../shared/form/form.component'; +import { getMockFormBuilderService } from '../../../../shared/mocks/form-builder-service.mock'; +import { RegistryServiceStub } from '../../../../shared/testing/registry.service.stub'; import { EnumKeysPipe } from '../../../../shared/utils/enum-keys-pipe'; import { MetadataSchemaFormComponent } from './metadata-schema-form.component'; describe('MetadataSchemaFormComponent', () => { let component: MetadataSchemaFormComponent; let fixture: ComponentFixture; - let registryService: RegistryService; - /* eslint-disable no-empty,@typescript-eslint/no-empty-function */ - const registryServiceStub = { - getActiveMetadataSchema: () => observableOf(undefined), - createOrUpdateMetadataSchema: (schema: MetadataSchema) => observableOf(schema), - cancelEditMetadataSchema: () => { - }, - clearMetadataSchemaRequests: () => observableOf(undefined), - }; - const formBuilderServiceStub = { - createFormGroup: () => { - return { - patchValue: () => { - }, - reset(_value?: any, _options?: { onlySelf?: boolean; emitEvent?: boolean; }): void { - }, - }; - }, - }; - /* eslint-enable no-empty, @typescript-eslint/no-empty-function */ + let registryService: RegistryServiceStub; beforeEach(waitForAsync(() => { + registryService = new RegistryServiceStub(); + return TestBed.configureTestingModule({ - imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule], - declarations: [MetadataSchemaFormComponent, EnumKeysPipe], + imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule, MetadataSchemaFormComponent, EnumKeysPipe], providers: [ - { provide: RegistryService, useValue: registryServiceStub }, - { provide: FormBuilderService, useValue: formBuilderServiceStub }, + { provide: RegistryService, useValue: registryService }, + { provide: FormBuilderService, useValue: getMockFormBuilderService() }, ], - schemas: [NO_ERRORS_SCHEMA], - }).compileComponents(); + schemas: [CUSTOM_ELEMENTS_SCHEMA], + }) + .overrideComponent(MetadataSchemaFormComponent, { + remove: { + imports: [FormComponent], + }, + }) + .compileComponents(); })); beforeEach(() => { @@ -81,7 +72,7 @@ describe('MetadataSchemaFormComponent', () => { describe('without an active schema', () => { beforeEach(() => { - spyOn(registryService, 'getActiveMetadataSchema').and.returnValue(observableOf(undefined)); + component.activeMetadataSchema$ = observableOf(undefined); component.onSubmit(); fixture.detectChanges(); }); @@ -100,7 +91,7 @@ describe('MetadataSchemaFormComponent', () => { } as MetadataSchema); beforeEach(() => { - spyOn(registryService, 'getActiveMetadataSchema').and.returnValue(observableOf(expectedWithId)); + component.activeMetadataSchema$ = observableOf(expectedWithId); component.onSubmit(); fixture.detectChanges(); }); diff --git a/src/app/admin/admin-registries/metadata-registry/metadata-schema-form/metadata-schema-form.component.ts b/src/app/admin/admin-registries/metadata-registry/metadata-schema-form/metadata-schema-form.component.ts index 640a9bff29..c58c4bef10 100644 --- a/src/app/admin/admin-registries/metadata-registry/metadata-schema-form/metadata-schema-form.component.ts +++ b/src/app/admin/admin-registries/metadata-registry/metadata-schema-form/metadata-schema-form.component.ts @@ -1,3 +1,7 @@ +import { + AsyncPipe, + NgIf, +} from '@angular/common'; import { Component, EventEmitter, @@ -12,24 +16,35 @@ import { DynamicFormLayout, DynamicInputModel, } from '@ng-dynamic-forms/core'; -import { TranslateService } from '@ngx-translate/core'; import { - combineLatest, + TranslateModule, + TranslateService, +} from '@ngx-translate/core'; +import { Observable, + Subscription, } from 'rxjs'; import { + map, switchMap, take, - tap, } from 'rxjs/operators'; import { MetadataSchema } from '../../../../core/metadata/metadata-schema.model'; import { RegistryService } from '../../../../core/registry/registry.service'; import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service'; +import { FormComponent } from '../../../../shared/form/form.component'; @Component({ selector: 'ds-metadata-schema-form', templateUrl: './metadata-schema-form.component.html', + imports: [ + NgIf, + AsyncPipe, + TranslateModule, + FormComponent, + ], + standalone: true, }) /** * A form used for creating and editing metadata schemas @@ -87,64 +102,71 @@ export class MetadataSchemaFormComponent implements OnInit, OnDestroy { */ @Output() submitForm: EventEmitter = new EventEmitter(); - constructor(public registryService: RegistryService, private formBuilderService: FormBuilderService, private translateService: TranslateService) { + /** + * The {@link MetadataSchema} that is currently being edited + */ + activeMetadataSchema$: Observable; + + subscriptions: Subscription[] = []; + + constructor( + protected registryService: RegistryService, + protected formBuilderService: FormBuilderService, + protected translateService: TranslateService, + ) { } ngOnInit() { - combineLatest([ - this.translateService.get(`${this.messagePrefix}.name`), - this.translateService.get(`${this.messagePrefix}.namespace`), - ]).subscribe(([name, namespace]) => { - this.name = new DynamicInputModel({ - id: 'name', - label: name, - name: 'name', - validators: { - required: null, - pattern: '^[^. ,]*$', - maxLength: 32, - }, - required: true, - errorMessages: { - pattern: 'error.validation.metadata.name.invalid-pattern', - maxLength: 'error.validation.metadata.name.max-length', - }, - }); - this.namespace = new DynamicInputModel({ - id: 'namespace', - label: namespace, - name: 'namespace', - validators: { - required: null, - maxLength: 256, - }, - required: true, - errorMessages: { - maxLength: 'error.validation.metadata.namespace.max-length', - }, - }); - this.formModel = [ - new DynamicFormGroupModel( - { - id: 'metadatadataschemagroup', - group:[this.namespace, this.name], - }), - ]; - this.formGroup = this.formBuilderService.createFormGroup(this.formModel); - this.registryService.getActiveMetadataSchema().subscribe((schema: MetadataSchema) => { - if (schema == null) { - this.clearFields(); - } else { - this.formGroup.patchValue({ - metadatadataschemagroup: { - name: schema.prefix, - namespace: schema.namespace, - }, - }); - this.name.disabled = true; - } - }); + this.name = new DynamicInputModel({ + id: 'name', + label: this.translateService.instant(`${this.messagePrefix}.name`), + name: 'name', + validators: { + required: null, + pattern: '^[^. ,]*$', + maxLength: 32, + }, + required: true, + errorMessages: { + pattern: 'error.validation.metadata.name.invalid-pattern', + maxLength: 'error.validation.metadata.name.max-length', + }, }); + this.namespace = new DynamicInputModel({ + id: 'namespace', + label: this.translateService.instant(`${this.messagePrefix}.namespace`), + name: 'namespace', + validators: { + required: null, + maxLength: 256, + }, + required: true, + errorMessages: { + maxLength: 'error.validation.metadata.namespace.max-length', + }, + }); + this.formModel = [ + new DynamicFormGroupModel( + { + id: 'metadatadataschemagroup', + group:[this.namespace, this.name], + }), + ]; + this.formGroup = this.formBuilderService.createFormGroup(this.formModel); + this.activeMetadataSchema$ = this.registryService.getActiveMetadataSchema(); + this.subscriptions.push(this.activeMetadataSchema$.subscribe((schema: MetadataSchema) => { + if (schema == null) { + this.clearFields(); + } else { + this.formGroup.patchValue({ + metadatadataschemagroup: { + name: schema.prefix, + namespace: schema.namespace, + }, + }); + this.name.disabled = true; + } + })); } /** @@ -161,48 +183,29 @@ export class MetadataSchemaFormComponent implements OnInit, OnDestroy { * Emit the updated/created schema using the EventEmitter submitForm */ onSubmit(): void { - this.registryService - .getActiveMetadataSchema() - .pipe( - take(1), - switchMap((schema: MetadataSchema) => { - const metadataValues = { - prefix: this.name.value, - namespace: this.namespace.value, - }; - - let createOrUpdate$: Observable; - - if (schema == null) { - createOrUpdate$ = - this.registryService.createOrUpdateMetadataSchema( - Object.assign(new MetadataSchema(), metadataValues), - ); - } else { - const updatedSchema = Object.assign( - new MetadataSchema(), - schema, - { - namespace: metadataValues.namespace, - }, - ); - createOrUpdate$ = - this.registryService.createOrUpdateMetadataSchema( - updatedSchema, - ); - } - - return createOrUpdate$; - }), - tap(() => { - this.registryService.clearMetadataSchemaRequests().subscribe(); - }), - ) - .subscribe((updatedOrCreatedSchema: MetadataSchema) => { - this.submitForm.emit(updatedOrCreatedSchema); - this.clearFields(); - this.registryService.cancelEditMetadataSchema(); - }); + this.activeMetadataSchema$.pipe( + take(1), + switchMap((schema: MetadataSchema) => { + const metadataValues = { + prefix: this.name.value, + namespace: this.namespace.value, + }; + if (schema == null) { + return this.registryService.createOrUpdateMetadataSchema(Object.assign(new MetadataSchema(), metadataValues)); + } else { + return this.registryService.createOrUpdateMetadataSchema(Object.assign(new MetadataSchema(), schema, { + namespace: metadataValues.namespace, + })); + } + }), + switchMap((updatedOrCreatedSchema: MetadataSchema) => this.registryService.clearMetadataSchemaRequests().pipe( + map(() => updatedOrCreatedSchema), + )), + ).subscribe((updatedOrCreatedSchema: MetadataSchema) => { + this.submitForm.emit(updatedOrCreatedSchema); + this.clearFields(); + this.registryService.cancelEditMetadataSchema(); + }); } /** @@ -218,5 +221,6 @@ export class MetadataSchemaFormComponent implements OnInit, OnDestroy { */ ngOnDestroy(): void { this.onCancel(); + this.subscriptions.forEach((subscription: Subscription) => subscription.unsubscribe()); } } diff --git a/src/app/admin/admin-registries/metadata-schema/metadata-field-form/metadata-field-form.component.spec.ts b/src/app/admin/admin-registries/metadata-schema/metadata-field-form/metadata-field-form.component.spec.ts index fb9037b582..440f52a24d 100644 --- a/src/app/admin/admin-registries/metadata-schema/metadata-field-form/metadata-field-form.component.spec.ts +++ b/src/app/admin/admin-registries/metadata-schema/metadata-field-form/metadata-field-form.component.spec.ts @@ -1,5 +1,5 @@ import { CommonModule } from '@angular/common'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; import { ComponentFixture, inject, @@ -15,13 +15,17 @@ import { MetadataField } from '../../../../core/metadata/metadata-field.model'; import { MetadataSchema } from '../../../../core/metadata/metadata-schema.model'; import { RegistryService } from '../../../../core/registry/registry.service'; import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service'; +import { FormComponent } from '../../../../shared/form/form.component'; +import { getMockFormBuilderService } from '../../../../shared/mocks/form-builder-service.mock'; +import { RegistryServiceStub } from '../../../../shared/testing/registry.service.stub'; import { EnumKeysPipe } from '../../../../shared/utils/enum-keys-pipe'; import { MetadataFieldFormComponent } from './metadata-field-form.component'; describe('MetadataFieldFormComponent', () => { let component: MetadataFieldFormComponent; let fixture: ComponentFixture; - let registryService: RegistryService; + + let registryService: RegistryServiceStub; const metadataSchema = Object.assign(new MetadataSchema(), { id: 1, @@ -29,39 +33,21 @@ describe('MetadataFieldFormComponent', () => { prefix: 'fake', }); - /* eslint-disable no-empty,@typescript-eslint/no-empty-function */ - const registryServiceStub = { - getActiveMetadataField: () => observableOf(undefined), - createMetadataField: (field: MetadataField) => observableOf(field), - updateMetadataField: (field: MetadataField) => observableOf(field), - cancelEditMetadataField: () => { - }, - cancelEditMetadataSchema: () => { - }, - clearMetadataFieldRequests: () => observableOf(undefined), - }; - const formBuilderServiceStub = { - createFormGroup: () => { - return { - patchValue: () => { - }, - reset(_value?: any, _options?: { onlySelf?: boolean; emitEvent?: boolean; }): void { - }, - }; - }, - }; - /* eslint-enable no-empty, @typescript-eslint/no-empty-function */ - beforeEach(waitForAsync(() => { + registryService = new RegistryServiceStub(); + return TestBed.configureTestingModule({ - imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule], - declarations: [MetadataFieldFormComponent, EnumKeysPipe], + imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule, MetadataFieldFormComponent, EnumKeysPipe], providers: [ - { provide: RegistryService, useValue: registryServiceStub }, - { provide: FormBuilderService, useValue: formBuilderServiceStub }, + { provide: RegistryService, useValue: registryService }, + { provide: FormBuilderService, useValue: getMockFormBuilderService() }, ], - schemas: [NO_ERRORS_SCHEMA], - }).compileComponents(); + schemas: [CUSTOM_ELEMENTS_SCHEMA], + }) + .overrideComponent(MetadataFieldFormComponent, { + remove: { imports: [FormComponent] }, + }) + .compileComponents(); })); beforeEach(() => { diff --git a/src/app/admin/admin-registries/metadata-schema/metadata-field-form/metadata-field-form.component.ts b/src/app/admin/admin-registries/metadata-schema/metadata-field-form/metadata-field-form.component.ts index 48451357a0..bfadd018ef 100644 --- a/src/app/admin/admin-registries/metadata-schema/metadata-field-form/metadata-field-form.component.ts +++ b/src/app/admin/admin-registries/metadata-schema/metadata-field-form/metadata-field-form.component.ts @@ -1,3 +1,7 @@ +import { + AsyncPipe, + NgIf, +} from '@angular/common'; import { Component, EventEmitter, @@ -14,7 +18,10 @@ import { DynamicInputModel, DynamicTextAreaModel, } from '@ng-dynamic-forms/core'; -import { TranslateService } from '@ngx-translate/core'; +import { + TranslateModule, + TranslateService, +} from '@ngx-translate/core'; import { combineLatest } from 'rxjs'; import { take } from 'rxjs/operators'; @@ -22,10 +29,18 @@ import { MetadataField } from '../../../../core/metadata/metadata-field.model'; import { MetadataSchema } from '../../../../core/metadata/metadata-schema.model'; import { RegistryService } from '../../../../core/registry/registry.service'; import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service'; +import { FormComponent } from '../../../../shared/form/form.component'; @Component({ selector: 'ds-metadata-field-form', templateUrl: './metadata-field-form.component.html', + imports: [ + NgIf, + FormComponent, + TranslateModule, + AsyncPipe, + ], + standalone: true, }) /** * A form used for creating and editing metadata fields diff --git a/src/app/admin/admin-registries/metadata-schema/metadata-schema.component.html b/src/app/admin/admin-registries/metadata-schema/metadata-schema.component.html index 97123d29a5..288266fb75 100644 --- a/src/app/admin/admin-registries/metadata-schema/metadata-schema.component.html +++ b/src/app/admin/admin-registries/metadata-schema/metadata-schema.component.html @@ -16,7 +16,6 @@ @@ -32,8 +31,8 @@ - + [ngClass]="{'table-primary' : (activeField$ | async)?.id === field.id}"> + { let comp: MetadataSchemaComponent; let fixture: ComponentFixture; - let registryService: RegistryService; - const mockSchemasList = [ + + let registryService: RegistryServiceStub; + let activatedRoute: ActivatedRouteStub; + let paginationService: PaginationServiceStub; + + const mockSchemasList: MetadataSchema[] = [ { id: 1, _links: { @@ -60,8 +67,8 @@ describe('MetadataSchemaComponent', () => { prefix: 'mock', namespace: 'http://dspace.org/mockschema', }, - ]; - const mockFieldsList = [ + ] as MetadataSchema[]; + const mockFieldsList: MetadataField[] = [ { id: 1, _links: { @@ -110,48 +117,66 @@ describe('MetadataSchemaComponent', () => { scopeNote: null, schema: createSuccessfulRemoteDataObject$(mockSchemasList[1]), }, - ]; - const mockSchemas = createSuccessfulRemoteDataObject$(buildPaginatedList(null, mockSchemasList)); - /* eslint-disable no-empty,@typescript-eslint/no-empty-function */ - const registryServiceStub = { - getMetadataSchemas: () => mockSchemas, - getMetadataFieldsBySchema: (schema: MetadataSchema) => createSuccessfulRemoteDataObject$(buildPaginatedList(null, mockFieldsList.filter((value) => value.id === 3 || value.id === 4))), - getMetadataSchemaByPrefix: (schemaName: string) => createSuccessfulRemoteDataObject$(mockSchemasList.filter((value) => value.prefix === schemaName)[0]), - getActiveMetadataField: () => observableOf(undefined), - getSelectedMetadataFields: () => observableOf([]), - editMetadataField: (schema) => { - }, - cancelEditMetadataField: () => { - }, - deleteMetadataField: () => observableOf(new RestResponse(true, 200, 'OK')), - deselectAllMetadataField: () => { - }, - clearMetadataFieldRequests: () => observableOf(undefined), - }; - /* eslint-enable no-empty, @typescript-eslint/no-empty-function */ + ] as MetadataField[]; const schemaNameParam = 'mock'; - const activatedRouteStub = Object.assign(new ActivatedRouteStub(), { - params: observableOf({ - schemaName: schemaNameParam, - }), + + const configurationDataService = jasmine.createSpyObj('configurationDataService', { + findByPropertyName: createSuccessfulRemoteDataObject$(Object.assign(new ConfigurationProperty(), { + name: 'test', + values: [ + 'org.dspace.ctask.general.ProfileFormats = test', + ], + })), + }); + + const groupDataService = jasmine.createSpyObj('groupsDataService', { + findListByHref: createSuccessfulRemoteDataObject$(createPaginatedList([])), + getGroupRegistryRouterLink: '', + getUUIDFromString: '', }); - const paginationService = new PaginationServiceStub(); beforeEach(waitForAsync(() => { + activatedRoute = new ActivatedRouteStub({ + schemaName: schemaNameParam, + }); + paginationService = new PaginationServiceStub(); + registryService = new RegistryServiceStub(); + spyOn(registryService, 'getMetadataFieldsBySchema').and.returnValue(createSuccessfulRemoteDataObject$(buildPaginatedList(null, mockFieldsList.filter((value) => value.id === 3 || value.id === 4)))); + spyOn(registryService, 'getMetadataSchemaByPrefix').and.callFake((schemaName) => createSuccessfulRemoteDataObject$(mockSchemasList.filter((value) => value.prefix === schemaName)[0])); + TestBed.configureTestingModule({ - imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule], - declarations: [MetadataSchemaComponent, PaginationComponent, EnumKeysPipe, VarDirective], - providers: [ - { provide: RegistryService, useValue: registryServiceStub }, - { provide: ActivatedRoute, useValue: activatedRouteStub }, - { provide: HostWindowService, useValue: new HostWindowServiceStub(0) }, - { provide: Router, useValue: new RouterStub() }, - { provide: PaginationService, useValue: paginationService }, - { provide: NotificationsService, useValue: new NotificationsServiceStub() }, + imports: [ + CommonModule, + RouterTestingModule.withRoutes([]), + TranslateModule.forRoot(), + NgbModule, + MetadataSchemaComponent, + PaginationComponent, + EnumKeysPipe, + VarDirective, ], - schemas: [NO_ERRORS_SCHEMA], - }).compileComponents(); + providers: [ + { provide: RegistryService, useValue: registryService }, + { provide: ActivatedRoute, useValue: activatedRoute }, + { provide: HostWindowService, useValue: new HostWindowServiceStub(0) }, + { provide: PaginationService, useValue: paginationService }, + { + provide: NotificationsService, + useValue: new NotificationsServiceStub(), + }, + { provide: GroupDataService, useValue: groupDataService }, + { provide: ConfigurationDataService, useValue: configurationDataService }, + { provide: SearchConfigurationService, useValue: new SearchConfigurationServiceStub() }, + ], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + }) + .overrideComponent(MetadataSchemaComponent, { + remove: { + imports: [MetadataFieldFormComponent], + }, + }) + .compileComponents(); })); beforeEach(() => { @@ -199,7 +224,7 @@ describe('MetadataSchemaComponent', () => { })); it('should cancel editing the selected field when clicked again', waitForAsync(() => { - spyOn(registryService, 'getActiveMetadataField').and.returnValue(observableOf(mockFieldsList[2] as MetadataField)); + comp.activeField$ = observableOf(mockFieldsList[2] as MetadataField); spyOn(registryService, 'cancelEditMetadataField'); row.click(); fixture.detectChanges(); @@ -214,7 +239,7 @@ describe('MetadataSchemaComponent', () => { beforeEach(() => { spyOn(registryService, 'deleteMetadataField').and.callThrough(); - spyOn(registryService, 'getSelectedMetadataFields').and.returnValue(observableOf(selectedFields as MetadataField[])); + comp.selectedMetadataFieldIDs$ = observableOf(selectedFields.map((metadataField: MetadataField) => metadataField.id)); comp.deleteFields(); fixture.detectChanges(); }); diff --git a/src/app/admin/admin-registries/metadata-schema/metadata-schema.component.ts b/src/app/admin/admin-registries/metadata-schema/metadata-schema.component.ts index 483aa9644e..ec5d6b4cb0 100644 --- a/src/app/admin/admin-registries/metadata-schema/metadata-schema.component.ts +++ b/src/app/admin/admin-registries/metadata-schema/metadata-schema.component.ts @@ -1,16 +1,28 @@ +import { + AsyncPipe, + NgClass, + NgForOf, + NgIf, +} from '@angular/common'; import { Component, OnDestroy, OnInit, } from '@angular/core'; -import { ActivatedRoute } from '@angular/router'; -import { TranslateService } from '@ngx-translate/core'; +import { + ActivatedRoute, + RouterLink, +} from '@angular/router'; +import { + TranslateModule, + TranslateService, +} from '@ngx-translate/core'; import { BehaviorSubject, combineLatest, - combineLatest as observableCombineLatest, Observable, of as observableOf, + Subscription, zip, } from 'rxjs'; import { @@ -30,21 +42,35 @@ import { getFirstCompletedRemoteData, getFirstSucceededRemoteDataPayload, } from '../../../core/shared/operators'; -import { hasValue } from '../../../shared/empty.util'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; +import { PaginationComponent } from '../../../shared/pagination/pagination.component'; import { toFindListOptions } from '../../../shared/pagination/pagination.utils'; import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model'; +import { VarDirective } from '../../../shared/utils/var.directive'; +import { MetadataFieldFormComponent } from './metadata-field-form/metadata-field-form.component'; @Component({ selector: 'ds-metadata-schema', templateUrl: './metadata-schema.component.html', styleUrls: ['./metadata-schema.component.scss'], + imports: [ + AsyncPipe, + VarDirective, + MetadataFieldFormComponent, + TranslateModule, + PaginationComponent, + NgIf, + NgForOf, + NgClass, + RouterLink, + ], + standalone: true, }) /** * A component used for managing all existing metadata fields within the current metadata schema. * The admin can create, edit or delete metadata fields here. */ -export class MetadataSchemaComponent implements OnInit, OnDestroy { +export class MetadataSchemaComponent implements OnDestroy, OnInit { /** * The metadata schema */ @@ -69,26 +95,33 @@ export class MetadataSchemaComponent implements OnInit, OnDestroy { */ needsUpdate$: BehaviorSubject = new BehaviorSubject(true); - constructor(private registryService: RegistryService, - private route: ActivatedRoute, - private notificationsService: NotificationsService, - private paginationService: PaginationService, - private translateService: TranslateService) { + /** + * The current {@link MetadataField} that is being edited + */ + activeField$: Observable; + /** + * The selected {@link MetadataField} IDs + */ + selectedMetadataFieldIDs$: Observable; + + subscriptions: Subscription[] = []; + + constructor( + protected registryService: RegistryService, + protected route: ActivatedRoute, + protected notificationsService: NotificationsService, + protected paginationService: PaginationService, + protected translateService: TranslateService, + ) { } ngOnInit(): void { - this.route.params.subscribe((params) => { - this.initialize(params); - }); - } - - /** - * Initialize the component using the params within the url (schemaName) - * @param params - */ - initialize(params) { - this.metadataSchema$ = this.registryService.getMetadataSchemaByPrefix(params.schemaName).pipe(getFirstSucceededRemoteDataPayload()); + this.metadataSchema$ = this.registryService.getMetadataSchemaByPrefix(this.route.snapshot.params.schemaName).pipe(getFirstSucceededRemoteDataPayload()); + this.activeField$ = this.registryService.getActiveMetadataField(); + this.selectedMetadataFieldIDs$ = this.registryService.getSelectedMetadataFields().pipe( + map((metadataFields: MetadataField[]) => metadataFields.map((metadataField: MetadataField) => metadataField.id)), + ); this.updateFields(); } @@ -121,30 +154,13 @@ export class MetadataSchemaComponent implements OnInit, OnDestroy { * @param field */ editField(field: MetadataField) { - this.getActiveField().pipe(take(1)).subscribe((activeField) => { + this.subscriptions.push(this.activeField$.pipe(take(1)).subscribe((activeField) => { if (field === activeField) { this.registryService.cancelEditMetadataField(); } else { this.registryService.editMetadataField(field); } - }); - } - - /** - * Checks whether the given metadata field is active (being edited) - * @param field - */ - isActive(field: MetadataField): Observable { - return this.getActiveField().pipe( - map((activeField) => field === activeField), - ); - } - - /** - * Gets the active metadata field (being edited) - */ - getActiveField(): Observable { - return this.registryService.getActiveMetadataField(); + })); } /** @@ -158,42 +174,25 @@ export class MetadataSchemaComponent implements OnInit, OnDestroy { this.registryService.deselectMetadataField(field); } - /** - * Checks whether a given metadata field is selected in the list (checkbox) - * @param field - */ - isSelected(field: MetadataField): Observable { - return this.registryService.getSelectedMetadataFields().pipe( - map((fields) => fields.find((selectedField) => selectedField === field) != null), - ); - } - /** * Delete all the selected metadata fields */ deleteFields() { - this.registryService.getSelectedMetadataFields().pipe(take(1)).subscribe( - (fields) => { - const tasks$ = []; - for (const field of fields) { - if (hasValue(field.id)) { - tasks$.push(this.registryService.deleteMetadataField(field.id).pipe(getFirstCompletedRemoteData())); - } - } - zip(...tasks$).subscribe((responses: RemoteData[]) => { - const successResponses = responses.filter((response: RemoteData) => response.hasSucceeded); - const failedResponses = responses.filter((response: RemoteData) => response.hasFailed); - if (successResponses.length > 0) { - this.showNotification(true, successResponses.length); - } - if (failedResponses.length > 0) { - this.showNotification(false, failedResponses.length); - } - this.registryService.deselectAllMetadataField(); - this.registryService.cancelEditMetadataField(); - }); - }, - ); + this.subscriptions.push(this.selectedMetadataFieldIDs$.pipe( + take(1), + switchMap((fieldIDs) => zip(fieldIDs.map((fieldID) => this.registryService.deleteMetadataField(fieldID).pipe(getFirstCompletedRemoteData())))), + ).subscribe((responses: RemoteData[]) => { + const successResponses = responses.filter((response: RemoteData) => response.hasSucceeded); + const failedResponses = responses.filter((response: RemoteData) => response.hasFailed); + if (successResponses.length > 0) { + this.showNotification(true, successResponses.length); + } + if (failedResponses.length > 0) { + this.showNotification(false, failedResponses.length); + } + this.registryService.deselectAllMetadataField(); + this.registryService.cancelEditMetadataField(); + })); } /** @@ -204,21 +203,19 @@ export class MetadataSchemaComponent implements OnInit, OnDestroy { showNotification(success: boolean, amount: number) { const prefix = 'admin.registries.schema.notification'; const suffix = success ? 'success' : 'failure'; - const messages = observableCombineLatest([ - this.translateService.get(success ? `${prefix}.${suffix}` : `${prefix}.${suffix}`), - this.translateService.get(`${prefix}.field.deleted.${suffix}`, { amount: amount }), - ]); - messages.subscribe(([head, content]) => { - if (success) { - this.notificationsService.success(head, content); - } else { - this.notificationsService.error(head, content); - } - }); + const head = this.translateService.instant(success ? `${prefix}.${suffix}` : `${prefix}.${suffix}`); + const content = this.translateService.instant(`${prefix}.field.deleted.${suffix}`, { amount: amount }); + if (success) { + this.notificationsService.success(head, content); + } else { + this.notificationsService.error(head, content); + } } + ngOnDestroy(): void { this.paginationService.clearPagination(this.config.id); this.registryService.deselectAllMetadataField(); + this.subscriptions.forEach((subscription: Subscription) => subscription.unsubscribe()); } } diff --git a/src/app/admin/admin-reports/admin-reports-routes.ts b/src/app/admin/admin-reports/admin-reports-routes.ts new file mode 100644 index 0000000000..be1f7cc7a0 --- /dev/null +++ b/src/app/admin/admin-reports/admin-reports-routes.ts @@ -0,0 +1,30 @@ +import { Route } from '@angular/router'; + +import { i18nBreadcrumbResolver } from '../../core/breadcrumbs/i18n-breadcrumb.resolver'; +import { FilteredCollectionsComponent } from './filtered-collections/filtered-collections.component'; +import { FilteredItemsComponent } from './filtered-items/filtered-items.component'; + +export const ROUTES: Route[] = [ + { + path: 'collections', + resolve: { breadcrumb: i18nBreadcrumbResolver }, + data: { title: 'admin.reports.collections.title', breadcrumbKey: 'admin.reports.collections' }, + children: [ + { + path: '', + component: FilteredCollectionsComponent, + }, + ], + }, + { + path: 'queries', + resolve: { breadcrumb: i18nBreadcrumbResolver }, + data: { title: 'admin.reports.items.title', breadcrumbKey: 'admin.reports.items' }, + children: [ + { + path: '', + component: FilteredItemsComponent, + }, + ], + }, +]; diff --git a/src/app/admin/admin-reports/admin-reports-routing.module.ts b/src/app/admin/admin-reports/admin-reports-routing.module.ts deleted file mode 100644 index e891fa9ad4..0000000000 --- a/src/app/admin/admin-reports/admin-reports-routing.module.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { NgModule } from '@angular/core'; -import { RouterModule } from '@angular/router'; - -import { I18nBreadcrumbResolver } from '../../core/breadcrumbs/i18n-breadcrumb.resolver'; -import { FilteredCollectionsComponent } from './filtered-collections/filtered-collections.component'; -import { FilteredItemsComponent } from './filtered-items/filtered-items.component'; - -@NgModule({ - imports: [ - RouterModule.forChild([ - { - path: 'collections', - resolve: { breadcrumb: I18nBreadcrumbResolver }, - data: { title: 'admin.reports.collections.title', breadcrumbKey: 'admin.reports.collections' }, - children: [ - { - path: '', - component: FilteredCollectionsComponent, - }, - ], - }, - { - path: 'queries', - resolve: { breadcrumb: I18nBreadcrumbResolver }, - data: { title: 'admin.reports.items.title', breadcrumbKey: 'admin.reports.items' }, - children: [ - { - path: '', - component: FilteredItemsComponent, - }, - ], - }, - ]), - ], -}) -export class AdminReportsRoutingModule { - -} diff --git a/src/app/admin/admin-reports/admin-reports.module.ts b/src/app/admin/admin-reports/admin-reports.module.ts deleted file mode 100644 index 6eb713f0a4..0000000000 --- a/src/app/admin/admin-reports/admin-reports.module.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { CommonModule } from '@angular/common'; -import { NgModule } from '@angular/core'; -import { RouterModule } from '@angular/router'; -import { NgbAccordionModule } from '@ng-bootstrap/ng-bootstrap'; - -import { FormModule } from '../../shared/form/form.module'; -import { SharedModule } from '../../shared/shared.module'; -import { AdminReportsRoutingModule } from './admin-reports-routing.module'; -import { FilteredCollectionsComponent } from './filtered-collections/filtered-collections.component'; -import { FilteredItemsComponent } from './filtered-items/filtered-items.component'; -import { FiltersComponent } from './filters-section/filters-section.component'; - -@NgModule({ - imports: [ - CommonModule, - SharedModule, - RouterModule, - FormModule, - AdminReportsRoutingModule, - NgbAccordionModule, - ], - declarations: [ - FilteredCollectionsComponent, - FilteredItemsComponent, - FiltersComponent, - ], -}) -export class AdminReportsModule { -} diff --git a/src/app/admin/admin-reports/filtered-collections/filtered-collections.component.spec.ts b/src/app/admin/admin-reports/filtered-collections/filtered-collections.component.spec.ts index 1fa350645b..dff6445225 100644 --- a/src/app/admin/admin-reports/filtered-collections/filtered-collections.component.spec.ts +++ b/src/app/admin/admin-reports/filtered-collections/filtered-collections.component.spec.ts @@ -39,7 +39,6 @@ describe('FiltersComponent', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [FilteredCollectionsComponent], imports: [ NgbAccordionModule, TranslateModule.forRoot({ @@ -49,6 +48,7 @@ describe('FiltersComponent', () => { }, }), HttpClientTestingModule, + FilteredCollectionsComponent, ], providers: [ FormBuilder, diff --git a/src/app/admin/admin-reports/filtered-collections/filtered-collections.component.ts b/src/app/admin/admin-reports/filtered-collections/filtered-collections.component.ts index 84e334e971..e1f54bd8d3 100644 --- a/src/app/admin/admin-reports/filtered-collections/filtered-collections.component.ts +++ b/src/app/admin/admin-reports/filtered-collections/filtered-collections.component.ts @@ -1,12 +1,21 @@ +import { + KeyValuePipe, + NgForOf, +} from '@angular/common'; import { Component, + OnInit, ViewChild, } from '@angular/core'; import { FormBuilder, FormGroup, } from '@angular/forms'; -import { NgbAccordion } from '@ng-bootstrap/ng-bootstrap'; +import { + NgbAccordion, + NgbAccordionModule, +} from '@ng-bootstrap/ng-bootstrap'; +import { TranslateModule } from '@ngx-translate/core'; import { Observable } from 'rxjs'; import { RestRequestMethod } from 'src/app/core/data/rest-request-method'; import { DspaceRestService } from 'src/app/core/dspace-rest/dspace-rest.service'; @@ -23,8 +32,16 @@ import { FilteredCollections } from './filtered-collections.model'; selector: 'ds-report-filtered-collections', templateUrl: './filtered-collections.component.html', styleUrls: ['./filtered-collections.component.scss'], + imports: [ + TranslateModule, + NgbAccordionModule, + FiltersComponent, + KeyValuePipe, + NgForOf, + ], + standalone: true, }) -export class FilteredCollectionsComponent { +export class FilteredCollectionsComponent implements OnInit { queryForm: FormGroup; results: FilteredCollections = new FilteredCollections(); diff --git a/src/app/admin/admin-reports/filtered-items/filtered-items.component.ts b/src/app/admin/admin-reports/filtered-items/filtered-items.component.ts index c63880ae48..4cb93aa9be 100644 --- a/src/app/admin/admin-reports/filtered-items/filtered-items.component.ts +++ b/src/app/admin/admin-reports/filtered-items/filtered-items.component.ts @@ -1,5 +1,11 @@ +import { + AsyncPipe, + NgForOf, + NgIf, +} from '@angular/common'; import { Component, + OnInit, ViewChild, } from '@angular/core'; import { @@ -7,9 +13,16 @@ import { FormBuilder, FormControl, FormGroup, + ReactiveFormsModule, } from '@angular/forms'; -import { NgbAccordion } from '@ng-bootstrap/ng-bootstrap'; -import { TranslateService } from '@ngx-translate/core'; +import { + NgbAccordion, + NgbAccordionModule, +} from '@ng-bootstrap/ng-bootstrap'; +import { + TranslateModule, + TranslateService, +} from '@ngx-translate/core'; import { map, Observable, @@ -43,8 +56,18 @@ import { QueryPredicate } from './query-predicate.model'; selector: 'ds-report-filtered-items', templateUrl: './filtered-items.component.html', styleUrls: ['./filtered-items.component.scss'], + imports: [ + ReactiveFormsModule, + NgbAccordionModule, + TranslateModule, + AsyncPipe, + NgIf, + NgForOf, + FiltersComponent, + ], + standalone: true, }) -export class FilteredItemsComponent { +export class FilteredItemsComponent implements OnInit { collections: OptionVO[]; presetQueries: PresetQuery[]; @@ -68,7 +91,7 @@ export class FilteredItemsComponent { private formBuilder: FormBuilder, private restService: DspaceRestService) {} - ngOnInit() { + ngOnInit(): void { this.loadCollections(); this.loadPresetQueries(); this.loadMetadataFields(); diff --git a/src/app/admin/admin-reports/filters-section/filters-section.component.spec.ts b/src/app/admin/admin-reports/filters-section/filters-section.component.spec.ts index 6b8b069f93..14bc6df593 100644 --- a/src/app/admin/admin-reports/filters-section/filters-section.component.spec.ts +++ b/src/app/admin/admin-reports/filters-section/filters-section.component.spec.ts @@ -20,7 +20,6 @@ describe('FiltersComponent', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - declarations: [FiltersComponent], imports: [ TranslateModule.forRoot({ loader: { @@ -28,6 +27,7 @@ describe('FiltersComponent', () => { useClass: TranslateLoaderMock, }, }), + FiltersComponent, ], providers: [ FormBuilder, diff --git a/src/app/admin/admin-reports/filters-section/filters-section.component.ts b/src/app/admin/admin-reports/filters-section/filters-section.component.ts index a739d849f8..85b7932ab4 100644 --- a/src/app/admin/admin-reports/filters-section/filters-section.component.ts +++ b/src/app/admin/admin-reports/filters-section/filters-section.component.ts @@ -1,3 +1,4 @@ +import { NgForOf } from '@angular/common'; import { Component, Input, @@ -6,7 +7,9 @@ import { FormBuilder, FormControl, FormGroup, + ReactiveFormsModule, } from '@angular/forms'; +import { TranslateModule } from '@ngx-translate/core'; import { Filter } from './filter.model'; import { FilterGroup } from './filter-group.model'; @@ -19,6 +22,12 @@ import { FilterGroup } from './filter-group.model'; selector: 'ds-filters', templateUrl: './filters-section.component.html', styleUrls: ['./filters-section.component.scss'], + imports: [ + NgForOf, + ReactiveFormsModule, + TranslateModule, + ], + standalone: true, }) export class FiltersComponent { diff --git a/src/app/admin/admin-routes.ts b/src/app/admin/admin-routes.ts new file mode 100644 index 0000000000..e5afe09cc7 --- /dev/null +++ b/src/app/admin/admin-routes.ts @@ -0,0 +1,85 @@ +import { Route } from '@angular/router'; + +import { i18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver'; +import { AdminCurationTasksComponent } from './admin-curation-tasks/admin-curation-tasks.component'; +import { BatchImportPageComponent } from './admin-import-batch-page/batch-import-page.component'; +import { ThemedMetadataImportPageComponent } from './admin-import-metadata-page/themed-metadata-import-page.component'; +import { + LDN_PATH, + NOTIFICATIONS_MODULE_PATH, + NOTIFY_DASHBOARD_MODULE_PATH, + REGISTRIES_MODULE_PATH, + REPORTS_MODULE_PATH, +} from './admin-routing-paths'; +import { ThemedAdminSearchPageComponent } from './admin-search-page/themed-admin-search-page.component'; +import { ThemedAdminWorkflowPageComponent } from './admin-workflow-page/themed-admin-workflow-page.component'; + +export const ROUTES: Route[] = [ + { + path: NOTIFICATIONS_MODULE_PATH, + loadChildren: () => import('./admin-notifications/admin-notifications-routes') + .then((m) => m.ROUTES), + }, + { + path: REGISTRIES_MODULE_PATH, + loadChildren: () => import('./admin-registries/admin-registries-routes') + .then((m) => m.ROUTES), + }, + { + path: 'search', + resolve: { breadcrumb: i18nBreadcrumbResolver }, + component: ThemedAdminSearchPageComponent, + data: { title: 'admin.search.title', breadcrumbKey: 'admin.search' }, + }, + { + path: 'workflow', + resolve: { breadcrumb: i18nBreadcrumbResolver }, + component: ThemedAdminWorkflowPageComponent, + data: { title: 'admin.workflow.title', breadcrumbKey: 'admin.workflow' }, + }, + { + path: 'curation-tasks', + resolve: { breadcrumb: i18nBreadcrumbResolver }, + component: AdminCurationTasksComponent, + data: { title: 'admin.curation-tasks.title', breadcrumbKey: 'admin.curation-tasks' }, + }, + { + path: 'metadata-import', + resolve: { breadcrumb: i18nBreadcrumbResolver }, + component: ThemedMetadataImportPageComponent, + data: { title: 'admin.metadata-import.title', breadcrumbKey: 'admin.metadata-import' }, + }, + { + path: 'batch-import', + resolve: { breadcrumb: i18nBreadcrumbResolver }, + component: BatchImportPageComponent, + data: { title: 'admin.batch-import.title', breadcrumbKey: 'admin.batch-import' }, + }, + { + path: 'system-wide-alert', + resolve: { breadcrumb: i18nBreadcrumbResolver }, + loadChildren: () => import('../system-wide-alert/system-wide-alert-routes').then((m) => m.ROUTES), + data: { title: 'admin.system-wide-alert.title', breadcrumbKey: 'admin.system-wide-alert' }, + }, + { + path: LDN_PATH, + children: [ + { path: '', pathMatch: 'full', redirectTo: 'services' }, + { + path: 'services', + loadChildren: () => import('./admin-ldn-services/admin-ldn-services-routes') + .then((m) => m.ROUTES), + }, + ], + }, + { + path: REPORTS_MODULE_PATH, + loadChildren: () => import('./admin-reports/admin-reports-routes') + .then((m) => m.ROUTES), + }, + { + path: NOTIFY_DASHBOARD_MODULE_PATH, + loadChildren: () => import('./admin-notify-dashboard/admin-notify-dashboard-routes') + .then((m) => m.ROUTES), + }, +]; diff --git a/src/app/admin/admin-routing.module.ts b/src/app/admin/admin-routing.module.ts deleted file mode 100644 index d9255e6482..0000000000 --- a/src/app/admin/admin-routing.module.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { NgModule } from '@angular/core'; -import { RouterModule } from '@angular/router'; - -import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver'; -import { I18nBreadcrumbsService } from '../core/breadcrumbs/i18n-breadcrumbs.service'; -import { SiteAdministratorGuard } from '../core/data/feature-authorization/feature-authorization-guard/site-administrator.guard'; -import { AdminCurationTasksComponent } from './admin-curation-tasks/admin-curation-tasks.component'; -import { BatchImportPageComponent } from './admin-import-batch-page/batch-import-page.component'; -import { MetadataImportPageComponent } from './admin-import-metadata-page/metadata-import-page.component'; -import { - LDN_PATH, - NOTIFICATIONS_MODULE_PATH, - NOTIFY_DASHBOARD_MODULE_PATH, - REGISTRIES_MODULE_PATH, - REPORTS_MODULE_PATH, -} from './admin-routing-paths'; -import { AdminSearchPageComponent } from './admin-search-page/admin-search-page.component'; -import { AdminWorkflowPageComponent } from './admin-workflow-page/admin-workflow-page.component'; - -@NgModule({ - imports: [ - RouterModule.forChild([ - { - path: NOTIFICATIONS_MODULE_PATH, - loadChildren: () => import('./admin-notifications/admin-notifications.module') - .then((m) => m.AdminNotificationsModule), - }, - { - path: REGISTRIES_MODULE_PATH, - loadChildren: () => import('./admin-registries/admin-registries.module') - .then((m) => m.AdminRegistriesModule), - canActivate: [SiteAdministratorGuard], - }, - { - path: 'search', - resolve: { breadcrumb: I18nBreadcrumbResolver }, - component: AdminSearchPageComponent, - data: { title: 'admin.search.title', breadcrumbKey: 'admin.search' }, - canActivate: [SiteAdministratorGuard], - }, - { - path: 'workflow', - resolve: { breadcrumb: I18nBreadcrumbResolver }, - component: AdminWorkflowPageComponent, - data: { title: 'admin.workflow.title', breadcrumbKey: 'admin.workflow' }, - canActivate: [SiteAdministratorGuard], - }, - { - path: 'curation-tasks', - resolve: { breadcrumb: I18nBreadcrumbResolver }, - component: AdminCurationTasksComponent, - data: { title: 'admin.curation-tasks.title', breadcrumbKey: 'admin.curation-tasks' }, - canActivate: [SiteAdministratorGuard], - }, - { - path: 'metadata-import', - resolve: { breadcrumb: I18nBreadcrumbResolver }, - component: MetadataImportPageComponent, - data: { title: 'admin.metadata-import.title', breadcrumbKey: 'admin.metadata-import' }, - canActivate: [SiteAdministratorGuard], - }, - { - path: 'batch-import', - resolve: { breadcrumb: I18nBreadcrumbResolver }, - component: BatchImportPageComponent, - data: { title: 'admin.batch-import.title', breadcrumbKey: 'admin.batch-import' }, - canActivate: [SiteAdministratorGuard], - }, - { - path: 'system-wide-alert', - resolve: { breadcrumb: I18nBreadcrumbResolver }, - loadChildren: () => import('../system-wide-alert/system-wide-alert.module').then((m) => m.SystemWideAlertModule), - data: { title: 'admin.system-wide-alert.title', breadcrumbKey: 'admin.system-wide-alert' }, - canActivate: [SiteAdministratorGuard], - }, - { - path: LDN_PATH, - children: [ - { path: '', pathMatch: 'full', redirectTo: 'services' }, - { - path: 'services', - loadChildren: () => import('./admin-ldn-services/admin-ldn-services.module') - .then((m) => m.AdminLdnServicesModule), - }, - ], - }, - { - path: REPORTS_MODULE_PATH, - loadChildren: () => import('./admin-reports/admin-reports.module') - .then((m) => m.AdminReportsModule), - }, - { - path: NOTIFY_DASHBOARD_MODULE_PATH, - loadChildren: () => import('./admin-notify-dashboard/admin-notify-dashboard.module') - .then((m) => m.AdminNotifyDashboardModule), - }, - ]), - ], - providers: [ - I18nBreadcrumbResolver, - I18nBreadcrumbsService, - ], -}) -export class AdminRoutingModule { - -} diff --git a/src/app/admin/admin-search-page/admin-search-page.component.spec.ts b/src/app/admin/admin-search-page/admin-search-page.component.spec.ts index 43c7d86a08..d3a39f12f4 100644 --- a/src/app/admin/admin-search-page/admin-search-page.component.spec.ts +++ b/src/app/admin/admin-search-page/admin-search-page.component.spec.ts @@ -4,17 +4,27 @@ import { TestBed, waitForAsync, } from '@angular/core/testing'; +import { ActivatedRoute } from '@angular/router'; +import { ThemedConfigurationSearchPageComponent } from '../../search-page/themed-configuration-search-page.component'; +import { ActivatedRouteStub } from '../../shared/testing/active-router.stub'; import { AdminSearchPageComponent } from './admin-search-page.component'; describe('AdminSearchPageComponent', () => { let component: AdminSearchPageComponent; let fixture: ComponentFixture; - beforeEach(waitForAsync(() => { - TestBed.configureTestingModule({ - declarations: [AdminSearchPageComponent], + beforeEach(waitForAsync(async () => { + await TestBed.configureTestingModule({ + imports: [AdminSearchPageComponent], + providers: [ + { provide: ActivatedRoute, useValue: new ActivatedRouteStub() }, + ], schemas: [NO_ERRORS_SCHEMA], + }).overrideComponent(AdminSearchPageComponent, { + remove: { + imports: [ThemedConfigurationSearchPageComponent], + }, }) .compileComponents(); })); diff --git a/src/app/admin/admin-search-page/admin-search-page.component.ts b/src/app/admin/admin-search-page/admin-search-page.component.ts index 6508b20efa..4ae11a9d47 100644 --- a/src/app/admin/admin-search-page/admin-search-page.component.ts +++ b/src/app/admin/admin-search-page/admin-search-page.component.ts @@ -1,11 +1,14 @@ import { Component } from '@angular/core'; import { Context } from '../../core/shared/context.model'; +import { ThemedConfigurationSearchPageComponent } from '../../search-page/themed-configuration-search-page.component'; @Component({ - selector: 'ds-admin-search-page', + selector: 'ds-base-admin-search-page', templateUrl: './admin-search-page.component.html', styleUrls: ['./admin-search-page.component.scss'], + standalone: true, + imports: [ThemedConfigurationSearchPageComponent], }) /** diff --git a/src/app/admin/admin-search-page/admin-search-results/admin-search-result-grid-element/collection-search-result/collection-admin-search-result-grid-element.component.spec.ts b/src/app/admin/admin-search-page/admin-search-results/admin-search-result-grid-element/collection-search-result/collection-admin-search-result-grid-element.component.spec.ts index 249c6b15bb..470b3a5271 100644 --- a/src/app/admin/admin-search-page/admin-search-results/admin-search-result-grid-element/collection-search-result/collection-admin-search-result-grid-element.component.spec.ts +++ b/src/app/admin/admin-search-page/admin-search-results/admin-search-result-grid-element/collection-search-result/collection-admin-search-result-grid-element.component.spec.ts @@ -20,7 +20,6 @@ import { mockTruncatableService } from '../../../../../shared/mocks/mock-trucata import { getMockThemeService } from '../../../../../shared/mocks/theme-service.mock'; import { CollectionElementLinkType } from '../../../../../shared/object-collection/collection-element-link.type'; import { CollectionSearchResult } from '../../../../../shared/object-collection/shared/collection-search-result.model'; -import { SharedModule } from '../../../../../shared/shared.module'; import { AuthServiceStub } from '../../../../../shared/testing/auth-service.stub'; import { AuthorizationDataServiceStub } from '../../../../../shared/testing/authorization-service.stub'; import { FileServiceStub } from '../../../../../shared/testing/file-service.stub'; @@ -52,9 +51,8 @@ describe('CollectionAdminSearchResultGridElementComponent', () => { NoopAnimationsModule, TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), - SharedModule, + CollectionAdminSearchResultGridElementComponent, ], - declarations: [CollectionAdminSearchResultGridElementComponent], providers: [ { provide: TruncatableService, useValue: mockTruncatableService }, { provide: BitstreamDataService, useValue: {} }, diff --git a/src/app/admin/admin-search-page/admin-search-results/admin-search-result-grid-element/collection-search-result/collection-admin-search-result-grid-element.component.ts b/src/app/admin/admin-search-page/admin-search-results/admin-search-result-grid-element/collection-search-result/collection-admin-search-result-grid-element.component.ts index 5d39b79da5..38352e7816 100644 --- a/src/app/admin/admin-search-page/admin-search-results/admin-search-result-grid-element/collection-search-result/collection-admin-search-result-grid-element.component.ts +++ b/src/app/admin/admin-search-page/admin-search-results/admin-search-result-grid-element/collection-search-result/collection-admin-search-result-grid-element.component.ts @@ -1,4 +1,8 @@ -import { Component } from '@angular/core'; +import { + Component, + OnInit, +} from '@angular/core'; +import { RouterLink } from '@angular/router'; import { getCollectionEditRoute } from '../../../../../collection-page/collection-page-routing-paths'; import { Collection } from '../../../../../core/shared/collection.model'; @@ -6,6 +10,7 @@ import { Context } from '../../../../../core/shared/context.model'; import { ViewMode } from '../../../../../core/shared/view-mode.model'; import { CollectionSearchResult } from '../../../../../shared/object-collection/shared/collection-search-result.model'; import { listableObjectComponent } from '../../../../../shared/object-collection/shared/listable-object/listable-object.decorator'; +import { CollectionSearchResultGridElementComponent } from '../../../../../shared/object-grid/search-result-grid-element/collection-search-result/collection-search-result-grid-element.component'; import { SearchResultGridElementComponent } from '../../../../../shared/object-grid/search-result-grid-element/search-result-grid-element.component'; @listableObjectComponent(CollectionSearchResult, ViewMode.GridElement, Context.AdminSearch) @@ -13,14 +18,16 @@ import { SearchResultGridElementComponent } from '../../../../../shared/object-g selector: 'ds-collection-admin-search-result-list-element', styleUrls: ['./collection-admin-search-result-grid-element.component.scss'], templateUrl: './collection-admin-search-result-grid-element.component.html', + standalone: true, + imports: [CollectionSearchResultGridElementComponent, RouterLink], }) /** * The component for displaying a list element for a collection search result on the admin search page */ -export class CollectionAdminSearchResultGridElementComponent extends SearchResultGridElementComponent { +export class CollectionAdminSearchResultGridElementComponent extends SearchResultGridElementComponent implements OnInit { editPath: string; - ngOnInit() { + ngOnInit(): void { super.ngOnInit(); this.editPath = getCollectionEditRoute(this.dso.uuid); } diff --git a/src/app/admin/admin-search-page/admin-search-results/admin-search-result-grid-element/community-search-result/community-admin-search-result-grid-element.component.spec.ts b/src/app/admin/admin-search-page/admin-search-results/admin-search-result-grid-element/community-search-result/community-admin-search-result-grid-element.component.spec.ts index 137f5be5f2..6821025653 100644 --- a/src/app/admin/admin-search-page/admin-search-results/admin-search-result-grid-element/community-search-result/community-admin-search-result-grid-element.component.spec.ts +++ b/src/app/admin/admin-search-page/admin-search-results/admin-search-result-grid-element/community-search-result/community-admin-search-result-grid-element.component.spec.ts @@ -21,7 +21,6 @@ import { mockTruncatableService } from '../../../../../shared/mocks/mock-trucata import { getMockThemeService } from '../../../../../shared/mocks/theme-service.mock'; import { CollectionElementLinkType } from '../../../../../shared/object-collection/collection-element-link.type'; import { CommunitySearchResult } from '../../../../../shared/object-collection/shared/community-search-result.model'; -import { SharedModule } from '../../../../../shared/shared.module'; import { AuthServiceStub } from '../../../../../shared/testing/auth-service.stub'; import { AuthorizationDataServiceStub } from '../../../../../shared/testing/authorization-service.stub'; import { FileServiceStub } from '../../../../../shared/testing/file-service.stub'; @@ -53,9 +52,8 @@ describe('CommunityAdminSearchResultGridElementComponent', () => { NoopAnimationsModule, TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), - SharedModule, + CommunityAdminSearchResultGridElementComponent, ], - declarations: [CommunityAdminSearchResultGridElementComponent], providers: [ { provide: TruncatableService, useValue: mockTruncatableService }, { provide: BitstreamDataService, useValue: {} }, diff --git a/src/app/admin/admin-search-page/admin-search-results/admin-search-result-grid-element/community-search-result/community-admin-search-result-grid-element.component.ts b/src/app/admin/admin-search-page/admin-search-results/admin-search-result-grid-element/community-search-result/community-admin-search-result-grid-element.component.ts index 9dd672e54c..509c1e9266 100644 --- a/src/app/admin/admin-search-page/admin-search-results/admin-search-result-grid-element/community-search-result/community-admin-search-result-grid-element.component.ts +++ b/src/app/admin/admin-search-page/admin-search-results/admin-search-result-grid-element/community-search-result/community-admin-search-result-grid-element.component.ts @@ -1,4 +1,8 @@ -import { Component } from '@angular/core'; +import { + Component, + OnInit, +} from '@angular/core'; +import { RouterLink } from '@angular/router'; import { getCommunityEditRoute } from '../../../../../community-page/community-page-routing-paths'; import { Community } from '../../../../../core/shared/community.model'; @@ -6,6 +10,7 @@ import { Context } from '../../../../../core/shared/context.model'; import { ViewMode } from '../../../../../core/shared/view-mode.model'; import { CommunitySearchResult } from '../../../../../shared/object-collection/shared/community-search-result.model'; import { listableObjectComponent } from '../../../../../shared/object-collection/shared/listable-object/listable-object.decorator'; +import { CommunitySearchResultGridElementComponent } from '../../../../../shared/object-grid/search-result-grid-element/community-search-result/community-search-result-grid-element.component'; import { SearchResultGridElementComponent } from '../../../../../shared/object-grid/search-result-grid-element/search-result-grid-element.component'; @listableObjectComponent(CommunitySearchResult, ViewMode.GridElement, Context.AdminSearch) @@ -13,14 +18,16 @@ import { SearchResultGridElementComponent } from '../../../../../shared/object-g selector: 'ds-community-admin-search-result-grid-element', styleUrls: ['./community-admin-search-result-grid-element.component.scss'], templateUrl: './community-admin-search-result-grid-element.component.html', + standalone: true, + imports: [CommunitySearchResultGridElementComponent, RouterLink], }) /** * The component for displaying a list element for a community search result on the admin search page */ -export class CommunityAdminSearchResultGridElementComponent extends SearchResultGridElementComponent { +export class CommunityAdminSearchResultGridElementComponent extends SearchResultGridElementComponent implements OnInit { editPath: string; - ngOnInit() { + ngOnInit(): void { super.ngOnInit(); this.editPath = getCommunityEditRoute(this.dso.uuid); } diff --git a/src/app/admin/admin-search-page/admin-search-results/admin-search-result-grid-element/item-search-result/item-admin-search-result-grid-element.component.spec.ts b/src/app/admin/admin-search-page/admin-search-results/admin-search-result-grid-element/item-search-result/item-admin-search-result-grid-element.component.spec.ts index 80bd4469f6..3eb8d9caf7 100644 --- a/src/app/admin/admin-search-page/admin-search-results/admin-search-result-grid-element/item-search-result/item-admin-search-result-grid-element.component.spec.ts +++ b/src/app/admin/admin-search-page/admin-search-results/admin-search-result-grid-element/item-search-result/item-admin-search-result-grid-element.component.spec.ts @@ -17,6 +17,7 @@ import { RemoteData } from '../../../../../core/data/remote-data'; import { Bitstream } from '../../../../../core/shared/bitstream.model'; import { FileService } from '../../../../../core/shared/file.service'; import { Item } from '../../../../../core/shared/item.model'; +import { ListableModule } from '../../../../../core/shared/listable.module'; import { ViewMode } from '../../../../../core/shared/view-mode.model'; import { mockTruncatableService } from '../../../../../shared/mocks/mock-trucatable.service'; import { getMockThemeService } from '../../../../../shared/mocks/theme-service.mock'; @@ -24,7 +25,6 @@ import { CollectionElementLinkType } from '../../../../../shared/object-collecti import { AccessStatusObject } from '../../../../../shared/object-collection/shared/badges/access-status-badge/access-status.model'; import { ItemSearchResult } from '../../../../../shared/object-collection/shared/item-search-result.model'; import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/remote-data.utils'; -import { SharedModule } from '../../../../../shared/shared.module'; import { AuthServiceStub } from '../../../../../shared/testing/auth-service.stub'; import { AuthorizationDataServiceStub } from '../../../../../shared/testing/authorization-service.stub'; import { FileServiceStub } from '../../../../../shared/testing/file-service.stub'; @@ -63,12 +63,12 @@ describe('ItemAdminSearchResultGridElementComponent', () => { init(); TestBed.configureTestingModule( { - declarations: [ItemAdminSearchResultGridElementComponent], imports: [ NoopAnimationsModule, TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), - SharedModule, + ListableModule, + ItemAdminSearchResultGridElementComponent, ], providers: [ { provide: TruncatableService, useValue: mockTruncatableService }, diff --git a/src/app/admin/admin-search-page/admin-search-results/admin-search-result-grid-element/item-search-result/item-admin-search-result-grid-element.component.ts b/src/app/admin/admin-search-page/admin-search-results/admin-search-result-grid-element/item-search-result/item-admin-search-result-grid-element.component.ts index b337df8ed2..fd5e641f52 100644 --- a/src/app/admin/admin-search-page/admin-search-results/admin-search-result-grid-element/item-search-result/item-admin-search-result-grid-element.component.ts +++ b/src/app/admin/admin-search-page/admin-search-results/admin-search-result-grid-element/item-search-result/item-admin-search-result-grid-element.component.ts @@ -23,12 +23,15 @@ import { import { SearchResultGridElementComponent } from '../../../../../shared/object-grid/search-result-grid-element/search-result-grid-element.component'; import { ThemeService } from '../../../../../shared/theme-support/theme.service'; import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service'; +import { ItemAdminSearchResultActionsComponent } from '../../item-admin-search-result-actions.component'; @listableObjectComponent(ItemSearchResult, ViewMode.GridElement, Context.AdminSearch) @Component({ selector: 'ds-item-admin-search-result-grid-element', styleUrls: ['./item-admin-search-result-grid-element.component.scss'], templateUrl: './item-admin-search-result-grid-element.component.html', + standalone: true, + imports: [ItemAdminSearchResultActionsComponent, DynamicComponentLoaderDirective], }) /** * The component for displaying a list element for an item search result on the admin search page diff --git a/src/app/admin/admin-search-page/admin-search-results/admin-search-result-list-element/collection-search-result/collection-admin-search-result-list-element.component.html b/src/app/admin/admin-search-page/admin-search-results/admin-search-result-list-element/collection-search-result/collection-admin-search-result-list-element.component.html index e51e207bbe..6c8342d2e6 100644 --- a/src/app/admin/admin-search-page/admin-search-results/admin-search-result-list-element/collection-search-result/collection-admin-search-result-list-element.component.html +++ b/src/app/admin/admin-search-page/admin-search-results/admin-search-result-list-element/collection-search-result/collection-admin-search-result-list-element.component.html @@ -3,7 +3,7 @@ [linkType]="linkType" [listID]="listID"> diff --git a/src/app/admin/admin-search-page/admin-search-results/admin-search-result-list-element/collection-search-result/collection-admin-search-result-list-element.component.spec.ts b/src/app/admin/admin-search-page/admin-search-results/admin-search-result-list-element/collection-search-result/collection-admin-search-result-list-element.component.spec.ts index cd7e7a237b..7a4e2da68d 100644 --- a/src/app/admin/admin-search-page/admin-search-results/admin-search-result-list-element/collection-search-result/collection-admin-search-result-list-element.component.spec.ts +++ b/src/app/admin/admin-search-page/admin-search-results/admin-search-result-list-element/collection-search-result/collection-admin-search-result-list-element.component.spec.ts @@ -15,8 +15,12 @@ import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service import { Collection } from '../../../../../core/shared/collection.model'; import { ViewMode } from '../../../../../core/shared/view-mode.model'; import { DSONameServiceMock } from '../../../../../shared/mocks/dso-name.service.mock'; +import { mockTruncatableService } from '../../../../../shared/mocks/mock-trucatable.service'; +import { getMockThemeService } from '../../../../../shared/mocks/theme-service.mock'; import { CollectionElementLinkType } from '../../../../../shared/object-collection/collection-element-link.type'; import { CollectionSearchResult } from '../../../../../shared/object-collection/shared/collection-search-result.model'; +import { CollectionSearchResultListElementComponent } from '../../../../../shared/object-list/search-result-list-element/collection-search-result/collection-search-result-list-element.component'; +import { ThemeService } from '../../../../../shared/theme-support/theme.service'; import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service'; import { CollectionAdminSearchResultListElementComponent } from './collection-admin-search-result-list-element.component'; @@ -39,14 +43,22 @@ describe('CollectionAdminSearchResultListElementComponent', () => { imports: [ TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), + CollectionAdminSearchResultListElementComponent, ], - declarations: [CollectionAdminSearchResultListElementComponent], - providers: [{ provide: TruncatableService, useValue: {} }, + providers: [ + { provide: TruncatableService, useValue: mockTruncatableService }, { provide: DSONameService, useClass: DSONameServiceMock }, - { provide: APP_CONFIG, useValue: environment }], + { provide: APP_CONFIG, useValue: environment }, + { provide: ThemeService, useValue: getMockThemeService() }, + ], schemas: [NO_ERRORS_SCHEMA], - }) - .compileComponents(); + }).overrideComponent(CollectionAdminSearchResultListElementComponent, { + remove: { + imports: [ + CollectionSearchResultListElementComponent, + ], + }, + }).compileComponents(); })); beforeEach(() => { @@ -64,7 +76,7 @@ describe('CollectionAdminSearchResultListElementComponent', () => { }); it('should render an edit button with the correct link', () => { - const a = fixture.debugElement.query(By.css('a')); + const a = fixture.debugElement.query(By.css('a[data-test="coll-link"]')); const link = a.nativeElement.href; expect(link).toContain(getCollectionEditRoute(id)); }); diff --git a/src/app/admin/admin-search-page/admin-search-results/admin-search-result-list-element/collection-search-result/collection-admin-search-result-list-element.component.ts b/src/app/admin/admin-search-page/admin-search-results/admin-search-result-list-element/collection-search-result/collection-admin-search-result-list-element.component.ts index 4ca4c3af4d..0924379ea5 100644 --- a/src/app/admin/admin-search-page/admin-search-results/admin-search-result-list-element/collection-search-result/collection-admin-search-result-list-element.component.ts +++ b/src/app/admin/admin-search-page/admin-search-results/admin-search-result-list-element/collection-search-result/collection-admin-search-result-list-element.component.ts @@ -1,4 +1,9 @@ -import { Component } from '@angular/core'; +import { + Component, + OnInit, +} from '@angular/core'; +import { RouterLink } from '@angular/router'; +import { TranslateModule } from '@ngx-translate/core'; import { getCollectionEditRoute } from '../../../../../collection-page/collection-page-routing-paths'; import { Collection } from '../../../../../core/shared/collection.model'; @@ -6,6 +11,7 @@ import { Context } from '../../../../../core/shared/context.model'; import { ViewMode } from '../../../../../core/shared/view-mode.model'; import { CollectionSearchResult } from '../../../../../shared/object-collection/shared/collection-search-result.model'; import { listableObjectComponent } from '../../../../../shared/object-collection/shared/listable-object/listable-object.decorator'; +import { CollectionSearchResultListElementComponent } from '../../../../../shared/object-list/search-result-list-element/collection-search-result/collection-search-result-list-element.component'; import { SearchResultListElementComponent } from '../../../../../shared/object-list/search-result-list-element/search-result-list-element.component'; @listableObjectComponent(CollectionSearchResult, ViewMode.ListElement, Context.AdminSearch) @@ -13,14 +19,16 @@ import { SearchResultListElementComponent } from '../../../../../shared/object-l selector: 'ds-collection-admin-search-result-list-element', styleUrls: ['./collection-admin-search-result-list-element.component.scss'], templateUrl: './collection-admin-search-result-list-element.component.html', + standalone: true, + imports: [CollectionSearchResultListElementComponent, RouterLink, TranslateModule], }) /** * The component for displaying a list element for a collection search result on the admin search page */ -export class CollectionAdminSearchResultListElementComponent extends SearchResultListElementComponent { +export class CollectionAdminSearchResultListElementComponent extends SearchResultListElementComponent implements OnInit { editPath: string; - ngOnInit() { + ngOnInit(): void { super.ngOnInit(); this.editPath = getCollectionEditRoute(this.dso.uuid); } diff --git a/src/app/admin/admin-search-page/admin-search-results/admin-search-result-list-element/community-search-result/community-admin-search-result-list-element.component.spec.ts b/src/app/admin/admin-search-page/admin-search-results/admin-search-result-list-element/community-search-result/community-admin-search-result-list-element.component.spec.ts index 3c1315cc64..04077bf590 100644 --- a/src/app/admin/admin-search-page/admin-search-results/admin-search-result-list-element/community-search-result/community-admin-search-result-list-element.component.spec.ts +++ b/src/app/admin/admin-search-page/admin-search-results/admin-search-result-list-element/community-search-result/community-admin-search-result-list-element.component.spec.ts @@ -15,8 +15,10 @@ import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service import { Community } from '../../../../../core/shared/community.model'; import { ViewMode } from '../../../../../core/shared/view-mode.model'; import { DSONameServiceMock } from '../../../../../shared/mocks/dso-name.service.mock'; +import { mockTruncatableService } from '../../../../../shared/mocks/mock-trucatable.service'; import { CollectionElementLinkType } from '../../../../../shared/object-collection/collection-element-link.type'; import { CommunitySearchResult } from '../../../../../shared/object-collection/shared/community-search-result.model'; +import { CommunitySearchResultListElementComponent } from '../../../../../shared/object-list/search-result-list-element/community-search-result/community-search-result-list-element.component'; import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service'; import { CommunityAdminSearchResultListElementComponent } from './community-admin-search-result-list-element.component'; @@ -39,13 +41,20 @@ describe('CommunityAdminSearchResultListElementComponent', () => { imports: [ TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), + CommunityAdminSearchResultListElementComponent, ], - declarations: [CommunityAdminSearchResultListElementComponent], - providers: [{ provide: TruncatableService, useValue: {} }, + providers: [ + { provide: TruncatableService, useValue: mockTruncatableService }, { provide: DSONameService, useClass: DSONameServiceMock }, - { provide: APP_CONFIG, useValue: environment }], + { provide: APP_CONFIG, useValue: environment }, + ], schemas: [NO_ERRORS_SCHEMA], }) + .overrideComponent(CommunityAdminSearchResultListElementComponent, { + remove: { + imports: [CommunitySearchResultListElementComponent], + }, + }) .compileComponents(); })); @@ -56,6 +65,7 @@ describe('CommunityAdminSearchResultListElementComponent', () => { component.linkTypes = CollectionElementLinkType; component.index = 0; component.viewModes = ViewMode; + component.ngOnInit(); fixture.detectChanges(); }); diff --git a/src/app/admin/admin-search-page/admin-search-results/admin-search-result-list-element/community-search-result/community-admin-search-result-list-element.component.ts b/src/app/admin/admin-search-page/admin-search-results/admin-search-result-list-element/community-search-result/community-admin-search-result-list-element.component.ts index a3f8b42a13..c4146dbd60 100644 --- a/src/app/admin/admin-search-page/admin-search-results/admin-search-result-list-element/community-search-result/community-admin-search-result-list-element.component.ts +++ b/src/app/admin/admin-search-page/admin-search-results/admin-search-result-list-element/community-search-result/community-admin-search-result-list-element.component.ts @@ -1,4 +1,9 @@ -import { Component } from '@angular/core'; +import { + Component, + OnInit, +} from '@angular/core'; +import { RouterLink } from '@angular/router'; +import { TranslateModule } from '@ngx-translate/core'; import { getCommunityEditRoute } from '../../../../../community-page/community-page-routing-paths'; import { Community } from '../../../../../core/shared/community.model'; @@ -6,6 +11,7 @@ import { Context } from '../../../../../core/shared/context.model'; import { ViewMode } from '../../../../../core/shared/view-mode.model'; import { CommunitySearchResult } from '../../../../../shared/object-collection/shared/community-search-result.model'; import { listableObjectComponent } from '../../../../../shared/object-collection/shared/listable-object/listable-object.decorator'; +import { CommunitySearchResultListElementComponent } from '../../../../../shared/object-list/search-result-list-element/community-search-result/community-search-result-list-element.component'; import { SearchResultListElementComponent } from '../../../../../shared/object-list/search-result-list-element/search-result-list-element.component'; @listableObjectComponent(CommunitySearchResult, ViewMode.ListElement, Context.AdminSearch) @@ -13,14 +19,16 @@ import { SearchResultListElementComponent } from '../../../../../shared/object-l selector: 'ds-community-admin-search-result-list-element', styleUrls: ['./community-admin-search-result-list-element.component.scss'], templateUrl: './community-admin-search-result-list-element.component.html', + standalone: true, + imports: [CommunitySearchResultListElementComponent, RouterLink, TranslateModule], }) /** * The component for displaying a list element for a community search result on the admin search page */ -export class CommunityAdminSearchResultListElementComponent extends SearchResultListElementComponent { +export class CommunityAdminSearchResultListElementComponent extends SearchResultListElementComponent implements OnInit { editPath: string; - ngOnInit() { + ngOnInit(): void { super.ngOnInit(); this.editPath = getCommunityEditRoute(this.dso.uuid); } diff --git a/src/app/admin/admin-search-page/admin-search-results/admin-search-result-list-element/item-search-result/item-admin-search-result-list-element.component.spec.ts b/src/app/admin/admin-search-page/admin-search-results/admin-search-result-list-element/item-search-result/item-admin-search-result-list-element.component.spec.ts index f34c2709d8..a3631473e9 100644 --- a/src/app/admin/admin-search-page/admin-search-results/admin-search-result-list-element/item-search-result/item-admin-search-result-list-element.component.spec.ts +++ b/src/app/admin/admin-search-page/admin-search-results/admin-search-result-list-element/item-search-result/item-admin-search-result-list-element.component.spec.ts @@ -13,9 +13,12 @@ import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service import { Item } from '../../../../../core/shared/item.model'; import { ViewMode } from '../../../../../core/shared/view-mode.model'; import { DSONameServiceMock } from '../../../../../shared/mocks/dso-name.service.mock'; +import { mockTruncatableService } from '../../../../../shared/mocks/mock-trucatable.service'; import { CollectionElementLinkType } from '../../../../../shared/object-collection/collection-element-link.type'; import { ItemSearchResult } from '../../../../../shared/object-collection/shared/item-search-result.model'; +import { ListableObjectComponentLoaderComponent } from '../../../../../shared/object-collection/shared/listable-object/listable-object-component-loader.component'; import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service'; +import { ItemAdminSearchResultActionsComponent } from '../../item-admin-search-result-actions.component'; import { ItemAdminSearchResultListElementComponent } from './item-admin-search-result-list-element.component'; describe('ItemAdminSearchResultListElementComponent', () => { @@ -37,13 +40,18 @@ describe('ItemAdminSearchResultListElementComponent', () => { imports: [ TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), + ItemAdminSearchResultListElementComponent, ], - declarations: [ItemAdminSearchResultListElementComponent], - providers: [{ provide: TruncatableService, useValue: {} }, + providers: [ + { provide: TruncatableService, useValue: mockTruncatableService }, { provide: DSONameService, useClass: DSONameServiceMock }, - { provide: APP_CONFIG, useValue: environment }], + { provide: APP_CONFIG, useValue: environment }, + ], schemas: [NO_ERRORS_SCHEMA], }) + .overrideComponent(ItemAdminSearchResultListElementComponent, { + remove: { imports: [ListableObjectComponentLoaderComponent, ItemAdminSearchResultActionsComponent] }, + }) .compileComponents(); })); diff --git a/src/app/admin/admin-search-page/admin-search-results/admin-search-result-list-element/item-search-result/item-admin-search-result-list-element.component.ts b/src/app/admin/admin-search-page/admin-search-results/admin-search-result-list-element/item-search-result/item-admin-search-result-list-element.component.ts index c181640c2c..d77e86689a 100644 --- a/src/app/admin/admin-search-page/admin-search-results/admin-search-result-list-element/item-search-result/item-admin-search-result-list-element.component.ts +++ b/src/app/admin/admin-search-page/admin-search-results/admin-search-result-list-element/item-search-result/item-admin-search-result-list-element.component.ts @@ -5,13 +5,17 @@ import { Item } from '../../../../../core/shared/item.model'; import { ViewMode } from '../../../../../core/shared/view-mode.model'; import { ItemSearchResult } from '../../../../../shared/object-collection/shared/item-search-result.model'; import { listableObjectComponent } from '../../../../../shared/object-collection/shared/listable-object/listable-object.decorator'; +import { ListableObjectComponentLoaderComponent } from '../../../../../shared/object-collection/shared/listable-object/listable-object-component-loader.component'; import { SearchResultListElementComponent } from '../../../../../shared/object-list/search-result-list-element/search-result-list-element.component'; +import { ItemAdminSearchResultActionsComponent } from '../../item-admin-search-result-actions.component'; @listableObjectComponent(ItemSearchResult, ViewMode.ListElement, Context.AdminSearch) @Component({ selector: 'ds-item-admin-search-result-list-element', styleUrls: ['./item-admin-search-result-list-element.component.scss'], templateUrl: './item-admin-search-result-list-element.component.html', + standalone: true, + imports: [ListableObjectComponentLoaderComponent, ItemAdminSearchResultActionsComponent], }) /** * The component for displaying a list element for an item search result on the admin search page diff --git a/src/app/admin/admin-search-page/admin-search-results/item-admin-search-result-actions.component.spec.ts b/src/app/admin/admin-search-page/admin-search-results/item-admin-search-result-actions.component.spec.ts index 33a2cacb80..c598c5b40d 100644 --- a/src/app/admin/admin-search-page/admin-search-results/item-admin-search-result-actions.component.spec.ts +++ b/src/app/admin/admin-search-page/admin-search-results/item-admin-search-result-actions.component.spec.ts @@ -39,8 +39,8 @@ describe('ItemAdminSearchResultActionsComponent', () => { imports: [ TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), + ItemAdminSearchResultActionsComponent, ], - declarations: [ItemAdminSearchResultActionsComponent], schemas: [NO_ERRORS_SCHEMA], }) .compileComponents(); diff --git a/src/app/admin/admin-search-page/admin-search-results/item-admin-search-result-actions.component.ts b/src/app/admin/admin-search-page/admin-search-results/item-admin-search-result-actions.component.ts index 54f6c82f3b..89d51481d7 100644 --- a/src/app/admin/admin-search-page/admin-search-results/item-admin-search-result-actions.component.ts +++ b/src/app/admin/admin-search-page/admin-search-results/item-admin-search-result-actions.component.ts @@ -1,7 +1,13 @@ +import { + NgClass, + NgIf, +} from '@angular/common'; import { Component, Input, } from '@angular/core'; +import { RouterLink } from '@angular/router'; +import { TranslateModule } from '@ngx-translate/core'; import { Item } from '../../../core/shared/item.model'; import { URLCombiner } from '../../../core/url-combiner/url-combiner'; @@ -19,6 +25,8 @@ import { getItemEditRoute } from '../../../item-page/item-page-routing-paths'; selector: 'ds-item-admin-search-result-actions-element', styleUrls: ['./item-admin-search-result-actions.component.scss'], templateUrl: './item-admin-search-result-actions.component.html', + standalone: true, + imports: [NgClass, RouterLink, NgIf, TranslateModule], }) /** * The component for displaying the actions for a list element for an item search result on the admin search page diff --git a/src/app/admin/admin-search-page/admin-search.module.ts b/src/app/admin/admin-search-page/admin-search.module.ts deleted file mode 100644 index f8eec908ff..0000000000 --- a/src/app/admin/admin-search-page/admin-search.module.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { NgModule } from '@angular/core'; - -import { JournalEntitiesModule } from '../../entity-groups/journal-entities/journal-entities.module'; -import { ResearchEntitiesModule } from '../../entity-groups/research-entities/research-entities.module'; -import { SearchModule } from '../../shared/search/search.module'; -import { SharedModule } from '../../shared/shared.module'; -import { AdminSearchPageComponent } from './admin-search-page.component'; -import { CollectionAdminSearchResultGridElementComponent } from './admin-search-results/admin-search-result-grid-element/collection-search-result/collection-admin-search-result-grid-element.component'; -import { CommunityAdminSearchResultGridElementComponent } from './admin-search-results/admin-search-result-grid-element/community-search-result/community-admin-search-result-grid-element.component'; -import { ItemAdminSearchResultGridElementComponent } from './admin-search-results/admin-search-result-grid-element/item-search-result/item-admin-search-result-grid-element.component'; -import { CollectionAdminSearchResultListElementComponent } from './admin-search-results/admin-search-result-list-element/collection-search-result/collection-admin-search-result-list-element.component'; -import { CommunityAdminSearchResultListElementComponent } from './admin-search-results/admin-search-result-list-element/community-search-result/community-admin-search-result-list-element.component'; -import { ItemAdminSearchResultListElementComponent } from './admin-search-results/admin-search-result-list-element/item-search-result/item-admin-search-result-list-element.component'; -import { ItemAdminSearchResultActionsComponent } from './admin-search-results/item-admin-search-result-actions.component'; - -const ENTRY_COMPONENTS = [ - // put only entry components that use custom decorator - ItemAdminSearchResultListElementComponent, - CommunityAdminSearchResultListElementComponent, - CollectionAdminSearchResultListElementComponent, - ItemAdminSearchResultGridElementComponent, - CommunityAdminSearchResultGridElementComponent, - CollectionAdminSearchResultGridElementComponent, - ItemAdminSearchResultActionsComponent, -]; - -@NgModule({ - imports: [ - SearchModule, - SharedModule.withEntryComponents(), - JournalEntitiesModule.withEntryComponents(), - ResearchEntitiesModule.withEntryComponents(), - ], - declarations: [ - AdminSearchPageComponent, - ...ENTRY_COMPONENTS, - ], -}) -export class AdminSearchModule { - /** - * NOTE: this method allows to resolve issue with components that using a custom decorator - * which are not loaded during SSR otherwise - */ - static withEntryComponents() { - return { - ngModule: SharedModule, - providers: ENTRY_COMPONENTS.map((component) => ({ provide: component })), - }; - } -} diff --git a/src/app/admin/admin-search-page/themed-admin-search-page.component.ts b/src/app/admin/admin-search-page/themed-admin-search-page.component.ts new file mode 100644 index 0000000000..d49c184784 --- /dev/null +++ b/src/app/admin/admin-search-page/themed-admin-search-page.component.ts @@ -0,0 +1,29 @@ +import { Component } from '@angular/core'; + +import { ThemedComponent } from '../../shared/theme-support/themed.component'; +import { AdminSearchPageComponent } from './admin-search-page.component'; + +/** + * Themed wrapper for {@link AdminSearchPageComponent} + */ +@Component({ + selector: 'ds-admin-search-page', + templateUrl: '../../shared/theme-support/themed.component.html', + standalone: true, + imports: [AdminSearchPageComponent], +}) +export class ThemedAdminSearchPageComponent extends ThemedComponent { + + protected getComponentName(): string { + return 'AdminSearchPageComponent'; + } + + protected importThemedComponent(themeName: string): Promise { + return import(`../../../themes/${themeName}/app/admin/admin-search-page/admin-search-page.component`); + } + + protected importUnthemedComponent(): Promise { + return import('./admin-search-page.component'); + } + +} diff --git a/src/app/admin/admin-sidebar/admin-sidebar-section/admin-sidebar-section.component.html b/src/app/admin/admin-sidebar/admin-sidebar-section/admin-sidebar-section.component.html index 24ba17fff4..30a7a3353b 100644 --- a/src/app/admin/admin-sidebar/admin-sidebar-section/admin-sidebar-section.component.html +++ b/src/app/admin/admin-sidebar/admin-sidebar-section/admin-sidebar-section.component.html @@ -14,7 +14,9 @@ diff --git a/src/app/admin/admin-sidebar/admin-sidebar-section/admin-sidebar-section.component.spec.ts b/src/app/admin/admin-sidebar/admin-sidebar-section/admin-sidebar-section.component.spec.ts index 3131899c49..fdee7c70e4 100644 --- a/src/app/admin/admin-sidebar/admin-sidebar-section/admin-sidebar-section.component.spec.ts +++ b/src/app/admin/admin-sidebar/admin-sidebar-section/admin-sidebar-section.component.spec.ts @@ -25,19 +25,13 @@ describe('AdminSidebarSectionComponent', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [NoopAnimationsModule, RouterTestingModule, TranslateModule.forRoot()], - declarations: [AdminSidebarSectionComponent, TestComponent], + imports: [NoopAnimationsModule, RouterTestingModule, TranslateModule.forRoot(), AdminSidebarSectionComponent, TestComponent], providers: [ { provide: 'sectionDataProvider', useValue: { model: { link: 'google.com' }, icon: iconString } }, { provide: MenuService, useValue: menuService }, { provide: CSSVariableService, useClass: CSSVariableServiceStub }, ], - }).overrideComponent(AdminSidebarSectionComponent, { - set: { - entryComponents: [TestComponent], - }, - }) - .compileComponents(); + }).compileComponents(); })); beforeEach(() => { @@ -65,19 +59,13 @@ describe('AdminSidebarSectionComponent', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [NoopAnimationsModule, RouterTestingModule, TranslateModule.forRoot()], - declarations: [AdminSidebarSectionComponent, TestComponent], + imports: [NoopAnimationsModule, RouterTestingModule, TranslateModule.forRoot(), AdminSidebarSectionComponent, TestComponent], providers: [ { provide: 'sectionDataProvider', useValue: { model: { link: 'google.com', disabled: true }, icon: iconString } }, { provide: MenuService, useValue: menuService }, { provide: CSSVariableService, useClass: CSSVariableServiceStub }, ], - }).overrideComponent(AdminSidebarSectionComponent, { - set: { - entryComponents: [TestComponent], - }, - }) - .compileComponents(); + }).compileComponents(); })); beforeEach(() => { @@ -107,6 +95,8 @@ describe('AdminSidebarSectionComponent', () => { @Component({ selector: 'ds-test-cmp', template: ``, + standalone: true, + imports: [RouterTestingModule], }) class TestComponent { } diff --git a/src/app/admin/admin-sidebar/admin-sidebar-section/admin-sidebar-section.component.ts b/src/app/admin/admin-sidebar/admin-sidebar-section/admin-sidebar-section.component.ts index d677c7f8ba..2910f948a2 100644 --- a/src/app/admin/admin-sidebar/admin-sidebar-section/admin-sidebar-section.component.ts +++ b/src/app/admin/admin-sidebar/admin-sidebar-section/admin-sidebar-section.component.ts @@ -1,18 +1,23 @@ +import { NgClass } from '@angular/common'; import { Component, Inject, Injector, OnInit, } from '@angular/core'; -import { Router } from '@angular/router'; +import { + Router, + RouterLink, +} from '@angular/router'; +import { TranslateModule } from '@ngx-translate/core'; import { isEmpty } from '../../../shared/empty.util'; import { MenuService } from '../../../shared/menu/menu.service'; import { MenuID } from '../../../shared/menu/menu-id.model'; import { LinkMenuItemModel } from '../../../shared/menu/menu-item/models/link.model'; -import { rendersSectionForMenu } from '../../../shared/menu/menu-section.decorator'; import { MenuSection } from '../../../shared/menu/menu-section.model'; import { MenuSectionComponent } from '../../../shared/menu/menu-section/menu-section.component'; +import { BrowserOnlyPipe } from '../../../shared/utils/browser-only.pipe'; /** * Represents a non-expandable section in the admin sidebar @@ -21,9 +26,10 @@ import { MenuSectionComponent } from '../../../shared/menu/menu-section/menu-sec selector: 'ds-admin-sidebar-section', templateUrl: './admin-sidebar-section.component.html', styleUrls: ['./admin-sidebar-section.component.scss'], + standalone: true, + imports: [NgClass, RouterLink, TranslateModule, BrowserOnlyPipe], }) -@rendersSectionForMenu(MenuID.ADMIN, false) export class AdminSidebarSectionComponent extends MenuSectionComponent implements OnInit { /** diff --git a/src/app/admin/admin-sidebar/admin-sidebar.component.html b/src/app/admin/admin-sidebar/admin-sidebar.component.html index f3d5409a64..41376f777e 100644 --- a/src/app/admin/admin-sidebar/admin-sidebar.component.html +++ b/src/app/admin/admin-sidebar/admin-sidebar.component.html @@ -42,6 +42,7 @@ diff --git a/src/app/community-page/community-page.component.ts b/src/app/community-page/community-page.component.ts index 916de6f67b..d04ecbee19 100644 --- a/src/app/community-page/community-page.component.ts +++ b/src/app/community-page/community-page.component.ts @@ -1,3 +1,7 @@ +import { + AsyncPipe, + NgIf, +} from '@angular/common'; import { ChangeDetectionStrategy, Component, @@ -6,7 +10,10 @@ import { import { ActivatedRoute, Router, + RouterModule, + RouterOutlet, } from '@angular/router'; +import { TranslateModule } from '@ngx-translate/core'; import { Observable } from 'rxjs'; import { filter, @@ -24,15 +31,47 @@ import { Bitstream } from '../core/shared/bitstream.model'; import { Community } from '../core/shared/community.model'; import { getAllSucceededRemoteDataPayload } from '../core/shared/operators'; import { fadeInOut } from '../shared/animations/fade'; +import { ThemedComcolPageBrowseByComponent } from '../shared/comcol/comcol-page-browse-by/themed-comcol-page-browse-by.component'; +import { ThemedComcolPageContentComponent } from '../shared/comcol/comcol-page-content/themed-comcol-page-content.component'; +import { ThemedComcolPageHandleComponent } from '../shared/comcol/comcol-page-handle/themed-comcol-page-handle.component'; +import { ComcolPageHeaderComponent } from '../shared/comcol/comcol-page-header/comcol-page-header.component'; +import { ComcolPageLogoComponent } from '../shared/comcol/comcol-page-logo/comcol-page-logo.component'; +import { DsoEditMenuComponent } from '../shared/dso-page/dso-edit-menu/dso-edit-menu.component'; import { hasValue } from '../shared/empty.util'; +import { ErrorComponent } from '../shared/error/error.component'; +import { ThemedLoadingComponent } from '../shared/loading/themed-loading.component'; +import { VarDirective } from '../shared/utils/var.directive'; +import { ViewTrackerComponent } from '../statistics/angulartics/dspace/view-tracker.component'; import { getCommunityPageRoute } from './community-page-routing-paths'; +import { ThemedCollectionPageSubCollectionListComponent } from './sections/sub-com-col-section/sub-collection-list/themed-community-page-sub-collection-list.component'; +import { ThemedCommunityPageSubCommunityListComponent } from './sections/sub-com-col-section/sub-community-list/themed-community-page-sub-community-list.component'; @Component({ - selector: 'ds-community-page', + selector: 'ds-base-community-page', styleUrls: ['./community-page.component.scss'], templateUrl: './community-page.component.html', changeDetection: ChangeDetectionStrategy.OnPush, animations: [fadeInOut], + imports: [ + ThemedComcolPageContentComponent, + ErrorComponent, + ThemedLoadingComponent, + NgIf, + TranslateModule, + ThemedCommunityPageSubCommunityListComponent, + ThemedCollectionPageSubCollectionListComponent, + ThemedComcolPageBrowseByComponent, + DsoEditMenuComponent, + ThemedComcolPageHandleComponent, + ComcolPageLogoComponent, + ComcolPageHeaderComponent, + AsyncPipe, + ViewTrackerComponent, + VarDirective, + RouterOutlet, + RouterModule, + ], + standalone: true, }) /** * This component represents a detail page for a single community diff --git a/src/app/community-page/community-page.module.ts b/src/app/community-page/community-page.module.ts deleted file mode 100644 index 07434c9673..0000000000 --- a/src/app/community-page/community-page.module.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { CommonModule } from '@angular/common'; -import { NgModule } from '@angular/core'; - -import { BrowseByPageModule } from '../browse-by/browse-by-page.module'; -import { ComcolModule } from '../shared/comcol/comcol.module'; -import { DsoPageModule } from '../shared/dso-page/dso-page.module'; -import { SharedModule } from '../shared/shared.module'; -import { StatisticsModule } from '../statistics/statistics.module'; -import { CommunityFormModule } from './community-form/community-form.module'; -import { CommunityPageComponent } from './community-page.component'; -import { CommunityPageRoutingModule } from './community-page-routing.module'; -import { CreateCommunityPageComponent } from './create-community-page/create-community-page.component'; -import { DeleteCommunityPageComponent } from './delete-community-page/delete-community-page.component'; -import { CommunityPageSubCollectionListComponent } from './sections/sub-com-col-section/sub-collection-list/community-page-sub-collection-list.component'; -import { ThemedCollectionPageSubCollectionListComponent } from './sections/sub-com-col-section/sub-collection-list/themed-community-page-sub-collection-list.component'; -import { SubComColSectionComponent } from './sections/sub-com-col-section/sub-com-col-section.component'; -import { CommunityPageSubCommunityListComponent } from './sections/sub-com-col-section/sub-community-list/community-page-sub-community-list.component'; -import { ThemedCommunityPageSubCommunityListComponent } from './sections/sub-com-col-section/sub-community-list/themed-community-page-sub-community-list.component'; -import { ThemedCommunityPageComponent } from './themed-community-page.component'; - -const DECLARATIONS = [ - CommunityPageComponent, - ThemedCommunityPageComponent, - ThemedCommunityPageSubCommunityListComponent, - CommunityPageSubCollectionListComponent, - ThemedCollectionPageSubCollectionListComponent, - CommunityPageSubCommunityListComponent, - CreateCommunityPageComponent, - DeleteCommunityPageComponent, - SubComColSectionComponent, -]; - -@NgModule({ - imports: [ - CommonModule, - SharedModule, - CommunityPageRoutingModule, - StatisticsModule.forRoot(), - CommunityFormModule, - ComcolModule, - DsoPageModule, - BrowseByPageModule, - ], - declarations: [ - ...DECLARATIONS, - ], - exports: [ - ...DECLARATIONS, - ], -}) - -export class CommunityPageModule { - -} diff --git a/src/app/community-page/community-page.resolver.spec.ts b/src/app/community-page/community-page.resolver.spec.ts index 70bb0075e3..e429ecf17c 100644 --- a/src/app/community-page/community-page.resolver.spec.ts +++ b/src/app/community-page/community-page.resolver.spec.ts @@ -1,11 +1,12 @@ +import { Observable } from 'rxjs'; import { first } from 'rxjs/operators'; import { createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils'; -import { CommunityPageResolver } from './community-page.resolver'; +import { communityPageResolver } from './community-page.resolver'; -describe('CommunityPageResolver', () => { +describe('communityPageResolver', () => { describe('resolve', () => { - let resolver: CommunityPageResolver; + let resolver: any; let communityService: any; let store: any; const uuid = '1234-65487-12354-1235'; @@ -17,11 +18,11 @@ describe('CommunityPageResolver', () => { store = jasmine.createSpyObj('store', { dispatch: {}, }); - resolver = new CommunityPageResolver(communityService, store); + resolver = communityPageResolver; }); it('should resolve a community with the correct id', (done) => { - resolver.resolve({ params: { id: uuid } } as any, { url: 'current-url' } as any) + (resolver({ params: { id: uuid } } as any, { url: 'current-url' } as any, communityService, store) as Observable) .pipe(first()) .subscribe( (resolved) => { diff --git a/src/app/community-page/community-page.resolver.ts b/src/app/community-page/community-page.resolver.ts index bb90c4d457..b8820629e7 100644 --- a/src/app/community-page/community-page.resolver.ts +++ b/src/app/community-page/community-page.resolver.ts @@ -1,12 +1,13 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, - Resolve, + ResolveFn, RouterStateSnapshot, } from '@angular/router'; import { Store } from '@ngrx/store'; import { Observable } from 'rxjs'; +import { AppState } from '../app.reducer'; import { CommunityDataService } from '../core/data/community-data.service'; import { RemoteData } from '../core/data/remote-data'; import { ResolvedAction } from '../core/resolving/resolver.actions'; @@ -29,37 +30,32 @@ export const COMMUNITY_PAGE_LINKS_TO_FOLLOW: FollowLinkConfig[] = [ ]; /** - * This class represents a resolver that requests a specific community before the route is activated + * Method for resolving a community based on the parameters in the current route + * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot + * @param {RouterStateSnapshot} state The current RouterStateSnapshot + * @param {CommunityDataService} communityService + * @param {Store} store + * @returns Observable<> Emits the found community based on the parameters in the current route, + * or an error if something went wrong */ -@Injectable() -export class CommunityPageResolver implements Resolve> { - constructor( - private communityService: CommunityDataService, - private store: Store, - ) { - } +export const communityPageResolver: ResolveFn> = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + communityService: CommunityDataService = inject(CommunityDataService), + store: Store = inject(Store), +): Observable> => { + const communityRD$ = communityService.findById( + route.params.id, + true, + false, + ...COMMUNITY_PAGE_LINKS_TO_FOLLOW, + ).pipe( + getFirstCompletedRemoteData(), + ); - /** - * Method for resolving a community based on the parameters in the current route - * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot - * @param {RouterStateSnapshot} state The current RouterStateSnapshot - * @returns Observable<> Emits the found community based on the parameters in the current route, - * or an error if something went wrong - */ - resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable> { - const communityRD$ = this.communityService.findById( - route.params.id, - true, - false, - ...COMMUNITY_PAGE_LINKS_TO_FOLLOW, - ).pipe( - getFirstCompletedRemoteData(), - ); + communityRD$.subscribe((communityRD: RemoteData) => { + store.dispatch(new ResolvedAction(state.url, communityRD.payload)); + }); - communityRD$.subscribe((communityRD: RemoteData) => { - this.store.dispatch(new ResolvedAction(state.url, communityRD.payload)); - }); - - return communityRD$; - } -} + return communityRD$; +}; diff --git a/src/app/community-page/create-community-page/create-community-page.component.html b/src/app/community-page/create-community-page/create-community-page.component.html index 57039040c2..1d3d6eb871 100644 --- a/src/app/community-page/create-community-page/create-community-page.component.html +++ b/src/app/community-page/create-community-page/create-community-page.component.html @@ -1,13 +1,18 @@ -
+
- -

{{ 'community.create.sub-head' | translate:{ parent: dsoNameService.getName(parent) } }}

+

{{ 'community.create.head' | translate }}

+

{{ 'community.create.sub-head' | translate:{ parent: dsoNameService.getName(parent) } }}

+ [isCreation]="true" + (back)="navigateToHome()"> +
+ +
+
diff --git a/src/app/community-page/create-community-page/create-community-page.component.spec.ts b/src/app/community-page/create-community-page/create-community-page.component.spec.ts index 08da572a4d..062c0ea062 100644 --- a/src/app/community-page/create-community-page/create-community-page.component.spec.ts +++ b/src/app/community-page/create-community-page/create-community-page.component.spec.ts @@ -10,12 +10,14 @@ import { RouterTestingModule } from '@angular/router/testing'; import { TranslateModule } from '@ngx-translate/core'; import { of as observableOf } from 'rxjs'; +import { AuthService } from '../../core/auth/auth.service'; import { CommunityDataService } from '../../core/data/community-data.service'; import { RequestService } from '../../core/data/request.service'; import { RouteService } from '../../core/services/route.service'; +import { AuthServiceMock } from '../../shared/mocks/auth.service.mock'; import { NotificationsService } from '../../shared/notifications/notifications.service'; -import { SharedModule } from '../../shared/shared.module'; import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub'; +import { CommunityFormComponent } from '../community-form/community-form.component'; import { CreateCommunityPageComponent } from './create-community-page.component'; describe('CreateCommunityPageComponent', () => { @@ -24,17 +26,23 @@ describe('CreateCommunityPageComponent', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [TranslateModule.forRoot(), SharedModule, CommonModule, RouterTestingModule], - declarations: [CreateCommunityPageComponent], + imports: [TranslateModule.forRoot(), CommonModule, RouterTestingModule, CreateCommunityPageComponent], providers: [ { provide: CommunityDataService, useValue: { findById: () => observableOf({}) } }, { provide: RouteService, useValue: { getQueryParameterValue: () => observableOf('1234') } }, { provide: Router, useValue: {} }, { provide: NotificationsService, useValue: new NotificationsServiceStub() }, { provide: RequestService, useValue: {} }, + { provide: AuthService, useValue: new AuthServiceMock() }, ], schemas: [NO_ERRORS_SCHEMA], - }).compileComponents(); + }) + .overrideComponent(CreateCommunityPageComponent, { + remove: { + imports: [CommunityFormComponent], + }, + }) + .compileComponents(); })); beforeEach(() => { diff --git a/src/app/community-page/create-community-page/create-community-page.component.ts b/src/app/community-page/create-community-page/create-community-page.component.ts index 65ae0abc90..082f6c4f0b 100644 --- a/src/app/community-page/create-community-page/create-community-page.component.ts +++ b/src/app/community-page/create-community-page/create-community-page.component.ts @@ -1,6 +1,13 @@ +import { + AsyncPipe, + NgIf, +} from '@angular/common'; import { Component } from '@angular/core'; import { Router } from '@angular/router'; -import { TranslateService } from '@ngx-translate/core'; +import { + TranslateModule, + TranslateService, +} from '@ngx-translate/core'; import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; import { CommunityDataService } from '../../core/data/community-data.service'; @@ -8,7 +15,10 @@ import { RequestService } from '../../core/data/request.service'; import { RouteService } from '../../core/services/route.service'; import { Community } from '../../core/shared/community.model'; import { CreateComColPageComponent } from '../../shared/comcol/comcol-forms/create-comcol-page/create-comcol-page.component'; +import { ThemedLoadingComponent } from '../../shared/loading/themed-loading.component'; import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { VarDirective } from '../../shared/utils/var.directive'; +import { CommunityFormComponent } from '../community-form/community-form.component'; /** * Component that represents the page where a user can create a new Community @@ -17,6 +27,15 @@ import { NotificationsService } from '../../shared/notifications/notifications.s selector: 'ds-create-community', styleUrls: ['./create-community-page.component.scss'], templateUrl: './create-community-page.component.html', + imports: [ + CommunityFormComponent, + TranslateModule, + VarDirective, + NgIf, + AsyncPipe, + ThemedLoadingComponent, + ], + standalone: true, }) export class CreateCommunityPageComponent extends CreateComColPageComponent { protected frontendURL = '/communities/'; diff --git a/src/app/community-page/create-community-page/create-community-page.guard.spec.ts b/src/app/community-page/create-community-page/create-community-page.guard.spec.ts index 8f1f3fb18a..363db42fa2 100644 --- a/src/app/community-page/create-community-page/create-community-page.guard.spec.ts +++ b/src/app/community-page/create-community-page/create-community-page.guard.spec.ts @@ -6,11 +6,11 @@ import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject$, } from '../../shared/remote-data.utils'; -import { CreateCommunityPageGuard } from './create-community-page.guard'; +import { createCommunityPageGuard } from './create-community-page.guard'; -describe('CreateCommunityPageGuard', () => { +describe('createCommunityPageGuard', () => { describe('canActivate', () => { - let guard: CreateCommunityPageGuard; + let guard: any; let router; let communityDataServiceStub: any; @@ -28,11 +28,11 @@ describe('CreateCommunityPageGuard', () => { }; router = new RouterMock(); - guard = new CreateCommunityPageGuard(router, communityDataServiceStub); + guard = createCommunityPageGuard; }); it('should return true when the parent ID resolves to a community', () => { - guard.canActivate({ queryParams: { parent: 'valid-id' } } as any, undefined) + guard({ queryParams: { parent: 'valid-id' } } as any, undefined, communityDataServiceStub, router) .pipe(first()) .subscribe( (canActivate) => @@ -41,7 +41,7 @@ describe('CreateCommunityPageGuard', () => { }); it('should return true when no parent ID has been provided', () => { - guard.canActivate({ queryParams: { } } as any, undefined) + guard({ queryParams: { } } as any, undefined, communityDataServiceStub, router) .pipe(first()) .subscribe( (canActivate) => @@ -50,7 +50,7 @@ describe('CreateCommunityPageGuard', () => { }); it('should return false when the parent ID does not resolve to a community', () => { - guard.canActivate({ queryParams: { parent: 'invalid-id' } } as any, undefined) + guard({ queryParams: { parent: 'invalid-id' } } as any, undefined, communityDataServiceStub, router) .pipe(first()) .subscribe( (canActivate) => @@ -59,7 +59,7 @@ describe('CreateCommunityPageGuard', () => { }); it('should return false when the parent ID resolves to an error response', () => { - guard.canActivate({ queryParams: { parent: 'error-id' } } as any, undefined) + guard({ queryParams: { parent: 'error-id' } } as any, undefined, communityDataServiceStub, router) .pipe(first()) .subscribe( (canActivate) => diff --git a/src/app/community-page/create-community-page/create-community-page.guard.ts b/src/app/community-page/create-community-page/create-community-page.guard.ts index 847273b367..c3ee8c7091 100644 --- a/src/app/community-page/create-community-page/create-community-page.guard.ts +++ b/src/app/community-page/create-community-page/create-community-page.guard.ts @@ -1,7 +1,7 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, - CanActivate, + CanActivateFn, Router, RouterStateSnapshot, } from '@angular/router'; @@ -24,35 +24,29 @@ import { } from '../../shared/empty.util'; /** - * Prevent creation of a community with an invalid parent community provided - * @class CreateCommunityPageGuard + * True when either NO parent ID query parameter has been provided, or the parent ID resolves to a valid parent community + * Reroutes to a 404 page when the page cannot be activated */ -@Injectable() -export class CreateCommunityPageGuard implements CanActivate { - public constructor(private router: Router, private communityService: CommunityDataService) { +export const createCommunityPageGuard: CanActivateFn = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + communityService: CommunityDataService = inject(CommunityDataService), + router: Router = inject(Router), +): Observable => { + const parentID = route.queryParams.parent; + if (hasNoValue(parentID)) { + return observableOf(true); } - /** - * True when either NO parent ID query parameter has been provided, or the parent ID resolves to a valid parent community - * Reroutes to a 404 page when the page cannot be activated - * @method canActivate - */ - canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { - const parentID = route.queryParams.parent; - if (hasNoValue(parentID)) { - return observableOf(true); - } - - return this.communityService.findById(parentID) - .pipe( - getFirstCompletedRemoteData(), - map((communityRD: RemoteData) => hasValue(communityRD) && communityRD.hasSucceeded && hasValue(communityRD.payload)), - tap((isValid: boolean) => { - if (!isValid) { - this.router.navigate(['/404']); - } - }, - ), - ); - } -} + return communityService.findById(parentID) + .pipe( + getFirstCompletedRemoteData(), + map((communityRD: RemoteData) => hasValue(communityRD) && communityRD.hasSucceeded && hasValue(communityRD.payload)), + tap((isValid: boolean) => { + if (!isValid) { + router.navigate(['/404']); + } + }, + ), + ); +}; diff --git a/src/app/community-page/delete-community-page/delete-community-page.component.spec.ts b/src/app/community-page/delete-community-page/delete-community-page.component.spec.ts index 0892c4d66c..524f3e3124 100644 --- a/src/app/community-page/delete-community-page/delete-community-page.component.spec.ts +++ b/src/app/community-page/delete-community-page/delete-community-page.component.spec.ts @@ -15,7 +15,6 @@ import { CommunityDataService } from '../../core/data/community-data.service'; import { RequestService } from '../../core/data/request.service'; import { DSONameServiceMock } from '../../shared/mocks/dso-name.service.mock'; import { NotificationsService } from '../../shared/notifications/notifications.service'; -import { SharedModule } from '../../shared/shared.module'; import { DeleteCommunityPageComponent } from './delete-community-page.component'; describe('DeleteCommunityPageComponent', () => { @@ -24,8 +23,7 @@ describe('DeleteCommunityPageComponent', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [TranslateModule.forRoot(), SharedModule, CommonModule, RouterTestingModule], - declarations: [DeleteCommunityPageComponent], + imports: [TranslateModule.forRoot(), CommonModule, RouterTestingModule, DeleteCommunityPageComponent], providers: [ { provide: DSONameService, useValue: new DSONameServiceMock() }, { provide: CommunityDataService, useValue: {} }, diff --git a/src/app/community-page/delete-community-page/delete-community-page.component.ts b/src/app/community-page/delete-community-page/delete-community-page.component.ts index c6888a893b..f35e2d6bd2 100644 --- a/src/app/community-page/delete-community-page/delete-community-page.component.ts +++ b/src/app/community-page/delete-community-page/delete-community-page.component.ts @@ -1,15 +1,23 @@ +import { + AsyncPipe, + NgIf, +} from '@angular/common'; import { Component } from '@angular/core'; import { ActivatedRoute, Router, } from '@angular/router'; -import { TranslateService } from '@ngx-translate/core'; +import { + TranslateModule, + TranslateService, +} from '@ngx-translate/core'; import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; import { CommunityDataService } from '../../core/data/community-data.service'; import { Community } from '../../core/shared/community.model'; import { DeleteComColPageComponent } from '../../shared/comcol/comcol-forms/delete-comcol-page/delete-comcol-page.component'; import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { VarDirective } from '../../shared/utils/var.directive'; /** * Component that represents the page where a user can delete an existing Community @@ -18,6 +26,13 @@ import { NotificationsService } from '../../shared/notifications/notifications.s selector: 'ds-delete-community', styleUrls: ['./delete-community-page.component.scss'], templateUrl: './delete-community-page.component.html', + imports: [ + TranslateModule, + AsyncPipe, + VarDirective, + NgIf, + ], + standalone: true, }) export class DeleteCommunityPageComponent extends DeleteComColPageComponent { protected frontendURL = '/communities/'; diff --git a/src/app/community-page/edit-community-page/community-access-control/community-access-control.component.spec.ts b/src/app/community-page/edit-community-page/community-access-control/community-access-control.component.spec.ts index aa01183519..28879ed7ab 100644 --- a/src/app/community-page/edit-community-page/community-access-control/community-access-control.component.spec.ts +++ b/src/app/community-page/edit-community-page/community-access-control/community-access-control.component.spec.ts @@ -9,12 +9,14 @@ import { } from 'rxjs'; import { Community } from '../../../core/shared/community.model'; +import { AccessControlFormContainerComponent } from '../../../shared/access-control-form-container/access-control-form-container.component'; import { createSuccessfulRemoteDataObject } from '../../../shared/remote-data.utils'; import { CommunityAccessControlComponent } from './community-access-control.component'; describe('CommunityAccessControlComponent', () => { let component: CommunityAccessControlComponent; let fixture: ComponentFixture; + const testCommunity = Object.assign(new Community(), { type: 'community', @@ -44,9 +46,16 @@ describe('CommunityAccessControlComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [ CommunityAccessControlComponent ], - providers: [{ provide: ActivatedRoute, useValue: routeStub }], + imports: [CommunityAccessControlComponent], + providers: [{ + provide: ActivatedRoute, useValue: routeStub, + }], }) + .overrideComponent(CommunityAccessControlComponent, { + remove: { + imports: [AccessControlFormContainerComponent], + }, + }) .compileComponents(); }); diff --git a/src/app/community-page/edit-community-page/community-access-control/community-access-control.component.ts b/src/app/community-page/edit-community-page/community-access-control/community-access-control.component.ts index e70af7062e..a0e094e21d 100644 --- a/src/app/community-page/edit-community-page/community-access-control/community-access-control.component.ts +++ b/src/app/community-page/edit-community-page/community-access-control/community-access-control.component.ts @@ -1,3 +1,7 @@ +import { + AsyncPipe, + NgIf, +} from '@angular/common'; import { Component, OnInit, @@ -9,11 +13,18 @@ import { map } from 'rxjs/operators'; import { RemoteData } from '../../../core/data/remote-data'; import { Community } from '../../../core/shared/community.model'; import { getFirstSucceededRemoteData } from '../../../core/shared/operators'; +import { AccessControlFormContainerComponent } from '../../../shared/access-control-form-container/access-control-form-container.component'; @Component({ selector: 'ds-community-access-control', templateUrl: './community-access-control.component.html', styleUrls: ['./community-access-control.component.scss'], + imports: [ + AccessControlFormContainerComponent, + NgIf, + AsyncPipe, + ], + standalone: true, }) export class CommunityAccessControlComponent implements OnInit { itemRD$: Observable>; diff --git a/src/app/community-page/edit-community-page/community-authorizations/community-authorizations.component.spec.ts b/src/app/community-page/edit-community-page/community-authorizations/community-authorizations.component.spec.ts index c02b390cb2..921bbf0cfd 100644 --- a/src/app/community-page/edit-community-page/community-authorizations/community-authorizations.component.spec.ts +++ b/src/app/community-page/edit-community-page/community-authorizations/community-authorizations.component.spec.ts @@ -15,6 +15,7 @@ import { of as observableOf } from 'rxjs'; import { Collection } from '../../../core/shared/collection.model'; import { DSpaceObject } from '../../../core/shared/dspace-object.model'; import { createSuccessfulRemoteDataObject } from '../../../shared/remote-data.utils'; +import { ResourcePoliciesComponent } from '../../../shared/resource-policies/resource-policies.component'; import { CommunityAuthorizationsComponent } from './community-authorizations.component'; describe('CommunityAuthorizationsComponent', () => { @@ -45,15 +46,21 @@ describe('CommunityAuthorizationsComponent', () => { TestBed.configureTestingModule({ imports: [ CommonModule, + CommunityAuthorizationsComponent, ], - declarations: [CommunityAuthorizationsComponent], providers: [ { provide: ActivatedRoute, useValue: routeStub }, ChangeDetectorRef, CommunityAuthorizationsComponent, ], schemas: [NO_ERRORS_SCHEMA], - }).compileComponents(); + }) + .overrideComponent(CommunityAuthorizationsComponent, { + remove: { + imports: [ResourcePoliciesComponent], + }, + }) + .compileComponents(); })); beforeEach(() => { diff --git a/src/app/community-page/edit-community-page/community-authorizations/community-authorizations.component.ts b/src/app/community-page/edit-community-page/community-authorizations/community-authorizations.component.ts index c31f798060..3e42a830be 100644 --- a/src/app/community-page/edit-community-page/community-authorizations/community-authorizations.component.ts +++ b/src/app/community-page/edit-community-page/community-authorizations/community-authorizations.component.ts @@ -1,3 +1,4 @@ +import { AsyncPipe } from '@angular/common'; import { Component, OnInit, @@ -11,10 +12,16 @@ import { import { RemoteData } from '../../../core/data/remote-data'; import { DSpaceObject } from '../../../core/shared/dspace-object.model'; +import { ResourcePoliciesComponent } from '../../../shared/resource-policies/resource-policies.component'; @Component({ selector: 'ds-community-authorizations', templateUrl: './community-authorizations.component.html', + imports: [ + ResourcePoliciesComponent, + AsyncPipe, + ], + standalone: true, }) /** * Component that handles the community Authorizations diff --git a/src/app/community-page/edit-community-page/community-curate/community-curate.component.spec.ts b/src/app/community-page/edit-community-page/community-curate/community-curate.component.spec.ts index 8928543a3a..541308c942 100644 --- a/src/app/community-page/edit-community-page/community-curate/community-curate.component.spec.ts +++ b/src/app/community-page/edit-community-page/community-curate/community-curate.component.spec.ts @@ -13,6 +13,7 @@ import { of as observableOf } from 'rxjs'; import { DSONameService } from '../../../core/breadcrumbs/dso-name.service'; import { Community } from '../../../core/shared/community.model'; +import { CurationFormComponent } from '../../../curation-form/curation-form.component'; import { createSuccessfulRemoteDataObject } from '../../../shared/remote-data.utils'; import { CommunityCurateComponent } from './community-curate.component'; @@ -42,14 +43,19 @@ describe('CommunityCurateComponent', () => { }); TestBed.configureTestingModule({ - imports: [TranslateModule.forRoot()], - declarations: [CommunityCurateComponent], + imports: [TranslateModule.forRoot(), CommunityCurateComponent], providers: [ { provide: ActivatedRoute, useValue: routeStub }, { provide: DSONameService, useValue: dsoNameService }, ], schemas: [CUSTOM_ELEMENTS_SCHEMA], - }).compileComponents(); + }) + .overrideComponent(CommunityCurateComponent, { + remove: { + imports: [CurationFormComponent], + }, + }) + .compileComponents(); })); beforeEach(() => { diff --git a/src/app/community-page/edit-community-page/community-curate/community-curate.component.ts b/src/app/community-page/edit-community-page/community-curate/community-curate.component.ts index aa28644ae7..fd4d240827 100644 --- a/src/app/community-page/edit-community-page/community-curate/community-curate.component.ts +++ b/src/app/community-page/edit-community-page/community-curate/community-curate.component.ts @@ -1,8 +1,10 @@ +import { AsyncPipe } from '@angular/common'; import { Component, OnInit, } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; +import { TranslateModule } from '@ngx-translate/core'; import { Observable } from 'rxjs'; import { filter, @@ -13,6 +15,7 @@ import { import { DSONameService } from '../../../core/breadcrumbs/dso-name.service'; import { RemoteData } from '../../../core/data/remote-data'; import { Community } from '../../../core/shared/community.model'; +import { CurationFormComponent } from '../../../curation-form/curation-form.component'; import { hasValue } from '../../../shared/empty.util'; /** @@ -21,6 +24,12 @@ import { hasValue } from '../../../shared/empty.util'; @Component({ selector: 'ds-community-curate', templateUrl: './community-curate.component.html', + imports: [ + CurationFormComponent, + TranslateModule, + AsyncPipe, + ], + standalone: true, }) export class CommunityCurateComponent implements OnInit { diff --git a/src/app/community-page/edit-community-page/community-metadata/community-metadata.component.html b/src/app/community-page/edit-community-page/community-metadata/community-metadata.component.html index 2ca5b768e4..bf75944242 100644 --- a/src/app/community-page/edit-community-page/community-metadata/community-metadata.component.html +++ b/src/app/community-page/edit-community-page/community-metadata/community-metadata.component.html @@ -1,5 +1,5 @@ - diff --git a/src/app/community-page/edit-community-page/community-metadata/community-metadata.component.spec.ts b/src/app/community-page/edit-community-page/community-metadata/community-metadata.component.spec.ts index ec4a502a6e..b82beaa3f7 100644 --- a/src/app/community-page/edit-community-page/community-metadata/community-metadata.component.spec.ts +++ b/src/app/community-page/edit-community-page/community-metadata/community-metadata.component.spec.ts @@ -12,8 +12,8 @@ import { of as observableOf } from 'rxjs'; import { CommunityDataService } from '../../../core/data/community-data.service'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; -import { SharedModule } from '../../../shared/shared.module'; import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub'; +import { CommunityFormComponent } from '../../community-form/community-form.component'; import { CommunityMetadataComponent } from './community-metadata.component'; describe('CommunityMetadataComponent', () => { @@ -22,15 +22,20 @@ describe('CommunityMetadataComponent', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [TranslateModule.forRoot(), SharedModule, CommonModule, RouterTestingModule], - declarations: [CommunityMetadataComponent], + imports: [TranslateModule.forRoot(), CommonModule, RouterTestingModule, CommunityMetadataComponent], providers: [ { provide: CommunityDataService, useValue: {} }, { provide: ActivatedRoute, useValue: { parent: { data: observableOf({ dso: { payload: {} } }) } } }, { provide: NotificationsService, useValue: new NotificationsServiceStub() }, ], schemas: [NO_ERRORS_SCHEMA], - }).compileComponents(); + }) + .overrideComponent(CommunityMetadataComponent, { + remove: { + imports: [CommunityFormComponent], + }, + }) + .compileComponents(); })); beforeEach(() => { diff --git a/src/app/community-page/edit-community-page/community-metadata/community-metadata.component.ts b/src/app/community-page/edit-community-page/community-metadata/community-metadata.component.ts index 9a6b93a0ce..8001bd2969 100644 --- a/src/app/community-page/edit-community-page/community-metadata/community-metadata.component.ts +++ b/src/app/community-page/edit-community-page/community-metadata/community-metadata.component.ts @@ -1,3 +1,4 @@ +import { AsyncPipe } from '@angular/common'; import { Component } from '@angular/core'; import { ActivatedRoute, @@ -9,6 +10,7 @@ import { CommunityDataService } from '../../../core/data/community-data.service' import { Community } from '../../../core/shared/community.model'; import { ComcolMetadataComponent } from '../../../shared/comcol/comcol-forms/edit-comcol-page/comcol-metadata/comcol-metadata.component'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; +import { CommunityFormComponent } from '../../community-form/community-form.component'; /** * Component for editing a community's metadata @@ -16,6 +18,11 @@ import { NotificationsService } from '../../../shared/notifications/notification @Component({ selector: 'ds-community-metadata', templateUrl: './community-metadata.component.html', + imports: [ + CommunityFormComponent, + AsyncPipe, + ], + standalone: true, }) export class CommunityMetadataComponent extends ComcolMetadataComponent { protected frontendURL = '/communities/'; diff --git a/src/app/community-page/edit-community-page/community-roles/community-roles.component.spec.ts b/src/app/community-page/edit-community-page/community-roles/community-roles.component.spec.ts index 895c61862a..f1e75b7e23 100644 --- a/src/app/community-page/edit-community-page/community-roles/community-roles.component.spec.ts +++ b/src/app/community-page/edit-community-page/community-roles/community-roles.component.spec.ts @@ -17,14 +17,12 @@ import { DSONameService } from '../../../core/breadcrumbs/dso-name.service'; import { RequestService } from '../../../core/data/request.service'; import { GroupDataService } from '../../../core/eperson/group-data.service'; import { Community } from '../../../core/shared/community.model'; -import { ComcolModule } from '../../../shared/comcol/comcol.module'; import { DSONameServiceMock } from '../../../shared/mocks/dso-name.service.mock'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$, } from '../../../shared/remote-data.utils'; -import { SharedModule } from '../../../shared/shared.module'; import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub'; import { CommunityRolesComponent } from './community-roles.component'; @@ -65,13 +63,9 @@ describe('CommunityRolesComponent', () => { TestBed.configureTestingModule({ imports: [ - ComcolModule, - SharedModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NoopAnimationsModule, - ], - declarations: [ CommunityRolesComponent, ], providers: [ diff --git a/src/app/community-page/edit-community-page/community-roles/community-roles.component.ts b/src/app/community-page/edit-community-page/community-roles/community-roles.component.ts index 5f6c57defc..2e85cbe4c3 100644 --- a/src/app/community-page/edit-community-page/community-roles/community-roles.component.ts +++ b/src/app/community-page/edit-community-page/community-roles/community-roles.component.ts @@ -1,3 +1,7 @@ +import { + AsyncPipe, + NgForOf, +} from '@angular/common'; import { Component, OnInit, @@ -16,6 +20,7 @@ import { getFirstSucceededRemoteData, getRemoteDataPayload, } from '../../../core/shared/operators'; +import { ComcolRoleComponent } from '../../../shared/comcol/comcol-forms/edit-comcol-page/comcol-role/comcol-role.component'; /** * Component for managing a community's roles @@ -23,6 +28,12 @@ import { @Component({ selector: 'ds-community-roles', templateUrl: './community-roles.component.html', + imports: [ + ComcolRoleComponent, + AsyncPipe, + NgForOf, + ], + standalone: true, }) export class CommunityRolesComponent implements OnInit { diff --git a/src/app/community-page/edit-community-page/edit-community-page-routes.ts b/src/app/community-page/edit-community-page/edit-community-page-routes.ts new file mode 100644 index 0000000000..2402c2037d --- /dev/null +++ b/src/app/community-page/edit-community-page/edit-community-page-routes.ts @@ -0,0 +1,88 @@ +import { Route } from '@angular/router'; + +import { i18nBreadcrumbResolver } from '../../core/breadcrumbs/i18n-breadcrumb.resolver'; +import { communityAdministratorGuard } from '../../core/data/feature-authorization/feature-authorization-guard/community-administrator.guard'; +import { ResourcePolicyCreateComponent } from '../../shared/resource-policies/create/resource-policy-create.component'; +import { ResourcePolicyEditComponent } from '../../shared/resource-policies/edit/resource-policy-edit.component'; +import { resourcePolicyResolver } from '../../shared/resource-policies/resolvers/resource-policy.resolver'; +import { resourcePolicyTargetResolver } from '../../shared/resource-policies/resolvers/resource-policy-target.resolver'; +import { CommunityAccessControlComponent } from './community-access-control/community-access-control.component'; +import { CommunityAuthorizationsComponent } from './community-authorizations/community-authorizations.component'; +import { CommunityCurateComponent } from './community-curate/community-curate.component'; +import { CommunityMetadataComponent } from './community-metadata/community-metadata.component'; +import { CommunityRolesComponent } from './community-roles/community-roles.component'; +import { EditCommunityPageComponent } from './edit-community-page.component'; + +/** + * Routing module that handles the routing for the Edit Community page administrator functionality + */ + +export const ROUTES: Route[] = [ + { + path: '', + resolve: { + breadcrumb: i18nBreadcrumbResolver, + }, + data: { breadcrumbKey: 'community.edit' }, + component: EditCommunityPageComponent, + canActivate: [communityAdministratorGuard], + children: [ + { + path: '', + redirectTo: 'metadata', + pathMatch: 'full', + }, + { + path: 'metadata', + component: CommunityMetadataComponent, + data: { + title: 'community.edit.tabs.metadata.title', + hideReturnButton: true, + showBreadcrumbs: true, + }, + }, + { + path: 'roles', + component: CommunityRolesComponent, + data: { title: 'community.edit.tabs.roles.title', showBreadcrumbs: true }, + }, + { + path: 'curate', + component: CommunityCurateComponent, + data: { title: 'community.edit.tabs.curate.title', showBreadcrumbs: true }, + }, + { + path: 'access-control', + component: CommunityAccessControlComponent, + data: { title: 'collection.edit.tabs.access-control.title', showBreadcrumbs: true }, + }, + { + path: 'authorizations', + data: { showBreadcrumbs: true }, + children: [ + { + path: 'create', + resolve: { + resourcePolicyTarget: resourcePolicyTargetResolver, + }, + component: ResourcePolicyCreateComponent, + data: { title: 'resource-policies.create.page.title' }, + }, + { + path: 'edit', + resolve: { + resourcePolicy: resourcePolicyResolver, + }, + component: ResourcePolicyEditComponent, + data: { title: 'resource-policies.edit.page.title' }, + }, + { + path: '', + component: CommunityAuthorizationsComponent, + data: { title: 'community.edit.tabs.authorizations.title', showBreadcrumbs: true, hideReturnButton: true }, + }, + ], + }, + ], + }, +]; diff --git a/src/app/community-page/edit-community-page/edit-community-page.component.spec.ts b/src/app/community-page/edit-community-page/edit-community-page.component.spec.ts index 824aef3ba4..f099f6fc2a 100644 --- a/src/app/community-page/edit-community-page/edit-community-page.component.spec.ts +++ b/src/app/community-page/edit-community-page/edit-community-page.component.spec.ts @@ -11,7 +11,6 @@ import { TranslateModule } from '@ngx-translate/core'; import { of as observableOf } from 'rxjs'; import { CommunityDataService } from '../../core/data/community-data.service'; -import { SharedModule } from '../../shared/shared.module'; import { EditCommunityPageComponent } from './edit-community-page.component'; describe('EditCommunityPageComponent', () => { @@ -43,8 +42,7 @@ describe('EditCommunityPageComponent', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [TranslateModule.forRoot(), SharedModule, CommonModule, RouterTestingModule], - declarations: [EditCommunityPageComponent], + imports: [TranslateModule.forRoot(), CommonModule, RouterTestingModule, EditCommunityPageComponent], providers: [ { provide: CommunityDataService, useValue: {} }, { provide: ActivatedRoute, useValue: routeStub }, diff --git a/src/app/community-page/edit-community-page/edit-community-page.component.ts b/src/app/community-page/edit-community-page/edit-community-page.component.ts index 5bd31e9ba0..194976c3a8 100644 --- a/src/app/community-page/edit-community-page/edit-community-page.component.ts +++ b/src/app/community-page/edit-community-page/edit-community-page.component.ts @@ -1,8 +1,17 @@ +import { + AsyncPipe, + NgClass, + NgForOf, + NgIf, +} from '@angular/common'; import { Component } from '@angular/core'; import { ActivatedRoute, Router, + RouterLink, + RouterOutlet, } from '@angular/router'; +import { TranslateModule } from '@ngx-translate/core'; import { Community } from '../../core/shared/community.model'; import { EditComColPageComponent } from '../../shared/comcol/comcol-forms/edit-comcol-page/edit-comcol-page.component'; @@ -14,6 +23,16 @@ import { getCommunityPageRoute } from '../community-page-routing-paths'; @Component({ selector: 'ds-edit-community', templateUrl: '../../shared/comcol/comcol-forms/edit-comcol-page/edit-comcol-page.component.html', + standalone: true, + imports: [ + RouterLink, + TranslateModule, + NgClass, + NgForOf, + RouterOutlet, + NgIf, + AsyncPipe, + ], }) export class EditCommunityPageComponent extends EditComColPageComponent { type = 'community'; diff --git a/src/app/community-page/edit-community-page/edit-community-page.module.ts b/src/app/community-page/edit-community-page/edit-community-page.module.ts deleted file mode 100644 index 13555e3800..0000000000 --- a/src/app/community-page/edit-community-page/edit-community-page.module.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { CommonModule } from '@angular/common'; -import { NgModule } from '@angular/core'; - -import { AccessControlFormModule } from '../../shared/access-control-form-container/access-control-form.module'; -import { ComcolModule } from '../../shared/comcol/comcol.module'; -import { ResourcePoliciesModule } from '../../shared/resource-policies/resource-policies.module'; -import { SharedModule } from '../../shared/shared.module'; -import { CommunityFormModule } from '../community-form/community-form.module'; -import { CommunityAccessControlComponent } from './community-access-control/community-access-control.component'; -import { CommunityAuthorizationsComponent } from './community-authorizations/community-authorizations.component'; -import { CommunityCurateComponent } from './community-curate/community-curate.component'; -import { CommunityMetadataComponent } from './community-metadata/community-metadata.component'; -import { CommunityRolesComponent } from './community-roles/community-roles.component'; -import { EditCommunityPageComponent } from './edit-community-page.component'; -import { EditCommunityPageRoutingModule } from './edit-community-page.routing.module'; - -/** - * Module that contains all components related to the Edit Community page administrator functionality - */ -@NgModule({ - imports: [ - CommonModule, - SharedModule, - EditCommunityPageRoutingModule, - CommunityFormModule, - ComcolModule, - ResourcePoliciesModule, - AccessControlFormModule, - ], - declarations: [ - EditCommunityPageComponent, - CommunityCurateComponent, - CommunityMetadataComponent, - CommunityRolesComponent, - CommunityAuthorizationsComponent, - CommunityAccessControlComponent, - ], -}) -export class EditCommunityPageModule { - -} diff --git a/src/app/community-page/edit-community-page/edit-community-page.routing.module.ts b/src/app/community-page/edit-community-page/edit-community-page.routing.module.ts deleted file mode 100644 index 4b0fa5d70b..0000000000 --- a/src/app/community-page/edit-community-page/edit-community-page.routing.module.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { NgModule } from '@angular/core'; -import { RouterModule } from '@angular/router'; - -import { I18nBreadcrumbResolver } from '../../core/breadcrumbs/i18n-breadcrumb.resolver'; -import { CommunityAdministratorGuard } from '../../core/data/feature-authorization/feature-authorization-guard/community-administrator.guard'; -import { ResourcePolicyCreateComponent } from '../../shared/resource-policies/create/resource-policy-create.component'; -import { ResourcePolicyEditComponent } from '../../shared/resource-policies/edit/resource-policy-edit.component'; -import { ResourcePolicyResolver } from '../../shared/resource-policies/resolvers/resource-policy.resolver'; -import { ResourcePolicyTargetResolver } from '../../shared/resource-policies/resolvers/resource-policy-target.resolver'; -import { CommunityAccessControlComponent } from './community-access-control/community-access-control.component'; -import { CommunityAuthorizationsComponent } from './community-authorizations/community-authorizations.component'; -import { CommunityCurateComponent } from './community-curate/community-curate.component'; -import { CommunityMetadataComponent } from './community-metadata/community-metadata.component'; -import { CommunityRolesComponent } from './community-roles/community-roles.component'; -import { EditCommunityPageComponent } from './edit-community-page.component'; - -/** - * Routing module that handles the routing for the Edit Community page administrator functionality - */ -@NgModule({ - imports: [ - RouterModule.forChild([ - { - path: '', - resolve: { - breadcrumb: I18nBreadcrumbResolver, - }, - data: { breadcrumbKey: 'community.edit' }, - component: EditCommunityPageComponent, - canActivate: [CommunityAdministratorGuard], - children: [ - { - path: '', - redirectTo: 'metadata', - pathMatch: 'full', - }, - { - path: 'metadata', - component: CommunityMetadataComponent, - data: { - title: 'community.edit.tabs.metadata.title', - hideReturnButton: true, - showBreadcrumbs: true, - }, - }, - { - path: 'roles', - component: CommunityRolesComponent, - data: { title: 'community.edit.tabs.roles.title', showBreadcrumbs: true }, - }, - { - path: 'curate', - component: CommunityCurateComponent, - data: { title: 'community.edit.tabs.curate.title', showBreadcrumbs: true }, - }, - { - path: 'access-control', - component: CommunityAccessControlComponent, - data: { title: 'collection.edit.tabs.access-control.title', showBreadcrumbs: true }, - }, - /*{ - path: 'authorizations', - component: CommunityAuthorizationsComponent, - data: { title: 'community.edit.tabs.authorizations.title', showBreadcrumbs: true } - },*/ - { - path: 'authorizations', - data: { showBreadcrumbs: true }, - children: [ - { - path: 'create', - resolve: { - resourcePolicyTarget: ResourcePolicyTargetResolver, - }, - component: ResourcePolicyCreateComponent, - data: { title: 'resource-policies.create.page.title' }, - }, - { - path: 'edit', - resolve: { - resourcePolicy: ResourcePolicyResolver, - }, - component: ResourcePolicyEditComponent, - data: { title: 'resource-policies.edit.page.title' }, - }, - { - path: '', - component: CommunityAuthorizationsComponent, - data: { title: 'community.edit.tabs.authorizations.title', showBreadcrumbs: true, hideReturnButton: true }, - }, - ], - }, - ], - }, - ]), - ], - providers: [ - ResourcePolicyResolver, - ResourcePolicyTargetResolver, - ], -}) -export class EditCommunityPageRoutingModule { - -} diff --git a/src/app/community-page/sections/sub-com-col-section/sub-collection-list/community-page-sub-collection-list.component.html b/src/app/community-page/sections/sub-com-col-section/sub-collection-list/community-page-sub-collection-list.component.html index b5fbf1a01d..59d7b3bb5e 100644 --- a/src/app/community-page/sections/sub-com-col-section/sub-collection-list/community-page-sub-collection-list.component.html +++ b/src/app/community-page/sections/sub-com-col-section/sub-collection-list/community-page-sub-collection-list.component.html @@ -9,5 +9,5 @@
- + diff --git a/src/app/community-page/sections/sub-com-col-section/sub-collection-list/community-page-sub-collection-list.component.spec.ts b/src/app/community-page/sections/sub-com-col-section/sub-collection-list/community-page-sub-collection-list.component.spec.ts index 2a6ec9510c..dc4ab52081 100644 --- a/src/app/community-page/sections/sub-com-col-section/sub-collection-list/community-page-sub-collection-list.component.spec.ts +++ b/src/app/community-page/sections/sub-com-col-section/sub-collection-list/community-page-sub-collection-list.component.spec.ts @@ -28,7 +28,6 @@ import { HostWindowService } from '../../../../shared/host-window.service'; import { getMockThemeService } from '../../../../shared/mocks/theme-service.mock'; import { SelectableListService } from '../../../../shared/object-list/selectable-list/selectable-list.service'; import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils'; -import { SharedModule } from '../../../../shared/shared.module'; import { HostWindowServiceStub } from '../../../../shared/testing/host-window-service.stub'; import { PaginationServiceStub } from '../../../../shared/testing/pagination-service.stub'; import { SearchConfigurationServiceStub } from '../../../../shared/testing/search-configuration-service.stub'; @@ -155,12 +154,11 @@ describe('CommunityPageSubCollectionListComponent', () => { TestBed.configureTestingModule({ imports: [ TranslateModule.forRoot(), - SharedModule, RouterTestingModule.withRoutes([]), NgbModule, NoopAnimationsModule, + CommunityPageSubCollectionListComponent, ], - declarations: [CommunityPageSubCollectionListComponent], providers: [ { provide: CollectionDataService, useValue: collectionDataServiceStub }, { provide: HostWindowService, useValue: new HostWindowServiceStub(0) }, diff --git a/src/app/community-page/sections/sub-com-col-section/sub-collection-list/community-page-sub-collection-list.component.ts b/src/app/community-page/sections/sub-com-col-section/sub-collection-list/community-page-sub-collection-list.component.ts index f526f76641..1e8ff1d46c 100644 --- a/src/app/community-page/sections/sub-com-col-section/sub-collection-list/community-page-sub-collection-list.component.ts +++ b/src/app/community-page/sections/sub-com-col-section/sub-collection-list/community-page-sub-collection-list.component.ts @@ -1,3 +1,7 @@ +import { + AsyncPipe, + NgIf, +} from '@angular/common'; import { Component, Input, @@ -5,6 +9,7 @@ import { OnInit, } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; +import { TranslateModule } from '@ngx-translate/core'; import { BehaviorSubject, combineLatest as observableCombineLatest, @@ -24,13 +29,27 @@ import { Collection } from '../../../../core/shared/collection.model'; import { Community } from '../../../../core/shared/community.model'; import { fadeIn } from '../../../../shared/animations/fade'; import { hasValue } from '../../../../shared/empty.util'; +import { ErrorComponent } from '../../../../shared/error/error.component'; +import { ThemedLoadingComponent } from '../../../../shared/loading/themed-loading.component'; +import { ObjectCollectionComponent } from '../../../../shared/object-collection/object-collection.component'; import { PaginationComponentOptions } from '../../../../shared/pagination/pagination-component-options.model'; +import { VarDirective } from '../../../../shared/utils/var.directive'; @Component({ - selector: 'ds-community-page-sub-collection-list', + selector: 'ds-base-community-page-sub-collection-list', styleUrls: ['./community-page-sub-collection-list.component.scss'], templateUrl: './community-page-sub-collection-list.component.html', - animations:[fadeIn], + animations: [fadeIn], + imports: [ + ObjectCollectionComponent, + ErrorComponent, + ThemedLoadingComponent, + NgIf, + TranslateModule, + AsyncPipe, + VarDirective, + ], + standalone: true, }) export class CommunityPageSubCollectionListComponent implements OnInit, OnDestroy { @Input() community: Community; diff --git a/src/app/community-page/sections/sub-com-col-section/sub-collection-list/themed-community-page-sub-collection-list.component.ts b/src/app/community-page/sections/sub-com-col-section/sub-collection-list/themed-community-page-sub-collection-list.component.ts index edc15260f6..4a965bc926 100644 --- a/src/app/community-page/sections/sub-com-col-section/sub-collection-list/themed-community-page-sub-collection-list.component.ts +++ b/src/app/community-page/sections/sub-com-col-section/sub-collection-list/themed-community-page-sub-collection-list.component.ts @@ -8,9 +8,11 @@ import { ThemedComponent } from '../../../../shared/theme-support/themed.compone import { CommunityPageSubCollectionListComponent } from './community-page-sub-collection-list.component'; @Component({ - selector: 'ds-themed-community-page-sub-collection-list', + selector: 'ds-community-page-sub-collection-list', styleUrls: [], templateUrl: '../../../../shared/theme-support/themed.component.html', + standalone: true, + imports: [CommunityPageSubCollectionListComponent], }) export class ThemedCollectionPageSubCollectionListComponent extends ThemedComponent { @Input() community: Community; diff --git a/src/app/community-page/sections/sub-com-col-section/sub-com-col-section.component.html b/src/app/community-page/sections/sub-com-col-section/sub-com-col-section.component.html index 515e08ffdf..a811014bcc 100644 --- a/src/app/community-page/sections/sub-com-col-section/sub-com-col-section.component.html +++ b/src/app/community-page/sections/sub-com-col-section/sub-com-col-section.component.html @@ -1,8 +1,8 @@ - - - + - + diff --git a/src/app/community-page/sections/sub-com-col-section/sub-com-col-section.component.spec.ts b/src/app/community-page/sections/sub-com-col-section/sub-com-col-section.component.spec.ts index e28ff98ee7..85d8eb4fb7 100644 --- a/src/app/community-page/sections/sub-com-col-section/sub-com-col-section.component.spec.ts +++ b/src/app/community-page/sections/sub-com-col-section/sub-com-col-section.component.spec.ts @@ -18,9 +18,7 @@ describe('SubComColSectionComponent', () => { activatedRoute.parent = new ActivatedRouteStub(); await TestBed.configureTestingModule({ - declarations: [ - SubComColSectionComponent, - ], + imports: [SubComColSectionComponent], providers: [ { provide: ActivatedRoute, useValue: activatedRoute }, ], diff --git a/src/app/community-page/sections/sub-com-col-section/sub-com-col-section.component.ts b/src/app/community-page/sections/sub-com-col-section/sub-com-col-section.component.ts index eb6f827240..7aed3be076 100644 --- a/src/app/community-page/sections/sub-com-col-section/sub-com-col-section.component.ts +++ b/src/app/community-page/sections/sub-com-col-section/sub-com-col-section.component.ts @@ -1,3 +1,7 @@ +import { + AsyncPipe, + NgIf, +} from '@angular/common'; import { Component, OnInit, @@ -11,11 +15,20 @@ import { map } from 'rxjs/operators'; import { RemoteData } from '../../../core/data/remote-data'; import { Community } from '../../../core/shared/community.model'; +import { ThemedCollectionPageSubCollectionListComponent } from './sub-collection-list/themed-community-page-sub-collection-list.component'; +import { ThemedCommunityPageSubCommunityListComponent } from './sub-community-list/themed-community-page-sub-community-list.component'; @Component({ selector: 'ds-sub-com-col-section', templateUrl: './sub-com-col-section.component.html', styleUrls: ['./sub-com-col-section.component.scss'], + imports: [ + ThemedCommunityPageSubCommunityListComponent, + ThemedCollectionPageSubCollectionListComponent, + AsyncPipe, + NgIf, + ], + standalone: true, }) export class SubComColSectionComponent implements OnInit { diff --git a/src/app/community-page/sections/sub-com-col-section/sub-community-list/community-page-sub-community-list.component.html b/src/app/community-page/sections/sub-com-col-section/sub-community-list/community-page-sub-community-list.component.html index 0834d08ba5..7f9840f6b7 100644 --- a/src/app/community-page/sections/sub-com-col-section/sub-community-list/community-page-sub-community-list.component.html +++ b/src/app/community-page/sections/sub-com-col-section/sub-community-list/community-page-sub-community-list.component.html @@ -9,5 +9,5 @@ - + diff --git a/src/app/community-page/sections/sub-com-col-section/sub-community-list/community-page-sub-community-list.component.spec.ts b/src/app/community-page/sections/sub-com-col-section/sub-community-list/community-page-sub-community-list.component.spec.ts index 3e99254056..2654585eda 100644 --- a/src/app/community-page/sections/sub-com-col-section/sub-community-list/community-page-sub-community-list.component.spec.ts +++ b/src/app/community-page/sections/sub-com-col-section/sub-community-list/community-page-sub-community-list.component.spec.ts @@ -28,7 +28,6 @@ import { HostWindowService } from '../../../../shared/host-window.service'; import { getMockThemeService } from '../../../../shared/mocks/theme-service.mock'; import { SelectableListService } from '../../../../shared/object-list/selectable-list/selectable-list.service'; import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils'; -import { SharedModule } from '../../../../shared/shared.module'; import { HostWindowServiceStub } from '../../../../shared/testing/host-window-service.stub'; import { PaginationServiceStub } from '../../../../shared/testing/pagination-service.stub'; import { SearchConfigurationServiceStub } from '../../../../shared/testing/search-configuration-service.stub'; @@ -156,12 +155,11 @@ describe('CommunityPageSubCommunityListComponent', () => { TestBed.configureTestingModule({ imports: [ TranslateModule.forRoot(), - SharedModule, RouterTestingModule.withRoutes([]), NgbModule, NoopAnimationsModule, + CommunityPageSubCommunityListComponent, ], - declarations: [CommunityPageSubCommunityListComponent], providers: [ { provide: CommunityDataService, useValue: communityDataServiceStub }, { provide: HostWindowService, useValue: new HostWindowServiceStub(0) }, diff --git a/src/app/community-page/sections/sub-com-col-section/sub-community-list/community-page-sub-community-list.component.ts b/src/app/community-page/sections/sub-com-col-section/sub-community-list/community-page-sub-community-list.component.ts index 181e6a3bd4..36bd9919bb 100644 --- a/src/app/community-page/sections/sub-com-col-section/sub-community-list/community-page-sub-community-list.component.ts +++ b/src/app/community-page/sections/sub-com-col-section/sub-community-list/community-page-sub-community-list.component.ts @@ -1,3 +1,7 @@ +import { + AsyncPipe, + NgIf, +} from '@angular/common'; import { Component, Input, @@ -5,6 +9,7 @@ import { OnInit, } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; +import { TranslateModule } from '@ngx-translate/core'; import { BehaviorSubject, combineLatest as observableCombineLatest, @@ -23,13 +28,31 @@ import { PaginationService } from '../../../../core/pagination/pagination.servic import { Community } from '../../../../core/shared/community.model'; import { fadeIn } from '../../../../shared/animations/fade'; import { hasValue } from '../../../../shared/empty.util'; +import { ErrorComponent } from '../../../../shared/error/error.component'; +import { ThemedLoadingComponent } from '../../../../shared/loading/themed-loading.component'; +import { ObjectCollectionComponent } from '../../../../shared/object-collection/object-collection.component'; import { PaginationComponentOptions } from '../../../../shared/pagination/pagination-component-options.model'; +import { VarDirective } from '../../../../shared/utils/var.directive'; @Component({ - selector: 'ds-community-page-sub-community-list', + selector: 'ds-base-community-page-sub-community-list', styleUrls: ['./community-page-sub-community-list.component.scss'], templateUrl: './community-page-sub-community-list.component.html', animations: [fadeIn], + imports: [ + ErrorComponent, + ThemedLoadingComponent, + VarDirective, + NgIf, + ObjectCollectionComponent, + AsyncPipe, + TranslateModule, + ObjectCollectionComponent, + ErrorComponent, + ThemedLoadingComponent, + VarDirective, + ], + standalone: true, }) /** * Component to render the sub-communities of a Community diff --git a/src/app/community-page/sections/sub-com-col-section/sub-community-list/themed-community-page-sub-community-list.component.ts b/src/app/community-page/sections/sub-com-col-section/sub-community-list/themed-community-page-sub-community-list.component.ts index 8de902138e..5988ad0f5e 100644 --- a/src/app/community-page/sections/sub-com-col-section/sub-community-list/themed-community-page-sub-community-list.component.ts +++ b/src/app/community-page/sections/sub-com-col-section/sub-community-list/themed-community-page-sub-community-list.component.ts @@ -8,9 +8,11 @@ import { ThemedComponent } from '../../../../shared/theme-support/themed.compone import { CommunityPageSubCommunityListComponent } from './community-page-sub-community-list.component'; @Component({ - selector: 'ds-themed-community-page-sub-community-list', + selector: 'ds-community-page-sub-community-list', styleUrls: [], templateUrl: '../../../../shared/theme-support/themed.component.html', + standalone: true, + imports: [CommunityPageSubCommunityListComponent], }) export class ThemedCommunityPageSubCommunityListComponent extends ThemedComponent { diff --git a/src/app/community-page/themed-community-page.component.ts b/src/app/community-page/themed-community-page.component.ts index 8b200b768e..b655452041 100644 --- a/src/app/community-page/themed-community-page.component.ts +++ b/src/app/community-page/themed-community-page.component.ts @@ -7,9 +7,11 @@ import { CommunityPageComponent } from './community-page.component'; * Themed wrapper for CommunityPageComponent */ @Component({ - selector: 'ds-themed-community-page', + selector: 'ds-community-page', styleUrls: [], templateUrl: '../shared/theme-support/themed.component.html', + standalone: true, + imports: [CommunityPageComponent], }) export class ThemedCommunityPageComponent extends ThemedComponent { protected getComponentName(): string { diff --git a/src/app/core/auth/auth-blocking.guard.spec.ts b/src/app/core/auth/auth-blocking.guard.spec.ts index 8cc071ec8e..295e5b1e75 100644 --- a/src/app/core/auth/auth-blocking.guard.spec.ts +++ b/src/app/core/auth/auth-blocking.guard.spec.ts @@ -17,10 +17,10 @@ import { storeModuleConfig, } from '../../app.reducer'; import { authReducer } from './auth.reducer'; -import { AuthBlockingGuard } from './auth-blocking.guard'; +import { authBlockingGuard } from './auth-blocking.guard'; -describe('AuthBlockingGuard', () => { - let guard: AuthBlockingGuard; +describe('authBlockingGuard', () => { + let guard: any; let initialState; let store: Store; let mockStore: MockStore; @@ -44,7 +44,7 @@ describe('AuthBlockingGuard', () => { ], providers: [ provideMockStore({ initialState }), - { provide: AuthBlockingGuard, useValue: guard }, + { provide: authBlockingGuard, useValue: guard }, ], }).compileComponents(); })); @@ -52,14 +52,14 @@ describe('AuthBlockingGuard', () => { beforeEach(() => { store = TestBed.inject(Store); mockStore = store as MockStore; - guard = new AuthBlockingGuard(store); + guard = authBlockingGuard; }); describe(`canActivate`, () => { describe(`when authState.blocking is undefined`, () => { it(`should not emit anything`, (done) => { - expect(guard.canActivate()).toBeObservable(cold('-')); + expect(guard(null, null, store)).toBeObservable(cold('-')); done(); }); }); @@ -77,7 +77,7 @@ describe('AuthBlockingGuard', () => { }); it(`should not emit anything`, (done) => { - expect(guard.canActivate()).toBeObservable(cold('-')); + expect(guard(null, null, store)).toBeObservable(cold('-')); done(); }); }); @@ -95,7 +95,7 @@ describe('AuthBlockingGuard', () => { }); it(`should succeed`, (done) => { - expect(guard.canActivate()).toBeObservable(cold('(a|)', { a: true })); + expect(guard(null, null, store)).toBeObservable(cold('(a|)', { a: true })); done(); }); }); diff --git a/src/app/core/auth/auth-blocking.guard.ts b/src/app/core/auth/auth-blocking.guard.ts index b6c575d789..c76480ec0d 100644 --- a/src/app/core/auth/auth-blocking.guard.ts +++ b/src/app/core/auth/auth-blocking.guard.ts @@ -1,5 +1,9 @@ -import { Injectable } from '@angular/core'; -import { CanActivate } from '@angular/router'; +import { inject } from '@angular/core'; +import { + ActivatedRouteSnapshot, + CanActivateFn, + RouterStateSnapshot, +} from '@angular/router'; import { select, Store, @@ -20,24 +24,16 @@ import { isAuthenticationBlocking } from './selectors'; * route until the authentication status has loaded. * To ensure all rest requests get the correct auth header. */ -@Injectable({ - providedIn: 'root', -}) -export class AuthBlockingGuard implements CanActivate { +export const authBlockingGuard: CanActivateFn = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + store: Store = inject(Store), +): Observable => { + return store.pipe(select(isAuthenticationBlocking)).pipe( + map((isBlocking: boolean) => isBlocking === false), + distinctUntilChanged(), + filter((finished: boolean) => finished === true), + take(1), + ); +}; - constructor(private store: Store) { - } - - /** - * True when the authentication isn't blocking everything - */ - canActivate(): Observable { - return this.store.pipe(select(isAuthenticationBlocking)).pipe( - map((isBlocking: boolean) => isBlocking === false), - distinctUntilChanged(), - filter((finished: boolean) => finished === true), - take(1), - ); - } - -} diff --git a/src/app/core/auth/auth-request.service.spec.ts b/src/app/core/auth/auth-request.service.spec.ts index 55101dd3c5..2220efe5fa 100644 --- a/src/app/core/auth/auth-request.service.spec.ts +++ b/src/app/core/auth/auth-request.service.spec.ts @@ -1,3 +1,7 @@ +import { + Observable, + of as observableOf, +} from 'rxjs'; import { TestScheduler } from 'rxjs/testing'; import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils'; @@ -5,18 +9,13 @@ import { RemoteDataBuildService } from '../cache/builders/remote-data-build.serv import { RemoteData } from '../data/remote-data'; import { PostRequest } from '../data/request.models'; import { RequestService } from '../data/request.service'; +import { RestRequestMethod } from '../data/rest-request-method'; import { HttpOptions } from '../dspace-rest/dspace-rest.service'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { AuthRequestService } from './auth-request.service'; +import { AuthStatus } from './models/auth-status.model'; import { ShortLivedToken } from './models/short-lived-token.model'; import objectContaining = jasmine.objectContaining; -import { - Observable, - of as observableOf, -} from 'rxjs'; - -import { RestRequestMethod } from '../data/rest-request-method'; -import { AuthStatus } from './models/auth-status.model'; describe(`AuthRequestService`, () => { let halService: HALEndpointService; diff --git a/src/app/core/auth/auth.service.spec.ts b/src/app/core/auth/auth.service.spec.ts index 7e2f4f5fe1..66c80b9bf5 100644 --- a/src/app/core/auth/auth.service.spec.ts +++ b/src/app/core/auth/auth.service.spec.ts @@ -12,7 +12,6 @@ import { Store, StoreModule, } from '@ngrx/store'; -import { REQUEST } from '@nguniversal/express-engine/tokens'; import { TranslateService } from '@ngx-translate/core'; import { cold } from 'jasmine-marbles'; import { @@ -20,6 +19,7 @@ import { of as observableOf, } from 'rxjs'; +import { REQUEST } from '../../../express.tokens'; import { AppState } from '../../app.reducer'; import { getMockTranslateService } from '../../shared/mocks/translate.service.mock'; import { NotificationsService } from '../../shared/notifications/notifications.service'; @@ -150,7 +150,6 @@ describe('AuthService test', () => { }, }), ], - declarations: [], providers: [ { provide: AuthRequestService, useValue: authRequest }, { provide: NativeWindowService, useValue: window }, diff --git a/src/app/core/auth/auth.service.ts b/src/app/core/auth/auth.service.ts index 7da72b25b6..cd773b68cf 100644 --- a/src/app/core/auth/auth.service.ts +++ b/src/app/core/auth/auth.service.ts @@ -9,10 +9,6 @@ import { select, Store, } from '@ngrx/store'; -import { - REQUEST, - RESPONSE, -} from '@nguniversal/express-engine/tokens'; import { TranslateService } from '@ngx-translate/core'; import { CookieAttributes } from 'js-cookie'; import { @@ -28,6 +24,10 @@ import { } from 'rxjs/operators'; import { environment } from '../../../environments/environment'; +import { + REQUEST, + RESPONSE, +} from '../../../express.tokens'; import { AppState } from '../../app.reducer'; import { hasNoValue, @@ -97,7 +97,7 @@ export const IMPERSONATING_COOKIE = 'dsImpersonatingEPerson'; /** * The auth service. */ -@Injectable() +@Injectable({ providedIn: 'root' }) export class AuthService { /** diff --git a/src/app/core/auth/authenticated.guard.ts b/src/app/core/auth/authenticated.guard.ts index 4a989e0979..eba6dc89f9 100644 --- a/src/app/core/auth/authenticated.guard.ts +++ b/src/app/core/auth/authenticated.guard.ts @@ -1,7 +1,8 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, - CanActivate, + CanActivateChildFn, + CanActivateFn, Router, RouterStateSnapshot, UrlTree, @@ -17,7 +18,7 @@ import { switchMap, } from 'rxjs/operators'; -import { CoreState } from '../core-state.model'; +import { AppState } from '../../app.reducer'; import { AuthService, LOGIN_ROUTE, @@ -29,49 +30,35 @@ import { /** * Prevent unauthorized activating and loading of routes - * @class AuthenticatedGuard + * True when user is authenticated + * UrlTree with redirect to login page when user isn't authenticated + * @method canActivate */ -@Injectable() -export class AuthenticatedGuard implements CanActivate { +export const authenticatedGuard: CanActivateFn = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + authService: AuthService = inject(AuthService), + router: Router = inject(Router), + store: Store = inject(Store), +): Observable => { + const url = state.url; + // redirect to sign in page if user is not authenticated + return store.pipe(select(isAuthenticationLoading)).pipe( + find((isLoading: boolean) => isLoading === false), + switchMap(() => store.pipe(select(isAuthenticated))), + map((authenticated) => { + if (authenticated) { + return authenticated; + } else { + authService.setRedirectUrl(url); + authService.removeToken(); + return router.createUrlTree([LOGIN_ROUTE]); + } + }), + ); +}; - /** - * @constructor - */ - constructor(private authService: AuthService, private router: Router, private store: Store) {} - - /** - * True when user is authenticated - * UrlTree with redirect to login page when user isn't authenticated - * @method canActivate - */ - canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { - const url = state.url; - return this.handleAuth(url); - } - - /** - * True when user is authenticated - * UrlTree with redirect to login page when user isn't authenticated - * @method canActivateChild - */ - canActivateChild(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { - return this.canActivate(route, state); - } - - private handleAuth(url: string): Observable { - // redirect to sign in page if user is not authenticated - return this.store.pipe(select(isAuthenticationLoading)).pipe( - find((isLoading: boolean) => isLoading === false), - switchMap(() => this.store.pipe(select(isAuthenticated))), - map((authenticated) => { - if (authenticated) { - return authenticated; - } else { - this.authService.setRedirectUrl(url); - this.authService.removeToken(); - return this.router.createUrlTree([LOGIN_ROUTE]); - } - }), - ); - } -} +export const AuthenticatedGuardChild: CanActivateChildFn = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, +) => authenticatedGuard(route, state); diff --git a/src/app/core/auth/models/auth-status.model.ts b/src/app/core/auth/models/auth-status.model.ts index 1a6938887d..f25e5a4892 100644 --- a/src/app/core/auth/models/auth-status.model.ts +++ b/src/app/core/auth/models/auth-status.model.ts @@ -36,14 +36,14 @@ export class AuthStatus implements CacheableObject { * The unique identifier of this auth status */ @autoserialize - id: string; + id: string; /** * The type for this AuthStatus */ @excludeFromEquals @autoserialize - type: ResourceType; + type: ResourceType; /** * The UUID of this auth status @@ -57,19 +57,19 @@ export class AuthStatus implements CacheableObject { * True if REST API is up and running, should never return false */ @autoserialize - okay: boolean; + okay: boolean; /** * If the auth status represents an authenticated state */ @autoserialize - authenticated: boolean; + authenticated: boolean; /** * The {@link HALLink}s for this AuthStatus */ @deserialize - _links: { + _links: { self: HALLink; eperson: HALLink; specialGroups: HALLink; @@ -80,32 +80,32 @@ export class AuthStatus implements CacheableObject { * Will be undefined unless the eperson {@link HALLink} has been resolved. */ @link(EPERSON) - eperson?: Observable>; + eperson?: Observable>; /** * The SpecialGroup of this auth status * Will be undefined unless the SpecialGroup {@link HALLink} has been resolved. */ @link(GROUP, true) - specialGroups?: Observable>>; + specialGroups?: Observable>>; /** * True if the token is valid, false if there was no token or the token wasn't valid */ @autoserialize - token?: AuthTokenInfo; + token?: AuthTokenInfo; /** * Authentication error if there was one for this status */ // TODO should be refactored to use the RemoteData error @autoserialize - error?: AuthError; + error?: AuthError; /** * All authentication methods enabled at the backend */ @autoserialize - authMethods: AuthMethod[]; + authMethods: AuthMethod[]; } diff --git a/src/app/core/auth/models/short-lived-token.model.ts b/src/app/core/auth/models/short-lived-token.model.ts index d91a26e990..5e8587d02d 100644 --- a/src/app/core/auth/models/short-lived-token.model.ts +++ b/src/app/core/auth/models/short-lived-token.model.ts @@ -22,19 +22,19 @@ export class ShortLivedToken implements CacheableObject { */ @excludeFromEquals @autoserialize - type: ResourceType; + type: ResourceType; /** * The value for this ShortLivedToken */ @autoserializeAs('token') - value: string; + value: string; /** * The {@link HALLink}s for this ShortLivedToken */ @deserialize - _links: { + _links: { self: HALLink; }; } diff --git a/src/app/core/auth/token-response-parsing.service.ts b/src/app/core/auth/token-response-parsing.service.ts index 3990d0f44e..03a45521e9 100644 --- a/src/app/core/auth/token-response-parsing.service.ts +++ b/src/app/core/auth/token-response-parsing.service.ts @@ -9,7 +9,7 @@ import { ResponseParsingService } from '../data/parsing.service'; import { RestRequest } from '../data/rest-request.model'; import { RawRestResponse } from '../dspace-rest/raw-rest-response.model'; -@Injectable() +@Injectable({ providedIn: 'root' }) /** * A ResponseParsingService used to parse RawRestResponse coming from the REST API to a token string * wrapped in a TokenResponse diff --git a/src/app/core/breadcrumbs/bitstream-breadcrumb.resolver.ts b/src/app/core/breadcrumbs/bitstream-breadcrumb.resolver.ts index f003bd0f85..5628fe6583 100644 --- a/src/app/core/breadcrumbs/bitstream-breadcrumb.resolver.ts +++ b/src/app/core/breadcrumbs/bitstream-breadcrumb.resolver.ts @@ -1,31 +1,36 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; +import { + ActivatedRouteSnapshot, + ResolveFn, + RouterStateSnapshot, +} from '@angular/router'; +import { Observable } from 'rxjs'; import { BITSTREAM_PAGE_LINKS_TO_FOLLOW } from '../../bitstream-page/bitstream-page.resolver'; +import { BreadcrumbConfig } from '../../breadcrumbs/breadcrumb/breadcrumb-config.model'; import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; import { BitstreamDataService } from '../data/bitstream-data.service'; import { Bitstream } from '../shared/bitstream.model'; +import { DSpaceObject } from '../shared/dspace-object.model'; import { BitstreamBreadcrumbsService } from './bitstream-breadcrumbs.service'; import { DSOBreadcrumbResolver } from './dso-breadcrumb.resolver'; /** - * The class that resolves the BreadcrumbConfig object for an Item + * The resolve function that resolves the BreadcrumbConfig object for an Item */ -@Injectable({ - providedIn: 'root', -}) -export class BitstreamBreadcrumbResolver extends DSOBreadcrumbResolver { - constructor( - protected breadcrumbService: BitstreamBreadcrumbsService, protected dataService: BitstreamDataService) { - super(breadcrumbService, dataService); - } +export const bitstreamBreadcrumbResolver: ResolveFn> = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + breadcrumbService: BitstreamBreadcrumbsService = inject(BitstreamBreadcrumbsService), + dataService: BitstreamDataService = inject(BitstreamDataService), +): Observable> => { + const linksToFollow: FollowLinkConfig[] = BITSTREAM_PAGE_LINKS_TO_FOLLOW as FollowLinkConfig[]; + return DSOBreadcrumbResolver( + route, + state, + breadcrumbService, + dataService, + ...linksToFollow, + ) as Observable>; +}; - /** - * Method that returns the follow links to already resolve - * The self links defined in this list are expected to be requested somewhere in the near future - * Requesting them as embeds will limit the number of requests - */ - get followLinks(): FollowLinkConfig[] { - return BITSTREAM_PAGE_LINKS_TO_FOLLOW; - } - -} diff --git a/src/app/core/breadcrumbs/collection-breadcrumb.resolver.ts b/src/app/core/breadcrumbs/collection-breadcrumb.resolver.ts index d337da22c7..7df656a961 100644 --- a/src/app/core/breadcrumbs/collection-breadcrumb.resolver.ts +++ b/src/app/core/breadcrumbs/collection-breadcrumb.resolver.ts @@ -1,29 +1,36 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; +import { + ActivatedRouteSnapshot, + ResolveFn, + RouterStateSnapshot, +} from '@angular/router'; +import { Observable } from 'rxjs'; +import { BreadcrumbConfig } from '../../breadcrumbs/breadcrumb/breadcrumb-config.model'; import { COLLECTION_PAGE_LINKS_TO_FOLLOW } from '../../collection-page/collection-page.resolver'; import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; import { CollectionDataService } from '../data/collection-data.service'; import { Collection } from '../shared/collection.model'; +import { DSpaceObject } from '../shared/dspace-object.model'; import { DSOBreadcrumbResolver } from './dso-breadcrumb.resolver'; import { DSOBreadcrumbsService } from './dso-breadcrumbs.service'; /** - * The class that resolves the BreadcrumbConfig object for a Collection + * The resolve function that resolves the BreadcrumbConfig object for a Collection */ -@Injectable({ - providedIn: 'root', -}) -export class CollectionBreadcrumbResolver extends DSOBreadcrumbResolver { - constructor(protected breadcrumbService: DSOBreadcrumbsService, protected dataService: CollectionDataService) { - super(breadcrumbService, dataService); - } +export const collectionBreadcrumbResolver: ResolveFn> = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + breadcrumbService: DSOBreadcrumbsService = inject(DSOBreadcrumbsService), + dataService: CollectionDataService = inject(CollectionDataService), +): Observable> => { + const linksToFollow: FollowLinkConfig[] = COLLECTION_PAGE_LINKS_TO_FOLLOW as FollowLinkConfig[]; + return DSOBreadcrumbResolver( + route, + state, + breadcrumbService, + dataService, + ...linksToFollow, + ) as Observable>; +}; - /** - * Method that returns the follow links to already resolve - * The self links defined in this list are expected to be requested somewhere in the near future - * Requesting them as embeds will limit the number of requests - */ - get followLinks(): FollowLinkConfig[] { - return COLLECTION_PAGE_LINKS_TO_FOLLOW; - } -} diff --git a/src/app/core/breadcrumbs/community-breadcrumb.resolver.ts b/src/app/core/breadcrumbs/community-breadcrumb.resolver.ts index 4cbffe9a6a..0c37b5ca4f 100644 --- a/src/app/core/breadcrumbs/community-breadcrumb.resolver.ts +++ b/src/app/core/breadcrumbs/community-breadcrumb.resolver.ts @@ -1,29 +1,50 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; +import { + ActivatedRouteSnapshot, + ResolveFn, + RouterStateSnapshot, +} from '@angular/router'; +import { Observable } from 'rxjs'; +import { BreadcrumbConfig } from '../../breadcrumbs/breadcrumb/breadcrumb-config.model'; import { COMMUNITY_PAGE_LINKS_TO_FOLLOW } from '../../community-page/community-page.resolver'; +import { hasValue } from '../../shared/empty.util'; import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; import { CommunityDataService } from '../data/community-data.service'; import { Community } from '../shared/community.model'; -import { DSOBreadcrumbResolver } from './dso-breadcrumb.resolver'; +import { DSpaceObject } from '../shared/dspace-object.model'; +import { + DSOBreadcrumbResolver, + DSOBreadcrumbResolverByUuid, +} from './dso-breadcrumb.resolver'; import { DSOBreadcrumbsService } from './dso-breadcrumbs.service'; /** - * The class that resolves the BreadcrumbConfig object for a Community + * The resolve function that resolves the BreadcrumbConfig object for a Community */ -@Injectable({ - providedIn: 'root', -}) -export class CommunityBreadcrumbResolver extends DSOBreadcrumbResolver { - constructor(protected breadcrumbService: DSOBreadcrumbsService, protected dataService: CommunityDataService) { - super(breadcrumbService, dataService); +export const communityBreadcrumbResolver: ResolveFn> = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + breadcrumbService: DSOBreadcrumbsService = inject(DSOBreadcrumbsService), + dataService: CommunityDataService = inject(CommunityDataService), +): Observable> => { + const linksToFollow: FollowLinkConfig[] = COMMUNITY_PAGE_LINKS_TO_FOLLOW as FollowLinkConfig[]; + if (hasValue(route.data.breadcrumbQueryParam) && hasValue(route.queryParams[route.data.breadcrumbQueryParam])) { + return DSOBreadcrumbResolverByUuid( + route, + state, + route.queryParams[route.data.breadcrumbQueryParam], + breadcrumbService, + dataService, + ...linksToFollow, + ) as Observable>; + } else { + return DSOBreadcrumbResolver( + route, + state, + breadcrumbService, + dataService, + ...linksToFollow, + ) as Observable>; } - - /** - * Method that returns the follow links to already resolve - * The self links defined in this list are expected to be requested somewhere in the near future - * Requesting them as embeds will limit the number of requests - */ - get followLinks(): FollowLinkConfig[] { - return COMMUNITY_PAGE_LINKS_TO_FOLLOW; - } -} +}; diff --git a/src/app/core/breadcrumbs/dso-breadcrumb.resolver.spec.ts b/src/app/core/breadcrumbs/dso-breadcrumb.resolver.spec.ts index 59fda031b2..c40a14a323 100644 --- a/src/app/core/breadcrumbs/dso-breadcrumb.resolver.spec.ts +++ b/src/app/core/breadcrumbs/dso-breadcrumb.resolver.spec.ts @@ -2,12 +2,11 @@ import { getTestScheduler } from 'jasmine-marbles'; import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; import { Collection } from '../shared/collection.model'; -import { CollectionBreadcrumbResolver } from './collection-breadcrumb.resolver'; -import { DSOBreadcrumbResolver } from './dso-breadcrumb.resolver'; +import { collectionBreadcrumbResolver } from './collection-breadcrumb.resolver'; describe('DSOBreadcrumbResolver', () => { describe('resolve', () => { - let resolver: DSOBreadcrumbResolver; + let resolver: any; let collectionService: any; let dsoBreadcrumbService: any; let testCollection: Collection; @@ -17,18 +16,21 @@ describe('DSOBreadcrumbResolver', () => { beforeEach(() => { uuid = '1234-65487-12354-1235'; - breadcrumbUrl = '/collections/' + uuid; - currentUrl = breadcrumbUrl + '/edit'; - testCollection = Object.assign(new Collection(), { uuid }); + breadcrumbUrl = `/collections/${uuid}`; + currentUrl = `${breadcrumbUrl}/edit`; + testCollection = Object.assign(new Collection(), { + uuid: uuid, + type: 'collection', + }); dsoBreadcrumbService = {}; collectionService = { - findById: (id: string) => createSuccessfulRemoteDataObject$(testCollection), + findById: () => createSuccessfulRemoteDataObject$(testCollection), }; - resolver = new CollectionBreadcrumbResolver(dsoBreadcrumbService, collectionService); + resolver = collectionBreadcrumbResolver; }); it('should resolve a breadcrumb config for the correct DSO', () => { - const resolvedConfig = resolver.resolve({ params: { id: uuid } } as any, { url: currentUrl } as any); + const resolvedConfig = resolver({ params: { id: uuid } } as any, { url: currentUrl } as any, dsoBreadcrumbService, collectionService); const expectedConfig = { provider: dsoBreadcrumbService, key: testCollection, url: breadcrumbUrl }; getTestScheduler().expectObservable(resolvedConfig).toBe('(a|)', { a: expectedConfig }); }); diff --git a/src/app/core/breadcrumbs/dso-breadcrumb.resolver.ts b/src/app/core/breadcrumbs/dso-breadcrumb.resolver.ts index 712f40a3c5..992627ddfa 100644 --- a/src/app/core/breadcrumbs/dso-breadcrumb.resolver.ts +++ b/src/app/core/breadcrumbs/dso-breadcrumb.resolver.ts @@ -1,17 +1,15 @@ -import { Injectable } from '@angular/core'; import { ActivatedRouteSnapshot, - Resolve, RouterStateSnapshot, } from '@angular/router'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; +import { getDSORoute } from '../../app-routing-paths'; import { BreadcrumbConfig } from '../../breadcrumbs/breadcrumb/breadcrumb-config.model'; import { hasValue } from '../../shared/empty.util'; import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; import { IdentifiableDataService } from '../data/base/identifiable-data.service'; -import { ChildHALResource } from '../shared/child-hal-resource.model'; import { DSpaceObject } from '../shared/dspace-object.model'; import { getFirstCompletedRemoteData, @@ -20,45 +18,52 @@ import { import { DSOBreadcrumbsService } from './dso-breadcrumbs.service'; /** - * The class that resolves the BreadcrumbConfig object for a DSpaceObject + * Method for resolving a breadcrumb config object + * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot + * @param {RouterStateSnapshot} state The current RouterStateSnapshot + * @param {DSOBreadcrumbsService} breadcrumbService + * @param {IdentifiableDataService} dataService + * @param linksToFollow + * @returns BreadcrumbConfig object */ -@Injectable({ - providedIn: 'root', -}) -export abstract class DSOBreadcrumbResolver implements Resolve> { - protected constructor( - protected breadcrumbService: DSOBreadcrumbsService, - protected dataService: IdentifiableDataService, - ) { - } +export const DSOBreadcrumbResolver: (route: ActivatedRouteSnapshot, state: RouterStateSnapshot, breadcrumbService: DSOBreadcrumbsService, dataService: IdentifiableDataService, ...linksToFollow: FollowLinkConfig[]) => Observable> = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + breadcrumbService: DSOBreadcrumbsService, + dataService: IdentifiableDataService, + ...linksToFollow: FollowLinkConfig[] +): Observable> => { + return DSOBreadcrumbResolverByUuid(route, state, route.params.id, breadcrumbService, dataService, ...linksToFollow); +}; - /** - * Method for resolving a breadcrumb config object - * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot - * @param {RouterStateSnapshot} state The current RouterStateSnapshot - * @returns BreadcrumbConfig object - */ - resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable> { - const uuid = route.params.id; - return this.dataService.findById(uuid, true, false, ...this.followLinks).pipe( - getFirstCompletedRemoteData(), - getRemoteDataPayload(), - map((object: T) => { - if (hasValue(object)) { - const fullPath = state.url; - const url = fullPath.substr(0, fullPath.indexOf(uuid)) + uuid; - return { provider: this.breadcrumbService, key: object, url: url }; - } else { - return undefined; - } - }), - ); - } - - /** - * Method that returns the follow links to already resolve - * The self links defined in this list are expected to be requested somewhere in the near future - * Requesting them as embeds will limit the number of requests - */ - abstract get followLinks(): FollowLinkConfig[]; -} +/** + * Method for resolving a breadcrumb config object with the given UUID + * + * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot + * @param {RouterStateSnapshot} state The current RouterStateSnapshot + * @param {String} uuid The uuid of the DSO object + * @param {DSOBreadcrumbsService} breadcrumbService + * @param {IdentifiableDataService} dataService + * @param linksToFollow + * @returns BreadcrumbConfig object + */ +export const DSOBreadcrumbResolverByUuid: (route: ActivatedRouteSnapshot, state: RouterStateSnapshot, uuid: string, breadcrumbService: DSOBreadcrumbsService, dataService: IdentifiableDataService, ...linksToFollow: FollowLinkConfig[]) => Observable> = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + uuid: string, + breadcrumbService: DSOBreadcrumbsService, + dataService: IdentifiableDataService, + ...linksToFollow: FollowLinkConfig[] +): Observable> => { + return dataService.findById(uuid, true, false, ...linksToFollow).pipe( + getFirstCompletedRemoteData(), + getRemoteDataPayload(), + map((object: DSpaceObject) => { + if (hasValue(object)) { + return { provider: breadcrumbService, key: object, url: getDSORoute(object) }; + } else { + return undefined; + } + }), + ); +}; diff --git a/src/app/core/breadcrumbs/dso-name.service.spec.ts b/src/app/core/breadcrumbs/dso-name.service.spec.ts index 90b08dca14..5f241b1a6c 100644 --- a/src/app/core/breadcrumbs/dso-name.service.spec.ts +++ b/src/app/core/breadcrumbs/dso-name.service.spec.ts @@ -9,6 +9,10 @@ describe(`DSONameService`, () => { let service: DSONameService; let mockPersonName: string; let mockPerson: DSpaceObject; + let mockEPersonNameFirst: string; + let mockEPersonFirst: DSpaceObject; + let mockEPersonName: string; + let mockEPerson: DSpaceObject; let mockOrgUnitName: string; let mockOrgUnit: DSpaceObject; let mockDSOName: string; @@ -25,6 +29,26 @@ describe(`DSONameService`, () => { }, }); + mockEPersonName = 'John Doe'; + mockEPerson = Object.assign(new DSpaceObject(), { + firstMetadataValue(keyOrKeys: string | string[], valueFilter?: MetadataValueFilter): string { + return mockEPersonName; + }, + getRenderTypes(): (string | GenericConstructor)[] { + return ['EPerson', Item, DSpaceObject]; + }, + }); + + mockEPersonNameFirst = 'John'; + mockEPersonFirst = Object.assign(new DSpaceObject(), { + firstMetadataValue(keyOrKeys: string | string[], valueFilter?: MetadataValueFilter): string { + return mockEPersonNameFirst; + }, + getRenderTypes(): (string | GenericConstructor)[] { + return ['EPerson', Item, DSpaceObject]; + }, + }); + mockOrgUnitName = 'Molecular Spectroscopy'; mockOrgUnit = Object.assign(new DSpaceObject(), { firstMetadataValue(keyOrKeys: string | string[], valueFilter?: MetadataValueFilter): string { @@ -67,6 +91,15 @@ describe(`DSONameService`, () => { expect(result).toBe('Bingo!'); }); + it(`should use the EPerson factory for EPerson objects`, () => { + spyOn((service as any).factories, 'EPerson').and.returnValue('Bingo!'); + + const result = service.getName(mockEPerson); + + expect((service as any).factories.EPerson).toHaveBeenCalledWith(mockEPerson); + expect(result).toBe('Bingo!'); + }); + it(`should use the Default factory for regular DSpaceObjects`, () => { spyOn((service as any).factories, 'Default').and.returnValue('Bingo!'); @@ -107,6 +140,35 @@ describe(`DSONameService`, () => { }); }); + describe(`factories.EPerson`, () => { + describe(`with eperson.firstname and without eperson.lastname`, () => { + beforeEach(() => { + spyOn(mockEPerson, 'firstMetadataValue').and.returnValues(...mockEPersonName.split(' ')); + }); + + it(`should return 'eperson.firstname' and 'eperson.lastname'`, () => { + const result = (service as any).factories.EPerson(mockEPerson); + expect(result).toBe(mockEPersonName); + expect(mockEPerson.firstMetadataValue).toHaveBeenCalledWith('eperson.firstname'); + expect(mockEPerson.firstMetadataValue).toHaveBeenCalledWith('eperson.lastname'); + }); + }); + + describe(` with eperson.firstname and without eperson.lastname`, () => { + beforeEach(() => { + spyOn(mockEPersonFirst, 'firstMetadataValue').and.returnValues(mockEPersonNameFirst, undefined); + }); + + it(`should return 'eperson.firstname'`, () => { + const result = (service as any).factories.EPerson(mockEPersonFirst); + expect(result).toBe(mockEPersonNameFirst); + expect(mockEPersonFirst.firstMetadataValue).toHaveBeenCalledWith('eperson.firstname'); + expect(mockEPersonFirst.firstMetadataValue).toHaveBeenCalledWith('eperson.lastname'); + }); + }); + }); + + describe(`factories.OrgUnit`, () => { beforeEach(() => { spyOn(mockOrgUnit, 'firstMetadataValue').and.callThrough(); diff --git a/src/app/core/breadcrumbs/i18n-breadcrumb.resolver.spec.ts b/src/app/core/breadcrumbs/i18n-breadcrumb.resolver.spec.ts index 05ef2969f7..a85338c490 100644 --- a/src/app/core/breadcrumbs/i18n-breadcrumb.resolver.spec.ts +++ b/src/app/core/breadcrumbs/i18n-breadcrumb.resolver.spec.ts @@ -1,9 +1,9 @@ import { URLCombiner } from '../url-combiner/url-combiner'; -import { I18nBreadcrumbResolver } from './i18n-breadcrumb.resolver'; +import { i18nBreadcrumbResolver } from './i18n-breadcrumb.resolver'; -describe('I18nBreadcrumbResolver', () => { +describe('i18nBreadcrumbResolver', () => { describe('resolve', () => { - let resolver: I18nBreadcrumbResolver; + let resolver: any; let i18nBreadcrumbService: any; let i18nKey: string; let route: any; @@ -27,18 +27,18 @@ describe('I18nBreadcrumbResolver', () => { }; expectedPath = new URLCombiner(parentSegment, segment).toString(); i18nBreadcrumbService = {}; - resolver = new I18nBreadcrumbResolver(i18nBreadcrumbService); + resolver = i18nBreadcrumbResolver; }); it('should resolve the breadcrumb config', () => { - const resolvedConfig = resolver.resolve(route, {} as any); + const resolvedConfig = resolver(route, {} as any, i18nBreadcrumbService); const expectedConfig = { provider: i18nBreadcrumbService, key: i18nKey, url: expectedPath }; expect(resolvedConfig).toEqual(expectedConfig); }); it('should resolve throw an error when no breadcrumbKey is defined', () => { expect(() => { - resolver.resolve({ data: {} } as any, undefined); + resolver({ data: {} } as any, undefined, i18nBreadcrumbService); }).toThrow(); }); }); diff --git a/src/app/core/breadcrumbs/i18n-breadcrumb.resolver.ts b/src/app/core/breadcrumbs/i18n-breadcrumb.resolver.ts index afcd461de8..5f5c779211 100644 --- a/src/app/core/breadcrumbs/i18n-breadcrumb.resolver.ts +++ b/src/app/core/breadcrumbs/i18n-breadcrumb.resolver.ts @@ -1,7 +1,7 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, - Resolve, + ResolveFn, RouterStateSnapshot, } from '@angular/router'; @@ -11,27 +11,21 @@ import { currentPathFromSnapshot } from '../../shared/utils/route.utils'; import { I18nBreadcrumbsService } from './i18n-breadcrumbs.service'; /** - * The class that resolves a BreadcrumbConfig object with an i18n key string for a route + * Method for resolving an I18n breadcrumb configuration object + * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot + * @param {RouterStateSnapshot} state The current RouterStateSnapshot + * @param {I18nBreadcrumbsService} breadcrumbService + * @returns BreadcrumbConfig object */ -@Injectable({ - providedIn: 'root', -}) -export class I18nBreadcrumbResolver implements Resolve> { - constructor(protected breadcrumbService: I18nBreadcrumbsService) { +export const i18nBreadcrumbResolver: ResolveFn> = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + breadcrumbService: I18nBreadcrumbsService = inject(I18nBreadcrumbsService), +): BreadcrumbConfig => { + const key = route.data.breadcrumbKey; + if (hasNoValue(key)) { + throw new Error('You provided an i18nBreadcrumbResolver for url \"' + route.url + '\" but no breadcrumbKey in the route\'s data'); } - - /** - * Method for resolving an I18n breadcrumb configuration object - * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot - * @param {RouterStateSnapshot} state The current RouterStateSnapshot - * @returns BreadcrumbConfig object - */ - resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): BreadcrumbConfig { - const key = route.data.breadcrumbKey; - if (hasNoValue(key)) { - throw new Error('You provided an i18nBreadcrumbResolver for url \"' + route.url + '\" but no breadcrumbKey in the route\'s data'); - } - const fullPath = currentPathFromSnapshot(route); - return { provider: this.breadcrumbService, key: key, url: fullPath }; - } -} + const fullPath = currentPathFromSnapshot(route); + return { provider: breadcrumbService, key: key, url: fullPath }; +}; diff --git a/src/app/core/breadcrumbs/item-breadcrumb.resolver.ts b/src/app/core/breadcrumbs/item-breadcrumb.resolver.ts index f609cbf3bc..ef021123d4 100644 --- a/src/app/core/breadcrumbs/item-breadcrumb.resolver.ts +++ b/src/app/core/breadcrumbs/item-breadcrumb.resolver.ts @@ -1,29 +1,35 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; +import { + ActivatedRouteSnapshot, + ResolveFn, + RouterStateSnapshot, +} from '@angular/router'; +import { Observable } from 'rxjs'; -import { ITEM_PAGE_LINKS_TO_FOLLOW } from '../../item-page/item.resolver'; +import { BreadcrumbConfig } from '../../breadcrumbs/breadcrumb/breadcrumb-config.model'; +import { getItemPageLinksToFollow } from '../../item-page/item.resolver'; import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; import { ItemDataService } from '../data/item-data.service'; +import { DSpaceObject } from '../shared/dspace-object.model'; import { Item } from '../shared/item.model'; import { DSOBreadcrumbResolver } from './dso-breadcrumb.resolver'; import { DSOBreadcrumbsService } from './dso-breadcrumbs.service'; /** - * The class that resolves the BreadcrumbConfig object for an Item + * The resolve function that resolves the BreadcrumbConfig object for an Item */ -@Injectable({ - providedIn: 'root', -}) -export class ItemBreadcrumbResolver extends DSOBreadcrumbResolver { - constructor(protected breadcrumbService: DSOBreadcrumbsService, protected dataService: ItemDataService) { - super(breadcrumbService, dataService); - } - - /** - * Method that returns the follow links to already resolve - * The self links defined in this list are expected to be requested somewhere in the near future - * Requesting them as embeds will limit the number of requests - */ - get followLinks(): FollowLinkConfig[] { - return ITEM_PAGE_LINKS_TO_FOLLOW; - } -} +export const itemBreadcrumbResolver: ResolveFn> = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + breadcrumbService: DSOBreadcrumbsService = inject(DSOBreadcrumbsService), + dataService: ItemDataService = inject(ItemDataService), +): Observable> => { + const linksToFollow: FollowLinkConfig[] = getItemPageLinksToFollow() as FollowLinkConfig[]; + return DSOBreadcrumbResolver( + route, + state, + breadcrumbService, + dataService, + ...linksToFollow, + ) as Observable>; +}; diff --git a/src/app/core/breadcrumbs/navigation-breadcrumb.resolver.spec.ts b/src/app/core/breadcrumbs/navigation-breadcrumb.resolver.spec.ts index db89d02f75..a6bbe49ddd 100644 --- a/src/app/core/breadcrumbs/navigation-breadcrumb.resolver.spec.ts +++ b/src/app/core/breadcrumbs/navigation-breadcrumb.resolver.spec.ts @@ -1,8 +1,8 @@ -import { NavigationBreadcrumbResolver } from './navigation-breadcrumb.resolver'; +import { navigationBreadcrumbResolver } from './navigation-breadcrumb.resolver'; -describe('NavigationBreadcrumbResolver', () => { +describe('navigationBreadcrumbResolver', () => { describe('resolve', () => { - let resolver: NavigationBreadcrumbResolver; + let resolver: any; let NavigationBreadcrumbService: any; let i18nKey: string; let relatedI18nKey: string; @@ -40,11 +40,11 @@ describe('NavigationBreadcrumbResolver', () => { }; expectedPath = '/base/example:/base'; NavigationBreadcrumbService = {}; - resolver = new NavigationBreadcrumbResolver(NavigationBreadcrumbService); + resolver = navigationBreadcrumbResolver; }); it('should resolve the breadcrumb config', () => { - const resolvedConfig = resolver.resolve(route, state); + const resolvedConfig = resolver(route, state, NavigationBreadcrumbService); const expectedConfig = { provider: NavigationBreadcrumbService, key: `${i18nKey}:${relatedI18nKey}`, url: expectedPath }; expect(resolvedConfig).toEqual(expectedConfig); }); diff --git a/src/app/core/breadcrumbs/navigation-breadcrumb.resolver.ts b/src/app/core/breadcrumbs/navigation-breadcrumb.resolver.ts index 594c1a694f..ac306ee3f5 100644 --- a/src/app/core/breadcrumbs/navigation-breadcrumb.resolver.ts +++ b/src/app/core/breadcrumbs/navigation-breadcrumb.resolver.ts @@ -1,7 +1,7 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, - Resolve, + ResolveFn, RouterStateSnapshot, } from '@angular/router'; @@ -9,49 +9,44 @@ import { BreadcrumbConfig } from '../../breadcrumbs/breadcrumb/breadcrumb-config import { NavigationBreadcrumbsService } from './navigation-breadcrumb.service'; /** - * The class that resolves a BreadcrumbConfig object with an i18n key string for a route and related parents + * Method for resolving an I18n breadcrumb configuration object + * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot + * @param {RouterStateSnapshot} state The current RouterStateSnapshot + * @param {NavigationBreadcrumbsService} breadcrumbService + * @returns BreadcrumbConfig object */ -@Injectable({ - providedIn: 'root', -}) -export class NavigationBreadcrumbResolver implements Resolve> { - - private parentRoutes: ActivatedRouteSnapshot[] = []; - constructor(protected breadcrumbService: NavigationBreadcrumbsService) { - } - - /** - * Method to collect all parent routes snapshot from current route snapshot - * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot - */ - private getParentRoutes(route: ActivatedRouteSnapshot): void { - if (route.parent) { - this.parentRoutes.push(route.parent); - this.getParentRoutes(route.parent); - } - } - /** - * Method for resolving an I18n breadcrumb configuration object - * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot - * @param {RouterStateSnapshot} state The current RouterStateSnapshot - * @returns BreadcrumbConfig object - */ - resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): BreadcrumbConfig { - this.getParentRoutes(route); - const relatedRoutes = route.data.relatedRoutes; - const parentPaths = this.parentRoutes.map(parent => parent.routeConfig?.path); - const relatedParentRoutes = relatedRoutes.filter(relatedRoute => parentPaths.includes(relatedRoute.path)); - const baseUrlSegmentPath = route.parent.url[route.parent.url.length - 1].path; - const baseUrl = state.url.substring(0, state.url.lastIndexOf(baseUrlSegmentPath) + baseUrlSegmentPath.length); +export const navigationBreadcrumbResolver: ResolveFn> = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + breadcrumbService: NavigationBreadcrumbsService = inject(NavigationBreadcrumbsService), +): BreadcrumbConfig => { + const parentRoutes: ActivatedRouteSnapshot[] = []; + getParentRoutes(route, parentRoutes); + const relatedRoutes = route.data.relatedRoutes; + const parentPaths = parentRoutes.map(parent => parent.routeConfig?.path); + const relatedParentRoutes = relatedRoutes.filter(relatedRoute => parentPaths.includes(relatedRoute.path)); + const baseUrlSegmentPath = route.parent.url[route.parent.url.length - 1].path; + const baseUrl = state.url.substring(0, state.url.lastIndexOf(baseUrlSegmentPath) + baseUrlSegmentPath.length); - const combinedParentBreadcrumbKeys = relatedParentRoutes.reduce((previous, current) => { - return `${previous}:${current.data.breadcrumbKey}`; - }, route.data.breadcrumbKey); - const combinedUrls = relatedParentRoutes.reduce((previous, current) => { - return `${previous}:${baseUrl}${current.path}`; - }, state.url); + const combinedParentBreadcrumbKeys = relatedParentRoutes.reduce((previous, current) => { + return `${previous}:${current.data.breadcrumbKey}`; + }, route.data.breadcrumbKey); + const combinedUrls = relatedParentRoutes.reduce((previous, current) => { + return `${previous}:${baseUrl}${current.path}`; + }, state.url); - return { provider: this.breadcrumbService, key: combinedParentBreadcrumbKeys, url: combinedUrls }; + return { provider: breadcrumbService, key: combinedParentBreadcrumbKeys, url: combinedUrls }; +}; + +/** + * Method to collect all parent routes snapshot from current route snapshot + * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot + * @param {ActivatedRouteSnapshot[]} parentRoutes + */ +function getParentRoutes(route: ActivatedRouteSnapshot, parentRoutes: ActivatedRouteSnapshot[]): void { + if (route.parent) { + parentRoutes.push(route.parent); + getParentRoutes(route.parent, parentRoutes); } } diff --git a/src/app/core/breadcrumbs/publication-claim-breadcrumb.resolver.spec.ts b/src/app/core/breadcrumbs/publication-claim-breadcrumb.resolver.spec.ts index 7a0e9d43ed..7c2c34d479 100644 --- a/src/app/core/breadcrumbs/publication-claim-breadcrumb.resolver.spec.ts +++ b/src/app/core/breadcrumbs/publication-claim-breadcrumb.resolver.spec.ts @@ -1,8 +1,8 @@ -import { PublicationClaimBreadcrumbResolver } from './publication-claim-breadcrumb.resolver'; +import { publicationClaimBreadcrumbResolver } from './publication-claim-breadcrumb.resolver'; -describe('PublicationClaimBreadcrumbResolver', () => { +describe('publicationClaimBreadcrumbResolver', () => { describe('resolve', () => { - let resolver: PublicationClaimBreadcrumbResolver; + let resolver: any; let publicationClaimBreadcrumbService: any; const fullPath = '/test/publication-claim/openaire:6bee076d-4f2a-4555-a475-04a267769b2a'; const expectedKey = '6bee076d-4f2a-4555-a475-04a267769b2a'; @@ -19,11 +19,11 @@ describe('PublicationClaimBreadcrumbResolver', () => { }, }; publicationClaimBreadcrumbService = {}; - resolver = new PublicationClaimBreadcrumbResolver(publicationClaimBreadcrumbService); + resolver = publicationClaimBreadcrumbResolver; }); it('should resolve the breadcrumb config', () => { - const resolvedConfig = resolver.resolve(route as any, { url: fullPath } as any); + const resolvedConfig = resolver(route as any, { url: fullPath } as any, publicationClaimBreadcrumbService); const expectedConfig = { provider: publicationClaimBreadcrumbService, key: expectedKey }; expect(resolvedConfig).toEqual(expectedConfig); }); diff --git a/src/app/core/breadcrumbs/publication-claim-breadcrumb.resolver.ts b/src/app/core/breadcrumbs/publication-claim-breadcrumb.resolver.ts index 7bcff921e1..a1b52ce333 100644 --- a/src/app/core/breadcrumbs/publication-claim-breadcrumb.resolver.ts +++ b/src/app/core/breadcrumbs/publication-claim-breadcrumb.resolver.ts @@ -1,29 +1,18 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, - Resolve, + ResolveFn, RouterStateSnapshot, } from '@angular/router'; import { BreadcrumbConfig } from '../../breadcrumbs/breadcrumb/breadcrumb-config.model'; import { PublicationClaimBreadcrumbService } from './publication-claim-breadcrumb.service'; -@Injectable({ - providedIn: 'root', -}) -export class PublicationClaimBreadcrumbResolver implements Resolve> { - constructor(protected breadcrumbService: PublicationClaimBreadcrumbService) { - } - - /** - * Method that resolve Publication Claim item into a breadcrumb - * The parameter are retrieved by the url since part of the Publication Claim route config - * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot - * @param {RouterStateSnapshot} state The current RouterStateSnapshot - * @returns BreadcrumbConfig object - */ - resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): BreadcrumbConfig { - const targetId = route.paramMap.get('targetId').split(':')[1]; - return { provider: this.breadcrumbService, key: targetId }; - } -} +export const publicationClaimBreadcrumbResolver: ResolveFn> = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + breadcrumbService: PublicationClaimBreadcrumbService = inject(PublicationClaimBreadcrumbService), +): BreadcrumbConfig => { + const targetId = route.paramMap.get('targetId').split(':')[1]; + return { provider: breadcrumbService, key: targetId }; +}; diff --git a/src/app/core/breadcrumbs/quality-assurance-breadcrumb.resolver.spec.ts b/src/app/core/breadcrumbs/quality-assurance-breadcrumb.resolver.spec.ts index ea6045c85e..fe2fe77e7f 100644 --- a/src/app/core/breadcrumbs/quality-assurance-breadcrumb.resolver.spec.ts +++ b/src/app/core/breadcrumbs/quality-assurance-breadcrumb.resolver.spec.ts @@ -1,8 +1,8 @@ -import { QualityAssuranceBreadcrumbResolver } from './quality-assurance-breadcrumb.resolver'; +import { qualityAssuranceBreadcrumbResolver } from './quality-assurance-breadcrumb.resolver'; -describe('QualityAssuranceBreadcrumbResolver', () => { +describe('qualityAssuranceBreadcrumbResolver', () => { describe('resolve', () => { - let resolver: QualityAssuranceBreadcrumbResolver; + let resolver: any; let qualityAssuranceBreadcrumbService: any; let route: any; const fullPath = '/test/quality-assurance/'; @@ -19,11 +19,11 @@ describe('QualityAssuranceBreadcrumbResolver', () => { }, }; qualityAssuranceBreadcrumbService = {}; - resolver = new QualityAssuranceBreadcrumbResolver(qualityAssuranceBreadcrumbService); + resolver = qualityAssuranceBreadcrumbResolver; }); it('should resolve the breadcrumb config', () => { - const resolvedConfig = resolver.resolve(route as any, { url: fullPath + 'testSourceId' } as any); + const resolvedConfig = resolver(route as any, { url: fullPath + 'testSourceId' } as any, qualityAssuranceBreadcrumbService); const expectedConfig = { provider: qualityAssuranceBreadcrumbService, key: expectedKey, url: fullPath }; expect(resolvedConfig).toEqual(expectedConfig); }); diff --git a/src/app/core/breadcrumbs/quality-assurance-breadcrumb.resolver.ts b/src/app/core/breadcrumbs/quality-assurance-breadcrumb.resolver.ts index 832cd5d08c..6507a75de6 100644 --- a/src/app/core/breadcrumbs/quality-assurance-breadcrumb.resolver.ts +++ b/src/app/core/breadcrumbs/quality-assurance-breadcrumb.resolver.ts @@ -1,37 +1,27 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, - Resolve, + ResolveFn, RouterStateSnapshot, } from '@angular/router'; import { BreadcrumbConfig } from '../../breadcrumbs/breadcrumb/breadcrumb-config.model'; import { QualityAssuranceBreadcrumbService } from './quality-assurance-breadcrumb.service'; -@Injectable({ - providedIn: 'root', -}) -export class QualityAssuranceBreadcrumbResolver implements Resolve> { - constructor(protected breadcrumbService: QualityAssuranceBreadcrumbService) {} +export const qualityAssuranceBreadcrumbResolver: ResolveFn> = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + breadcrumbService: QualityAssuranceBreadcrumbService = inject(QualityAssuranceBreadcrumbService), +): BreadcrumbConfig => { + const sourceId = route.paramMap.get('sourceId'); + const topicId = route.paramMap.get('topicId'); + let key = sourceId; - /** - * Method that resolve QA item into a breadcrumb - * The parameter are retrieved by the url since part of the QA route config - * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot - * @param {RouterStateSnapshot} state The current RouterStateSnapshot - * @returns BreadcrumbConfig object - */ - resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): BreadcrumbConfig { - const sourceId = route.paramMap.get('sourceId'); - const topicId = route.paramMap.get('topicId'); - let key = sourceId; - - if (topicId) { - key += `:${topicId}`; - } - const fullPath = state.url; - const url = fullPath.substr(0, fullPath.indexOf(sourceId)); - - return { provider: this.breadcrumbService, key, url }; + if (topicId) { + key += `:${topicId}`; } -} + const fullPath = state.url; + const url = fullPath.substring(0, fullPath.indexOf(sourceId)); + + return { provider: breadcrumbService, key, url }; +}; diff --git a/src/app/core/browse/browse-definition-data.service.ts b/src/app/core/browse/browse-definition-data.service.ts index b823f42076..9c0d0d16c9 100644 --- a/src/app/core/browse/browse-definition-data.service.ts +++ b/src/app/core/browse/browse-definition-data.service.ts @@ -15,7 +15,6 @@ import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { RequestParam } from '../cache/models/request-param.model'; import { ObjectCacheService } from '../cache/object-cache.service'; -import { dataService } from '../data/base/data-service.decorator'; import { FindAllData, FindAllDataImpl, @@ -31,7 +30,6 @@ import { RemoteData } from '../data/remote-data'; import { BrowseDefinitionRestRequest } from '../data/request.models'; import { RequestService } from '../data/request.service'; import { BrowseDefinition } from '../shared/browse-definition.model'; -import { BROWSE_DEFINITION } from '../shared/browse-definition.resource-type'; import { HALEndpointService } from '../shared/hal-endpoint.service'; /** @@ -76,7 +74,6 @@ class BrowseDefinitionFindAllDataImpl extends FindAllDataImpl @Injectable({ providedIn: 'root', }) -@dataService(BROWSE_DEFINITION) export class BrowseDefinitionDataService extends IdentifiableDataService implements FindAllData, SearchData { private findAllData: BrowseDefinitionFindAllDataImpl; private searchData: SearchDataImpl; diff --git a/src/app/core/browse/browse.service.spec.ts b/src/app/core/browse/browse.service.spec.ts index f9999439e1..f3328c2bed 100644 --- a/src/app/core/browse/browse.service.spec.ts +++ b/src/app/core/browse/browse.service.spec.ts @@ -7,7 +7,6 @@ import { of as observableOf } from 'rxjs'; import { TestScheduler } from 'rxjs/testing'; import { getMockHrefOnlyDataService } from '../../shared/mocks/href-only-data.service.mock'; -import { getMockRemoteDataBuildService } from '../../shared/mocks/remote-data-build.service.mock'; import { getMockRequestService } from '../../shared/mocks/request.service.mock'; import { createSuccessfulRemoteDataObject, @@ -18,7 +17,6 @@ import { createPaginatedList, getFirstUsedArgumentOfSpyMethod, } from '../../shared/testing/utils.test'; -import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { RequestService } from '../data/request.service'; import { RequestEntry } from '../data/request-entry.model'; import { FlatBrowseDefinition } from '../shared/flat-browse-definition.model'; @@ -31,7 +29,6 @@ describe('BrowseService', () => { let scheduler: TestScheduler; let service: BrowseService; let requestService: RequestService; - let rdbService: RemoteDataBuildService; const browsesEndpointURL = 'https://rest.api/browses'; const halService: any = new HALEndpointServiceStub(browsesEndpointURL); @@ -129,7 +126,6 @@ describe('BrowseService', () => { halService, browseDefinitionDataService, hrefOnlyDataService, - rdbService, ); } @@ -141,11 +137,9 @@ describe('BrowseService', () => { beforeEach(() => { requestService = getMockRequestService(getRequestEntry$(true)); - rdbService = getMockRemoteDataBuildService(); service = initTestService(); spyOn(halService, 'getEndpoint').and .returnValue(hot('--a-', { a: browsesEndpointURL })); - spyOn(rdbService, 'buildList').and.callThrough(); }); it('should call BrowseDefinitionDataService to create the RemoteData Observable', () => { @@ -162,9 +156,7 @@ describe('BrowseService', () => { beforeEach(() => { requestService = getMockRequestService(getRequestEntry$(true)); - rdbService = getMockRemoteDataBuildService(); service = initTestService(); - spyOn(rdbService, 'buildList').and.callThrough(); }); describe('when getBrowseEntriesFor is called with a valid browse definition id', () => { @@ -215,7 +207,6 @@ describe('BrowseService', () => { describe('if getBrowseDefinitions fires', () => { beforeEach(() => { requestService = getMockRequestService(getRequestEntry$(true)); - rdbService = getMockRemoteDataBuildService(); service = initTestService(); spyOn(service, 'getBrowseDefinitions').and .returnValue(hot('--a-', { @@ -270,7 +261,6 @@ describe('BrowseService', () => { describe('if getBrowseDefinitions doesn\'t fire', () => { it('should return undefined', () => { requestService = getMockRequestService(getRequestEntry$(true)); - rdbService = getMockRemoteDataBuildService(); service = initTestService(); spyOn(service, 'getBrowseDefinitions').and .returnValue(hot('----')); @@ -288,9 +278,7 @@ describe('BrowseService', () => { describe('getFirstItemFor', () => { beforeEach(() => { requestService = getMockRequestService(); - rdbService = getMockRemoteDataBuildService(); service = initTestService(); - spyOn(rdbService, 'buildList').and.callThrough(); }); describe('when getFirstItemFor is called with a valid browse definition id', () => { diff --git a/src/app/core/browse/browse.service.ts b/src/app/core/browse/browse.service.ts index a724673d32..5fe06a700e 100644 --- a/src/app/core/browse/browse.service.ts +++ b/src/app/core/browse/browse.service.ts @@ -6,6 +6,7 @@ import { startWith, } from 'rxjs/operators'; +import { environment } from '../../../environments/environment'; import { hasValue, hasValueOperator, @@ -16,7 +17,6 @@ import { followLink, FollowLinkConfig, } from '../../shared/utils/follow-link-config.model'; -import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { SortDirection } from '../cache/models/sort-options.model'; import { HrefOnlyDataService } from '../data/href-only-data.service'; import { PaginatedList } from '../data/paginated-list.model'; @@ -38,14 +38,20 @@ import { URLCombiner } from '../url-combiner/url-combiner'; import { BrowseDefinitionDataService } from './browse-definition-data.service'; import { BrowseEntrySearchOptions } from './browse-entry-search-options.model'; -export const BROWSE_LINKS_TO_FOLLOW: FollowLinkConfig[] = [ - followLink('thumbnail'), -]; +export function getBrowseLinksToFollow(): FollowLinkConfig[] { + const followLinks = [ + followLink('thumbnail'), + ]; + if (environment.item.showAccessStatuses) { + followLinks.push(followLink('accessStatus')); + } + return followLinks; +} /** * The service handling all browse requests */ -@Injectable() +@Injectable({ providedIn: 'root' }) export class BrowseService { protected linkPath = 'browses'; @@ -67,7 +73,6 @@ export class BrowseService { protected halService: HALEndpointService, private browseDefinitionDataService: BrowseDefinitionDataService, private hrefOnlyDataService: HrefOnlyDataService, - private rdb: RemoteDataBuildService, ) { } @@ -117,7 +122,7 @@ export class BrowseService { }), ); if (options.fetchThumbnail ) { - return this.hrefOnlyDataService.findListByHref(href$, {}, undefined, undefined, ...BROWSE_LINKS_TO_FOLLOW); + return this.hrefOnlyDataService.findListByHref(href$, {}, undefined, undefined, ...getBrowseLinksToFollow()); } return this.hrefOnlyDataService.findListByHref(href$); } @@ -165,7 +170,7 @@ export class BrowseService { }), ); if (options.fetchThumbnail) { - return this.hrefOnlyDataService.findListByHref(href$, {}, undefined, undefined, ...BROWSE_LINKS_TO_FOLLOW); + return this.hrefOnlyDataService.findListByHref(href$, {}, undefined, undefined, ...getBrowseLinksToFollow()); } return this.hrefOnlyDataService.findListByHref(href$); } diff --git a/src/app/core/cache/builders/link.service.spec.ts b/src/app/core/cache/builders/link.service.spec.ts index 0bb5e4f238..122945ab6a 100644 --- a/src/app/core/cache/builders/link.service.spec.ts +++ b/src/app/core/cache/builders/link.service.spec.ts @@ -1,14 +1,13 @@ /* eslint-disable max-classes-per-file */ -import { Injectable } from '@angular/core'; import { TestBed } from '@angular/core/testing'; -import { isEmpty } from 'rxjs/operators'; - import { - followLink, - FollowLinkConfig, -} from '../../../shared/utils/follow-link-config.model'; -import { DATA_SERVICE_FACTORY } from '../../data/base/data-service.decorator'; -import { FindListOptions } from '../../data/find-list-options.model'; + isEmpty, + take, +} from 'rxjs/operators'; + +import { APP_DATA_SERVICES_MAP } from '../../../../config/app-config.interface'; +import { TestDataService } from '../../../shared/testing/test-data-service.mock'; +import { followLink } from '../../../shared/utils/follow-link-config.model'; import { HALLink } from '../../shared/hal-link.model'; import { HALResource } from '../../shared/hal-resource.model'; import { ResourceType } from '../../shared/resource-type'; @@ -32,22 +31,17 @@ class TestModel implements HALResource { self: HALLink; predecessor: HALLink; successor: HALLink; + standardLinkName: HALLink; }; predecessor?: TestModel; successor?: TestModel; + renamedProperty?: TestModel; } -@Injectable() -class TestDataService { - findListByHref(href: string, findListOptions: FindListOptions = {}, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]) { - return 'findListByHref'; - } - - findByHref(href: string, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]) { - return 'findByHref'; - } -} +const mockDataServiceMap: any = new Map([ + [TEST_MODEL.value, () => import('../../../shared/testing/test-data-service.mock').then(m => m.TestDataService)], +]); let testDataService: TestDataService; @@ -74,35 +68,48 @@ describe('LinkService', () => { testDataService = new TestDataService(); spyOn(testDataService, 'findListByHref').and.callThrough(); spyOn(testDataService, 'findByHref').and.callThrough(); + + const linksDefinitions = new Map(); + linksDefinitions.set('predecessor', { + resourceType: TEST_MODEL, + linkName: 'predecessor', + propertyName: 'predecessor', + }); + linksDefinitions.set('successor', { + resourceType: TEST_MODEL, + linkName: 'successor', + propertyName: 'successor', + }); + linksDefinitions.set('standardLinkName', { + resourceType: TEST_MODEL, + linkName: 'standardLinkName', + propertyName: 'renamedProperty', + }); + TestBed.configureTestingModule({ - providers: [LinkService, { - provide: TestDataService, - useValue: testDataService, - }, { - provide: DATA_SERVICE_FACTORY, - useValue: jasmine.createSpy('getDataServiceFor').and.returnValue(TestDataService), - }, { - provide: LINK_DEFINITION_FACTORY, - useValue: jasmine.createSpy('getLinkDefinition').and.returnValue({ - resourceType: TEST_MODEL, - linkName: 'predecessor', - propertyName: 'predecessor', - }), - }, { - provide: LINK_DEFINITION_MAP_FACTORY, - useValue: jasmine.createSpy('getLinkDefinitions').and.returnValue([ - { + providers: [ + LinkService, + { + provide: TestDataService, + useValue: testDataService, + }, + { + provide: APP_DATA_SERVICES_MAP, + useValue: mockDataServiceMap, + }, + { + provide: LINK_DEFINITION_FACTORY, + useValue: jasmine.createSpy('getLinkDefinition').and.returnValue({ resourceType: TEST_MODEL, linkName: 'predecessor', propertyName: 'predecessor', - }, - { - resourceType: TEST_MODEL, - linkName: 'successor', - propertyName: 'successor', - }, - ]), - }], + }), + }, + { + provide: LINK_DEFINITION_MAP_FACTORY, + useValue: jasmine.createSpy('getLinkDefinitions').and.returnValue(linksDefinitions), + }, + ], }); service = TestBed.inject(LinkService); }); @@ -110,10 +117,22 @@ describe('LinkService', () => { describe('resolveLink', () => { describe(`when the linkdefinition concerns a single object`, () => { beforeEach(() => { - service.resolveLink(testModel, followLink('predecessor', {}, followLink('successor'))); + result = service.resolveLink(testModel, followLink('predecessor', {}, followLink('successor'))); }); - it('should call dataservice.findByHref with the correct href and nested links', () => { - expect(testDataService.findByHref).toHaveBeenCalledWith(testModel._links.predecessor.href, true, true, followLink('successor')); + it('should call dataservice.findByHref with the correct href and nested links', (done) => { + result.predecessor.pipe(take(1)).subscribe(() => { + expect(testDataService.findByHref).toHaveBeenCalledWith(testModel._links.predecessor.href, true, true, followLink('successor')); + done(); + }); + }); + }); + describe(`when the propertyName is different than linkName`, () => { + beforeEach(() => { + result = service.resolveLink(testModel, followLink('standardLinkName', {})); + }); + it('link should be assign to custom property', () => { + expect(result.renamedProperty).toBeDefined(); + expect(result.standardLinkName).toBeUndefined(); }); }); describe(`when the linkdefinition concerns a list`, () => { @@ -124,10 +143,13 @@ describe('LinkService', () => { propertyName: 'predecessor', isList: true, }); - service.resolveLink(testModel, followLink('predecessor', { findListOptions: { some: 'options ' } as any }, followLink('successor'))); + result = service.resolveLink(testModel, followLink('predecessor', { findListOptions: { some: 'options ' } as any }, followLink('successor'))); }); - it('should call dataservice.findListByHref with the correct href, findListOptions, and nested links', () => { - expect(testDataService.findListByHref).toHaveBeenCalledWith(testModel._links.predecessor.href, { some: 'options ' } as any, true, true, followLink('successor')); + it('should call dataservice.findListByHref with the correct href, findListOptions, and nested links', (done) => { + result.predecessor.pipe(take(1)).subscribe((res) => { + expect(testDataService.findListByHref).toHaveBeenCalledWith(testModel._links.predecessor.href, { some: 'options ' } as any, true, true, followLink('successor')); + done(); + }); }); }); describe('either way', () => { @@ -139,15 +161,14 @@ describe('LinkService', () => { expect((service as any).getLinkDefinition).toHaveBeenCalledWith(testModel.constructor as any, 'predecessor'); }); - it('should call getDataServiceFor with the correct resource type', () => { - expect((service as any).getDataServiceFor).toHaveBeenCalledWith(TEST_MODEL); - }); - - it('should return the model with the resolved link', () => { + it('should return the model with the resolved link', (done) => { expect(result.type).toBe(TEST_MODEL); expect(result.value).toBe('a test value'); expect(result._links.self.href).toBe('http://self.link'); - expect(result.predecessor).toBe('findByHref'); + result.predecessor.subscribe((res) => { + expect(res).toBe('findByHref'); + done(); + }); }); }); @@ -164,12 +185,16 @@ describe('LinkService', () => { describe(`when there is no dataservice for the resourcetype in the link`, () => { beforeEach(() => { - ((service as any).getDataServiceFor as jasmine.Spy).and.returnValue(undefined); + (service as any).map = {}; }); - it('should throw an error', () => { - expect(() => { - service.resolveLink(testModel, followLink('predecessor', {}, followLink('successor'))); - }).toThrow(); + it('should throw an error', (done) => { + result = service.resolveLink(testModel, followLink('predecessor', {}, followLink('successor'))); + result.predecessor.subscribe({ + error: (error: unknown) => { + expect(error).toBeDefined(); + done(); + }, + }); }); }); }); @@ -234,8 +259,11 @@ describe('LinkService', () => { result = service.resolveLinks(testModel, followLink('predecessor')); }); - it('should return the model with the resolved link', () => { - expect(result.predecessor).toBe('findByHref'); + it('should return the model with the resolved link', (done) => { + result.predecessor.subscribe((res) => { + expect(res).toBe('findByHref'); + done(); + }); }); }); diff --git a/src/app/core/cache/builders/link.service.ts b/src/app/core/cache/builders/link.service.ts index 78cd28085e..df3ccf1c87 100644 --- a/src/app/core/cache/builders/link.service.ts +++ b/src/app/core/cache/builders/link.service.ts @@ -7,20 +7,26 @@ import { EMPTY, Observable, } from 'rxjs'; +import { + catchError, + switchMap, +} from 'rxjs/operators'; import { - hasNoValue, + APP_DATA_SERVICES_MAP, + LazyDataServicesMap, +} from '../../../../config/app-config.interface'; +import { hasValue, isNotEmpty, } from '../../../shared/empty.util'; import { FollowLinkConfig } from '../../../shared/utils/follow-link-config.model'; -import { DATA_SERVICE_FACTORY } from '../../data/base/data-service.decorator'; import { HALDataService } from '../../data/base/hal-data-service.interface'; import { PaginatedList } from '../../data/paginated-list.model'; import { RemoteData } from '../../data/remote-data'; +import { lazyDataService } from '../../lazy-data-service'; import { GenericConstructor } from '../../shared/generic-constructor'; import { HALResource } from '../../shared/hal-resource.model'; -import { ResourceType } from '../../shared/resource-type'; import { LINK_DEFINITION_FACTORY, LINK_DEFINITION_MAP_FACTORY, @@ -31,14 +37,12 @@ import { * A Service to handle the resolving and removing * of resolved {@link HALLink}s on HALResources */ -@Injectable({ - providedIn: 'root', -}) +@Injectable({ providedIn: 'root' }) export class LinkService { constructor( - protected parentInjector: Injector, - @Inject(DATA_SERVICE_FACTORY) private getDataServiceFor: (resourceType: ResourceType) => GenericConstructor>, + protected injector: Injector, + @Inject(APP_DATA_SERVICES_MAP) private map: LazyDataServicesMap, @Inject(LINK_DEFINITION_FACTORY) private getLinkDefinition: (source: GenericConstructor, linkName: keyof T['_links']) => LinkDefinition, @Inject(LINK_DEFINITION_MAP_FACTORY) private getLinkDefinitions: (source: GenericConstructor) => Map>, ) { @@ -67,34 +71,32 @@ export class LinkService { */ public resolveLinkWithoutAttaching(model, linkToFollow: FollowLinkConfig): Observable>> { const matchingLinkDef = this.getLinkDefinition(model.constructor, linkToFollow.name); - if (hasValue(matchingLinkDef)) { - const provider = this.getDataServiceFor(matchingLinkDef.resourceType); + const lazyProvider$: Observable> = lazyDataService(this.map, matchingLinkDef.resourceType.value, this.injector); + return lazyProvider$.pipe( + switchMap((provider: HALDataService) => { + const link = model._links[matchingLinkDef.linkName]; + if (hasValue(link)) { + const href = link.href; - if (hasNoValue(provider)) { - throw new Error(`The @link() for ${String(linkToFollow.name)} on ${model.constructor.name} models uses the resource type ${matchingLinkDef.resourceType.value.toUpperCase()}, but there is no service with an @dataService(${matchingLinkDef.resourceType.value.toUpperCase()}) annotation in order to retrieve it`); - } - - const service: HALDataService = Injector.create({ - providers: [], - parent: this.parentInjector, - }).get(provider); - - const link = model._links[matchingLinkDef.linkName]; - if (hasValue(link)) { - const href = link.href; - - try { - if (matchingLinkDef.isList) { - return service.findListByHref(href, linkToFollow.findListOptions, linkToFollow.useCachedVersionIfAvailable, linkToFollow.reRequestOnStale, ...linkToFollow.linksToFollow); - } else { - return service.findByHref(href, linkToFollow.useCachedVersionIfAvailable, linkToFollow.reRequestOnStale, ...linkToFollow.linksToFollow); + try { + if (matchingLinkDef.isList) { + return provider.findListByHref(href, linkToFollow.findListOptions, linkToFollow.useCachedVersionIfAvailable, linkToFollow.reRequestOnStale, ...linkToFollow.linksToFollow); + } else { + return provider.findByHref(href, linkToFollow.useCachedVersionIfAvailable, linkToFollow.reRequestOnStale, ...linkToFollow.linksToFollow); + } + } catch (e) { + console.error(`Something went wrong when using ${matchingLinkDef.resourceType.value}) ${hasValue(provider) ? '' : '(undefined) '}to resolve link ${String(linkToFollow.name)} at ${href}`); + throw e; + } } - } catch (e) { - console.error(`Something went wrong when using @dataService(${matchingLinkDef.resourceType.value}) ${hasValue(service) ? '' : '(undefined) '}to resolve link ${String(linkToFollow.name)} at ${href}`); - throw e; - } - } + + return EMPTY; + }), + catchError((err: unknown) => { + throw new Error(`The @link() for ${String(linkToFollow.name)} on ${model.constructor.name} models uses the resource type ${matchingLinkDef.resourceType.value.toUpperCase()}, but there is no service with an @dataService(${matchingLinkDef.resourceType.value.toUpperCase()}) annotation in order to retrieve it`); + }), + ); } else if (!linkToFollow.isOptional) { throw new Error(`followLink('${String(linkToFollow.name)}') was used as a required link for a ${model.constructor.name}, but there is no property on ${model.constructor.name} models with an @link() for ${String(linkToFollow.name)}`); } @@ -110,7 +112,16 @@ export class LinkService { * @param linkToFollow the {@link FollowLinkConfig} to resolve */ public resolveLink(model, linkToFollow: FollowLinkConfig): T { - model[linkToFollow.name] = this.resolveLinkWithoutAttaching(model, linkToFollow); + const linkDefinitions = this.getLinkDefinitions(model.constructor as GenericConstructor); + const linkDef = linkDefinitions.get(linkToFollow.name); + + if (isNotEmpty(linkDef)) { + // If link exist in definition we can resolve it and use a real property name + model[linkDef.propertyName] = this.resolveLinkWithoutAttaching(model, linkToFollow); + } else { + // For some links we don't have a definition, so we use the link name as property name + model[linkToFollow.name] = this.resolveLinkWithoutAttaching(model, linkToFollow); + } return model; } diff --git a/src/app/core/cache/builders/remote-data-build.service.ts b/src/app/core/cache/builders/remote-data-build.service.ts index e5a6114872..36305b4a0c 100644 --- a/src/app/core/cache/builders/remote-data-build.service.ts +++ b/src/app/core/cache/builders/remote-data-build.service.ts @@ -50,7 +50,7 @@ import { ObjectCacheService } from '../object-cache.service'; import { getClassForType } from './build-decorators'; import { LinkService } from './link.service'; -@Injectable() +@Injectable({ providedIn: 'root' }) export class RemoteDataBuildService { constructor(protected objectCache: ObjectCacheService, protected linkService: LinkService, diff --git a/src/app/core/cache/models/request-param.model.ts b/src/app/core/cache/models/request-param.model.ts index ac21fe0b8a..b78fa45e33 100644 --- a/src/app/core/cache/models/request-param.model.ts +++ b/src/app/core/cache/models/request-param.model.ts @@ -1,9 +1,14 @@ - /** * Class representing a query parameter (query?fieldName=fieldValue) used in FindListOptions object */ export class RequestParam { - constructor(public fieldName: string, public fieldValue: any) { - + constructor( + public fieldName: string, + public fieldValue: any, + public encodeValue = true, + ) { + if (encodeValue) { + this.fieldValue = encodeURIComponent(fieldValue); + } } } diff --git a/src/app/core/cache/models/self-link.model.ts b/src/app/core/cache/models/self-link.model.ts index 903a779495..a87acdd506 100644 --- a/src/app/core/cache/models/self-link.model.ts +++ b/src/app/core/cache/models/self-link.model.ts @@ -3,9 +3,9 @@ import { autoserialize } from 'cerialize'; export class SelfLink { @autoserialize - self: string; + self: string; @autoserialize - uuid: string; + uuid: string; } diff --git a/src/app/core/cache/object-cache.reducer.ts b/src/app/core/cache/object-cache.reducer.ts index 631ecd2209..1a780ff008 100644 --- a/src/app/core/cache/object-cache.reducer.ts +++ b/src/app/core/cache/object-cache.reducer.ts @@ -174,20 +174,25 @@ export function objectCacheReducer(state = initialState, action: ObjectCacheActi * the new state, with the object added, or overwritten. */ function addToObjectCache(state: ObjectCacheState, action: AddToObjectCacheAction): ObjectCacheState { - const existing = state[action.payload.objectToCache._links.self.href] || {} as any; + const cacheLink = hasValue(action.payload.objectToCache?._links?.self) ? action.payload.objectToCache._links.self.href : action.payload.alternativeLink; + const existing = state[cacheLink] || {} as any; const newAltLinks = hasValue(action.payload.alternativeLink) ? [action.payload.alternativeLink] : []; - return Object.assign({}, state, { - [action.payload.objectToCache._links.self.href]: { - data: action.payload.objectToCache, - timeCompleted: action.payload.timeCompleted, - msToLive: action.payload.msToLive, - requestUUIDs: [action.payload.requestUUID, ...(existing.requestUUIDs || [])], - dependentRequestUUIDs: existing.dependentRequestUUIDs || [], - isDirty: isNotEmpty(existing.patches), - patches: existing.patches || [], - alternativeLinks: [...(existing.alternativeLinks || []), ...newAltLinks], - } as ObjectCacheEntry, - }); + if (hasValue(cacheLink)) { + return Object.assign({}, state, { + [cacheLink]: { + data: action.payload.objectToCache, + timeCompleted: action.payload.timeCompleted, + msToLive: action.payload.msToLive, + requestUUIDs: [action.payload.requestUUID, ...(existing.requestUUIDs || [])], + dependentRequestUUIDs: existing.dependentRequestUUIDs || [], + isDirty: isNotEmpty(existing.patches), + patches: existing.patches || [], + alternativeLinks: [...(existing.alternativeLinks || []), ...newAltLinks], + } as ObjectCacheEntry, + }); + } else { + return state; + } } /** @@ -304,7 +309,7 @@ function addDependentsObjectCacheState(state: ObjectCacheState, action: AddDepen /** - * Remove all dependent request UUIDs from a cached object, used to clear out-of-date depedencies + * Remove all dependent request UUIDs from a cached object, used to clear out-of-date dependencies * * @param state the current state * @param action an AddDependentsObjectCacheAction diff --git a/src/app/core/cache/object-cache.service.ts b/src/app/core/cache/object-cache.service.ts index 09ea6450bb..f645b5a878 100644 --- a/src/app/core/cache/object-cache.service.ts +++ b/src/app/core/cache/object-cache.service.ts @@ -78,7 +78,7 @@ const entryFromSelfLinkSelector = /** * A service to interact with the object cache */ -@Injectable() +@Injectable({ providedIn: 'root' }) export class ObjectCacheService { constructor( private store: Store, @@ -99,7 +99,9 @@ export class ObjectCacheService { * An optional alternative link to this object */ add(object: CacheableObject, msToLive: number, requestUUID: string, alternativeLink?: string): void { - object = this.linkService.removeResolvedLinks(object); // Ensure the object we're storing has no resolved links + if (hasValue(object)) { + object = this.linkService.removeResolvedLinks(object); // Ensure the object we're storing has no resolved links + } this.store.dispatch(new AddToObjectCacheAction(object, new Date().getTime(), msToLive, requestUUID, alternativeLink)); } @@ -175,11 +177,15 @@ export class ObjectCacheService { }, ), map((entry: ObjectCacheEntry) => { - const type: GenericConstructor = getClassForType((entry.data as any).type); - if (typeof type !== 'function') { - throw new Error(`${type} is not a valid constructor for ${JSON.stringify(entry.data)}`); + if (hasValue(entry.data)) { + const type: GenericConstructor = getClassForType((entry.data as any).type); + if (typeof type !== 'function') { + throw new Error(`${type} is not a valid constructor for ${JSON.stringify(entry.data)}`); + } + return Object.assign(new type(), entry.data) as T; + } else { + return null; } - return Object.assign(new type(), entry.data) as T; }), ); } diff --git a/src/app/core/coar-notify/notify-info/notify-info.component.html b/src/app/core/coar-notify/notify-info/notify-info.component.html deleted file mode 100644 index 3370f83d03..0000000000 --- a/src/app/core/coar-notify/notify-info/notify-info.component.html +++ /dev/null @@ -1,18 +0,0 @@ -
- - {{ 'coar-notify-support.title' | translate }} - - -

{{ 'coar-notify-support.title' | translate }}

-

- -

{{ 'coar-notify-support.ldn-inbox.title' | translate }}

-

- -

{{ 'coar-notify-support.message-moderation.title' | translate }}

-

- {{ 'coar-notify-support.message-moderation.content' | translate }} - {{ 'coar-notify-support.message-moderation.feedback-form' | translate }} -

- -
diff --git a/src/app/core/coar-notify/notify-info/notify-info.component.ts b/src/app/core/coar-notify/notify-info/notify-info.component.ts deleted file mode 100644 index bfa268440b..0000000000 --- a/src/app/core/coar-notify/notify-info/notify-info.component.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { - Component, - OnInit, -} from '@angular/core'; -import { - map, - Observable, - of, -} from 'rxjs'; - -import { NotifyInfoService } from './notify-info.service'; - -@Component({ - selector: 'ds-notify-info', - templateUrl: './notify-info.component.html', - styleUrls: ['./notify-info.component.scss'], -}) -/** - * Component for displaying COAR notification information. - */ -export class NotifyInfoComponent implements OnInit { - /** - * Observable containing the COAR REST INBOX API URLs. - */ - coarRestApiUrl: Observable = of([]); - - constructor(private notifyInfoService: NotifyInfoService) {} - - ngOnInit() { - this.coarRestApiUrl = this.notifyInfoService.getCoarLdnLocalInboxUrls(); - } - - /** - * Generates HTML code for COAR REST API links. - * @returns An Observable that emits the generated HTML code. - */ - generateCoarRestApiLinksHTML() { - return this.coarRestApiUrl.pipe( - // transform the data into HTML - map((urls) => { - return urls.map(url => ` - ${url} - `).join(','); - }), - ); - } -} diff --git a/src/app/core/coar-notify/notify-info/notify-info.guard.spec.ts b/src/app/core/coar-notify/notify-info/notify-info.guard.spec.ts index 7c8cc3f320..706c8f684b 100644 --- a/src/app/core/coar-notify/notify-info/notify-info.guard.spec.ts +++ b/src/app/core/coar-notify/notify-info/notify-info.guard.spec.ts @@ -1,36 +1,27 @@ -import { TestBed } from '@angular/core/testing'; -import { Router } from '@angular/router'; import { of } from 'rxjs'; -import { NotifyInfoGuard } from './notify-info.guard'; -import { NotifyInfoService } from './notify-info.service'; +import { notifyInfoGuard } from './notify-info.guard'; -describe('NotifyInfoGuard', () => { - let guard: NotifyInfoGuard; +describe('notifyInfoGuard', () => { + let guard: any; let notifyInfoServiceSpy: any; let router: any; beforeEach(() => { notifyInfoServiceSpy = jasmine.createSpyObj('NotifyInfoService', ['isCoarConfigEnabled']); router = jasmine.createSpyObj('Router', ['parseUrl']); - TestBed.configureTestingModule({ - providers: [ - NotifyInfoGuard, - { provide: NotifyInfoService, useValue: notifyInfoServiceSpy }, - { provide: Router, useValue: router }, - ], - }); - guard = TestBed.inject(NotifyInfoGuard); + guard = notifyInfoGuard; }); it('should be created', () => { - expect(guard).toBeTruthy(); + notifyInfoServiceSpy.isCoarConfigEnabled.and.returnValue(of(true)); + expect(guard(null, null, notifyInfoServiceSpy, router)).toBeTruthy(); }); it('should return true if COAR config is enabled', (done) => { notifyInfoServiceSpy.isCoarConfigEnabled.and.returnValue(of(true)); - guard.canActivate(null, null).subscribe((result) => { + guard(null, null, notifyInfoServiceSpy, router).subscribe((result) => { expect(result).toBe(true); done(); }); @@ -40,7 +31,7 @@ describe('NotifyInfoGuard', () => { notifyInfoServiceSpy.isCoarConfigEnabled.and.returnValue(of(false)); router.parseUrl.and.returnValue(of('/404')); - guard.canActivate(null, null).subscribe(() => { + guard(null, null, notifyInfoServiceSpy, router).subscribe(() => { expect(router.parseUrl).toHaveBeenCalledWith('/404'); done(); }); diff --git a/src/app/core/coar-notify/notify-info/notify-info.guard.ts b/src/app/core/coar-notify/notify-info/notify-info.guard.ts index 91f3bf6cde..1025e7b62b 100644 --- a/src/app/core/coar-notify/notify-info/notify-info.guard.ts +++ b/src/app/core/coar-notify/notify-info/notify-info.guard.ts @@ -1,7 +1,7 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, - CanActivate, + CanActivateFn, Router, RouterStateSnapshot, UrlTree, @@ -11,27 +11,13 @@ import { map } from 'rxjs/operators'; import { NotifyInfoService } from './notify-info.service'; -@Injectable({ - providedIn: 'root', -}) -export class NotifyInfoGuard implements CanActivate { - constructor( - private notifyInfoService: NotifyInfoService, - private router: Router, - ) {} - - canActivate( - route: ActivatedRouteSnapshot, - state: RouterStateSnapshot, - ): Observable { - return this.notifyInfoService.isCoarConfigEnabled().pipe( - map(coarLdnEnabled => { - if (coarLdnEnabled) { - return true; - } else { - return this.router.parseUrl('/404'); - } - }), - ); - } -} +export const notifyInfoGuard: CanActivateFn = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + notifyInfoService: NotifyInfoService = inject(NotifyInfoService), + router: Router = inject(Router), +): Observable => { + return notifyInfoService.isCoarConfigEnabled().pipe( + map(isEnabled => isEnabled ? true : router.parseUrl('/404')), + ); +}; diff --git a/src/app/core/coar-notify/notify-info/notify-info.service.spec.ts b/src/app/core/coar-notify/notify-info/notify-info.service.spec.ts index d32ad729dd..6fa8295be0 100644 --- a/src/app/core/coar-notify/notify-info/notify-info.service.spec.ts +++ b/src/app/core/coar-notify/notify-info/notify-info.service.spec.ts @@ -1,7 +1,9 @@ import { TestBed } from '@angular/core/testing'; +import { ActivatedRoute } from '@angular/router'; import { of } from 'rxjs'; import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; +import { ActivatedRouteStub } from '../../../shared/testing/active-router.stub'; import { ConfigurationDataService } from '../../data/configuration-data.service'; import { AuthorizationDataService } from '../../data/feature-authorization/authorization-data.service'; import { NotifyInfoService } from './notify-info.service'; @@ -22,6 +24,7 @@ describe('NotifyInfoService', () => { NotifyInfoService, { provide: ConfigurationDataService, useValue: configurationDataService }, { provide: AuthorizationDataService, useValue: authorizationDataService }, + { provide: ActivatedRoute, useValue: new ActivatedRouteStub() }, ], }); service = TestBed.inject(NotifyInfoService); diff --git a/src/app/core/coar-notify/notify-info/notify-info.service.ts b/src/app/core/coar-notify/notify-info/notify-info.service.ts index a70a5d5cc0..455c7902ee 100644 --- a/src/app/core/coar-notify/notify-info/notify-info.service.ts +++ b/src/app/core/coar-notify/notify-info/notify-info.service.ts @@ -7,11 +7,9 @@ import { import { ConfigurationDataService } from '../../data/configuration-data.service'; import { AuthorizationDataService } from '../../data/feature-authorization/authorization-data.service'; import { FeatureID } from '../../data/feature-authorization/feature-id'; +import { RemoteData } from '../../data/remote-data'; import { ConfigurationProperty } from '../../shared/configuration-property.model'; -import { - getFirstSucceededRemoteData, - getRemoteDataPayload, -} from '../../shared/operators'; +import { getFirstCompletedRemoteData } from '../../shared/operators'; /** * Service to check COAR availability and LDN services information for the COAR Notify functionalities @@ -41,11 +39,8 @@ export class NotifyInfoService { */ getCoarLdnLocalInboxUrls(): Observable { return this.configService.findByPropertyName('ldn.notify.inbox').pipe( - getFirstSucceededRemoteData(), - getRemoteDataPayload(), - map((response: ConfigurationProperty) => { - return response.values; - }), + getFirstCompletedRemoteData(), + map((responseRD: RemoteData) => responseRD.hasSucceeded ? responseRD.payload.values : []), ); } diff --git a/src/app/core/config/bulk-access-config-data.service.ts b/src/app/core/config/bulk-access-config-data.service.ts index edae249a8f..8023e58489 100644 --- a/src/app/core/config/bulk-access-config-data.service.ts +++ b/src/app/core/config/bulk-access-config-data.service.ts @@ -2,17 +2,14 @@ import { Injectable } from '@angular/core'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { ObjectCacheService } from '../cache/object-cache.service'; -import { dataService } from '../data/base/data-service.decorator'; import { RequestService } from '../data/request.service'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { ConfigDataService } from './config-data.service'; -import { BULK_ACCESS_CONDITION_OPTIONS } from './models/config-type'; /** * Data Service responsible for retrieving Bulk Access Condition Options from the REST API */ @Injectable({ providedIn: 'root' }) -@dataService(BULK_ACCESS_CONDITION_OPTIONS) export class BulkAccessConfigDataService extends ConfigDataService { constructor( diff --git a/src/app/core/config/models/bulk-access-condition-options.model.ts b/src/app/core/config/models/bulk-access-condition-options.model.ts index c491343852..514c682b4e 100644 --- a/src/app/core/config/models/bulk-access-condition-options.model.ts +++ b/src/app/core/config/models/bulk-access-condition-options.model.ts @@ -25,19 +25,19 @@ export class BulkAccessConditionOptions extends ConfigObject { */ @excludeFromEquals @autoserialize - type: ResourceType; + type: ResourceType; @autoserializeAs(String, 'name') - uuid: string; + uuid: string; @autoserialize - id: string; + id: string; @autoserialize - itemAccessConditionOptions: AccessesConditionOption[]; + itemAccessConditionOptions: AccessesConditionOption[]; @autoserialize - bitstreamAccessConditionOptions: AccessesConditionOption[]; + bitstreamAccessConditionOptions: AccessesConditionOption[]; _links: { self: HALLink }; } diff --git a/src/app/core/config/models/config-submission-access.model.ts b/src/app/core/config/models/config-submission-access.model.ts index 4617dc4719..2e9a929183 100644 --- a/src/app/core/config/models/config-submission-access.model.ts +++ b/src/app/core/config/models/config-submission-access.model.ts @@ -22,25 +22,25 @@ export class SubmissionAccessModel extends ConfigObject { * A list of available item access conditions */ @autoserialize - accessConditionOptions: AccessesConditionOption[]; + accessConditionOptions: AccessesConditionOption[]; /** * Boolean that indicates whether the current item must be findable via search or browse. */ @autoserialize - discoverable: boolean; + discoverable: boolean; /** * Boolean that indicates whether or not the user can change the discoverable flag. */ @autoserialize - canChangeDiscoverable: boolean; + canChangeDiscoverable: boolean; /** * The links to all related resources returned by the rest api. */ @deserialize - _links: { + _links: { self: HALLink }; diff --git a/src/app/core/config/models/config-submission-definition.model.ts b/src/app/core/config/models/config-submission-definition.model.ts index 2d6b1ad604..eda4f54340 100644 --- a/src/app/core/config/models/config-submission-definition.model.ts +++ b/src/app/core/config/models/config-submission-definition.model.ts @@ -23,20 +23,20 @@ export class SubmissionDefinitionModel extends ConfigObject { * A boolean representing if this submission definition is the default or not */ @autoserialize - isDefault: boolean; + isDefault: boolean; /** * A list of SubmissionSectionModel that are present in this submission definition */ // TODO refactor using remotedata @deserialize - sections: PaginatedList; + sections: PaginatedList; /** * The links to all related resources returned by the rest api. */ @deserialize - _links: { + _links: { self: HALLink, collections: HALLink, sections: HALLink diff --git a/src/app/core/config/models/config-submission-form.model.ts b/src/app/core/config/models/config-submission-form.model.ts index f6011adc76..c524a83916 100644 --- a/src/app/core/config/models/config-submission-form.model.ts +++ b/src/app/core/config/models/config-submission-form.model.ts @@ -27,5 +27,5 @@ export class SubmissionFormModel extends ConfigObject { * An array of [FormRowModel] that are present in this form */ @autoserialize - rows: FormRowModel[]; + rows: FormRowModel[]; } diff --git a/src/app/core/config/models/config-submission-section.model.ts b/src/app/core/config/models/config-submission-section.model.ts index 0d4ae9aa10..13e19544dc 100644 --- a/src/app/core/config/models/config-submission-section.model.ts +++ b/src/app/core/config/models/config-submission-section.model.ts @@ -4,20 +4,16 @@ import { inheritSerialization, } from 'cerialize'; +import { + SectionScope, + SectionVisibility, +} from '../../../submission/objects/section-visibility.model'; import { SectionsType } from '../../../submission/sections/sections-type'; import { typedObject } from '../../cache/builders/build-decorators'; import { HALLink } from '../../shared/hal-link.model'; import { ConfigObject } from './config.model'; import { SUBMISSION_SECTION_TYPE } from './config-type'; -/** - * An interface that define section visibility and its properties. - */ -export interface SubmissionSectionVisibility { - main: any; - other: any; -} - @typedObject @inheritSerialization(ConfigObject) export class SubmissionSectionModel extends ConfigObject { @@ -27,31 +23,37 @@ export class SubmissionSectionModel extends ConfigObject { * The header for this section */ @autoserialize - header: string; + header: string; /** * A boolean representing if this submission section is the mandatory or not */ @autoserialize - mandatory: boolean; + mandatory: boolean; + + /** + * The submission scope for this section + */ + @autoserialize + scope: SectionScope; /** * A string representing the kind of section object */ @autoserialize - sectionType: SectionsType; + sectionType: SectionsType; /** - * The [SubmissionSectionVisibility] object for this section + * The [SectionVisibility] object for this section */ @autoserialize - visibility: SubmissionSectionVisibility; + visibility: SectionVisibility; /** * The {@link HALLink}s for this SubmissionSectionModel */ @deserialize - _links: { + _links: { self: HALLink; config: HALLink; }; diff --git a/src/app/core/config/models/config-submission-upload.model.ts b/src/app/core/config/models/config-submission-upload.model.ts index edc4626f83..cabc84d0f5 100644 --- a/src/app/core/config/models/config-submission-upload.model.ts +++ b/src/app/core/config/models/config-submission-upload.model.ts @@ -27,22 +27,22 @@ export class SubmissionUploadModel extends ConfigObject { * A list of available bitstream access conditions */ @autoserialize - accessConditionOptions: AccessConditionOption[]; + accessConditionOptions: AccessConditionOption[]; /** * An object representing the configuration describing the bitstream metadata form */ @link(SUBMISSION_FORMS_TYPE) - metadata?: Observable>; + metadata?: Observable>; @autoserialize - required: boolean; + required: boolean; @autoserialize - maxSize: number; + maxSize: number; @deserialize - _links: { + _links: { metadata: HALLink self: HALLink }; diff --git a/src/app/core/config/models/config.model.ts b/src/app/core/config/models/config.model.ts index c1db44e891..74d090b89d 100644 --- a/src/app/core/config/models/config.model.ts +++ b/src/app/core/config/models/config.model.ts @@ -27,13 +27,13 @@ export abstract class ConfigObject implements CacheableObject { */ @excludeFromEquals @autoserialize - type: ResourceType; + type: ResourceType; /** * The links to all related resources returned by the rest api. */ @deserialize - _links: { + _links: { self: HALLink, [name: string]: HALLink }; diff --git a/src/app/core/config/submission-accesses-config-data.service.ts b/src/app/core/config/submission-accesses-config-data.service.ts index 16986405fd..bc7a7e66d3 100644 --- a/src/app/core/config/submission-accesses-config-data.service.ts +++ b/src/app/core/config/submission-accesses-config-data.service.ts @@ -4,20 +4,17 @@ import { Observable } from 'rxjs'; import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { ObjectCacheService } from '../cache/object-cache.service'; -import { dataService } from '../data/base/data-service.decorator'; import { RemoteData } from '../data/remote-data'; import { RequestService } from '../data/request.service'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { ConfigDataService } from './config-data.service'; import { ConfigObject } from './models/config.model'; import { SubmissionAccessesModel } from './models/config-submission-accesses.model'; -import { SUBMISSION_ACCESSES_TYPE } from './models/config-type'; /** * Provides methods to retrieve, from REST server, bitstream access conditions configurations applicable during the submission process. */ -@Injectable() -@dataService(SUBMISSION_ACCESSES_TYPE) +@Injectable({ providedIn: 'root' }) export class SubmissionAccessesConfigDataService extends ConfigDataService { constructor( protected requestService: RequestService, diff --git a/src/app/core/config/submission-forms-config-data.service.ts b/src/app/core/config/submission-forms-config-data.service.ts index fb8e0e60da..fe1234defb 100644 --- a/src/app/core/config/submission-forms-config-data.service.ts +++ b/src/app/core/config/submission-forms-config-data.service.ts @@ -4,20 +4,17 @@ import { Observable } from 'rxjs'; import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { ObjectCacheService } from '../cache/object-cache.service'; -import { dataService } from '../data/base/data-service.decorator'; import { RemoteData } from '../data/remote-data'; import { RequestService } from '../data/request.service'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { ConfigDataService } from './config-data.service'; import { ConfigObject } from './models/config.model'; import { SubmissionFormsModel } from './models/config-submission-forms.model'; -import { SUBMISSION_FORMS_TYPE } from './models/config-type'; /** * Data service to retrieve submission form configuration objects from the REST server. */ -@Injectable() -@dataService(SUBMISSION_FORMS_TYPE) +@Injectable({ providedIn: 'root' }) export class SubmissionFormsConfigDataService extends ConfigDataService { constructor( protected requestService: RequestService, diff --git a/src/app/core/config/submission-uploads-config-data.service.ts b/src/app/core/config/submission-uploads-config-data.service.ts index b98c8b4f91..10d749080e 100644 --- a/src/app/core/config/submission-uploads-config-data.service.ts +++ b/src/app/core/config/submission-uploads-config-data.service.ts @@ -4,20 +4,17 @@ import { Observable } from 'rxjs'; import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { ObjectCacheService } from '../cache/object-cache.service'; -import { dataService } from '../data/base/data-service.decorator'; import { RemoteData } from '../data/remote-data'; import { RequestService } from '../data/request.service'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { ConfigDataService } from './config-data.service'; import { ConfigObject } from './models/config.model'; import { SubmissionUploadsModel } from './models/config-submission-uploads.model'; -import { SUBMISSION_UPLOADS_TYPE } from './models/config-type'; /** * Provides methods to retrieve, from REST server, bitstream access conditions configurations applicable during the submission process. */ -@Injectable() -@dataService(SUBMISSION_UPLOADS_TYPE) +@Injectable({ providedIn: 'root' }) export class SubmissionUploadsConfigDataService extends ConfigDataService { constructor( protected requestService: RequestService, diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts deleted file mode 100644 index 10433caa26..0000000000 --- a/src/app/core/core.module.ts +++ /dev/null @@ -1,455 +0,0 @@ -import { CommonModule } from '@angular/common'; -import { HttpClient } from '@angular/common/http'; -import { - ModuleWithProviders, - NgModule, - Optional, - SkipSelf, -} from '@angular/core'; -import { EffectsModule } from '@ngrx/effects'; -import { - Action, - StoreConfig, - StoreModule, -} from '@ngrx/store'; - -import { environment } from '../../environments/environment'; -import { LdnItemfiltersService } from '../admin/admin-ldn-services/ldn-services-data/ldn-itemfilters-data.service'; -import { LdnServicesService } from '../admin/admin-ldn-services/ldn-services-data/ldn-services-data.service'; -import { Itemfilter } from '../admin/admin-ldn-services/ldn-services-model/ldn-service-itemfilters'; -import { LdnService } from '../admin/admin-ldn-services/ldn-services-model/ldn-services.model'; -import { AdminNotifyMessage } from '../admin/admin-notify-dashboard/models/admin-notify-message.model'; -import { storeModuleConfig } from '../app.reducer'; -import { NotifyRequestsStatus } from '../item-page/simple/notify-requests-status/notify-requests-status.model'; -import { MyDSpaceGuard } from '../my-dspace-page/my-dspace.guard'; -import { Process } from '../process-page/processes/process.model'; -import { Script } from '../process-page/scripts/script.model'; -import { ProfileClaimService } from '../profile-page/profile-claim/profile-claim.service'; -import { isNotEmpty } from '../shared/empty.util'; -import { HostWindowService } from '../shared/host-window.service'; -import { MenuService } from '../shared/menu/menu.service'; -import { EndpointMockingRestService } from '../shared/mocks/dspace-rest/endpoint-mocking-rest.service'; -import { - MOCK_RESPONSE_MAP, - mockResponseMap, - ResponseMapMock, -} from '../shared/mocks/dspace-rest/mocks/response-map.mock'; -import { NotificationsService } from '../shared/notifications/notifications.service'; -import { AccessStatusObject } from '../shared/object-collection/shared/badges/access-status-badge/access-status.model'; -import { IdentifierData } from '../shared/object-list/identifier-data/identifier-data.model'; -import { SelectableListService } from '../shared/object-list/selectable-list/selectable-list.service'; -import { ObjectSelectService } from '../shared/object-select/object-select.service'; -import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model'; -import { SidebarService } from '../shared/sidebar/sidebar.service'; -import { Subscription } from '../shared/subscriptions/models/subscription.model'; -import { CoarNotifyConfigDataService } from '../submission/sections/section-coar-notify/coar-notify-config-data.service'; -import { SubmissionCoarNotifyConfig } from '../submission/sections/section-coar-notify/submission-coar-notify.config'; -import { AuthenticatedGuard } from './auth/authenticated.guard'; -import { AuthStatus } from './auth/models/auth-status.model'; -import { ShortLivedToken } from './auth/models/short-lived-token.model'; -import { TokenResponseParsingService } from './auth/token-response-parsing.service'; -import { BrowseService } from './browse/browse.service'; -import { RemoteDataBuildService } from './cache/builders/remote-data-build.service'; -import { ObjectCacheService } from './cache/object-cache.service'; -import { BulkAccessConditionOptions } from './config/models/bulk-access-condition-options.model'; -import { SubmissionAccessesModel } from './config/models/config-submission-accesses.model'; -import { SubmissionDefinitionsModel } from './config/models/config-submission-definitions.model'; -import { SubmissionFormsModel } from './config/models/config-submission-forms.model'; -import { SubmissionSectionModel } from './config/models/config-submission-section.model'; -import { SubmissionUploadsModel } from './config/models/config-submission-uploads.model'; -import { SubmissionFormsConfigDataService } from './config/submission-forms-config-data.service'; -import { coreEffects } from './core.effects'; -import { coreReducers } from './core.reducers'; -import { CoreState } from './core-state.model'; -import { AccessStatusDataService } from './data/access-status-data.service'; -import { ArrayMoveChangeAnalyzer } from './data/array-move-change-analyzer.service'; -import { BitstreamDataService } from './data/bitstream-data.service'; -import { BitstreamFormatDataService } from './data/bitstream-format-data.service'; -import { CollectionDataService } from './data/collection-data.service'; -import { CommunityDataService } from './data/community-data.service'; -import { ConfigurationDataService } from './data/configuration-data.service'; -import { ContentSourceResponseParsingService } from './data/content-source-response-parsing.service'; -import { DebugResponseParsingService } from './data/debug-response-parsing.service'; -import { DefaultChangeAnalyzer } from './data/default-change-analyzer.service'; -import { DSOChangeAnalyzer } from './data/dso-change-analyzer.service'; -import { DSOResponseParsingService } from './data/dso-response-parsing.service'; -import { DSpaceObjectDataService } from './data/dspace-object-data.service'; -import { EndpointMapResponseParsingService } from './data/endpoint-map-response-parsing.service'; -import { EntityTypeDataService } from './data/entity-type-data.service'; -import { ExternalSourceDataService } from './data/external-source-data.service'; -import { FacetConfigResponseParsingService } from './data/facet-config-response-parsing.service'; -import { FacetValueResponseParsingService } from './data/facet-value-response-parsing.service'; -import { AuthorizationDataService } from './data/feature-authorization/authorization-data.service'; -import { SiteAdministratorGuard } from './data/feature-authorization/feature-authorization-guard/site-administrator.guard'; -import { SiteRegisterGuard } from './data/feature-authorization/feature-authorization-guard/site-register.guard'; -import { FeatureDataService } from './data/feature-authorization/feature-data.service'; -import { FilteredDiscoveryPageResponseParsingService } from './data/filtered-discovery-page-response-parsing.service'; -import { ItemDataService } from './data/item-data.service'; -import { ItemTemplateDataService } from './data/item-template-data.service'; -import { LookupRelationService } from './data/lookup-relation.service'; -import { MetadataFieldDataService } from './data/metadata-field-data.service'; -import { MetadataSchemaDataService } from './data/metadata-schema-data.service'; -import { MyDSpaceResponseParsingService } from './data/mydspace-response-parsing.service'; -import { NotifyRequestsStatusDataService } from './data/notify-services-status-data.service'; -import { ObjectUpdatesService } from './data/object-updates/object-updates.service'; -import { ProcessDataService } from './data/processes/process-data.service'; -import { ScriptDataService } from './data/processes/script-data.service'; -import { RelationshipDataService } from './data/relationship-data.service'; -import { RelationshipTypeDataService } from './data/relationship-type-data.service'; -import { Root } from './data/root.model'; -import { RootDataService } from './data/root-data.service'; -import { SearchResponseParsingService } from './data/search-response-parsing.service'; -import { SiteDataService } from './data/site-data.service'; -import { VersionDataService } from './data/version-data.service'; -import { VersionHistoryDataService } from './data/version-history-data.service'; -import { WorkflowActionDataService } from './data/workflow-action-data.service'; -import { DspaceRestService } from './dspace-rest/dspace-rest.service'; -import { EndUserAgreementService } from './end-user-agreement/end-user-agreement.service'; -import { EndUserAgreementCookieGuard } from './end-user-agreement/end-user-agreement-cookie.guard'; -import { EndUserAgreementCurrentUserGuard } from './end-user-agreement/end-user-agreement-current-user.guard'; -import { EPersonDataService } from './eperson/eperson-data.service'; -import { GroupDataService } from './eperson/group-data.service'; -import { EPerson } from './eperson/models/eperson.model'; -import { Group } from './eperson/models/group.model'; -import { FeedbackDataService } from './feedback/feedback-data.service'; -import { JsonPatchOperationsBuilder } from './json-patch/builder/json-patch-operations-builder'; -import { MetadataService } from './metadata/metadata.service'; -import { MetadataField } from './metadata/metadata-field.model'; -import { MetadataSchema } from './metadata/metadata-schema.model'; -import { SuggestionSource } from './notifications/models/suggestion-source.model'; -import { SuggestionTarget } from './notifications/models/suggestion-target.model'; -import { QualityAssuranceEventObject } from './notifications/qa/models/quality-assurance-event.model'; -import { QualityAssuranceSourceObject } from './notifications/qa/models/quality-assurance-source.model'; -import { QualityAssuranceTopicObject } from './notifications/qa/models/quality-assurance-topic.model'; -import { OrcidHistory } from './orcid/model/orcid-history.model'; -import { OrcidQueue } from './orcid/model/orcid-queue.model'; -import { OrcidAuthService } from './orcid/orcid-auth.service'; -import { OrcidHistoryDataService } from './orcid/orcid-history-data.service'; -import { OrcidQueueDataService } from './orcid/orcid-queue-data.service'; -import { ResearcherProfile } from './profile/model/researcher-profile.model'; -import { ResearcherProfileDataService } from './profile/researcher-profile-data.service'; -import { RegistryService } from './registry/registry.service'; -import { ReloadGuard } from './reload/reload.guard'; -import { ResourcePolicy } from './resource-policy/models/resource-policy.model'; -import { ResourcePolicyDataService } from './resource-policy/resource-policy-data.service'; -import { RoleService } from './roles/role.service'; -import { LinkHeadService } from './services/link-head.service'; -import { ServerResponseService } from './services/server-response.service'; -import { - NativeWindowFactory, - NativeWindowService, -} from './services/window.service'; -import { Authorization } from './shared/authorization.model'; -import { Bitstream } from './shared/bitstream.model'; -import { BitstreamFormat } from './shared/bitstream-format.model'; -import { BrowseDefinition } from './shared/browse-definition.model'; -import { BrowseEntry } from './shared/browse-entry.model'; -import { Bundle } from './shared/bundle.model'; -import { Collection } from './shared/collection.model'; -import { Community } from './shared/community.model'; -import { ConfigurationProperty } from './shared/configuration-property.model'; -import { DSpaceObject } from './shared/dspace-object.model'; -import { ExternalSource } from './shared/external-source.model'; -import { ExternalSourceEntry } from './shared/external-source-entry.model'; -import { Feature } from './shared/feature.model'; -import { FlatBrowseDefinition } from './shared/flat-browse-definition.model'; -import { HALEndpointService } from './shared/hal-endpoint.service'; -import { HierarchicalBrowseDefinition } from './shared/hierarchical-browse-definition.model'; -import { Item } from './shared/item.model'; -import { ItemType } from './shared/item-relationships/item-type.model'; -import { Relationship } from './shared/item-relationships/relationship.model'; -import { RelationshipType } from './shared/item-relationships/relationship-type.model'; -import { ItemRequest } from './shared/item-request.model'; -import { License } from './shared/license.model'; -import { NonHierarchicalBrowseDefinition } from './shared/non-hierarchical-browse-definition'; -import { Registration } from './shared/registration.model'; -import { SearchService } from './shared/search/search.service'; -import { SearchConfigurationService } from './shared/search/search-configuration.service'; -import { SearchFilterService } from './shared/search/search-filter.service'; -import { SearchConfig } from './shared/search/search-filters/search-config.model'; -import { SequenceService } from './shared/sequence.service'; -import { Site } from './shared/site.model'; -import { TemplateItem } from './shared/template-item.model'; -import { UUIDService } from './shared/uuid.service'; -import { ValueListBrowseDefinition } from './shared/value-list-browse-definition.model'; -import { Version } from './shared/version.model'; -import { VersionHistory } from './shared/version-history.model'; -import { UsageReport } from './statistics/models/usage-report.model'; -import { CorrectionTypeDataService } from './submission/correctiontype-data.service'; -import { SubmissionCcLicence } from './submission/models/submission-cc-license.model'; -import { SubmissionCcLicenceUrl } from './submission/models/submission-cc-license-url.model'; -import { WorkflowItem } from './submission/models/workflowitem.model'; -import { WorkspaceItem } from './submission/models/workspaceitem.model'; -import { SubmissionCcLicenseDataService } from './submission/submission-cc-license-data.service'; -import { SubmissionCcLicenseUrlDataService } from './submission/submission-cc-license-url-data.service'; -import { SubmissionDuplicateDataService } from './submission/submission-duplicate-data.service'; -import { SubmissionJsonPatchOperationsService } from './submission/submission-json-patch-operations.service'; -import { SubmissionResponseParsingService } from './submission/submission-response-parsing.service'; -import { SubmissionRestService } from './submission/submission-rest.service'; -import { Vocabulary } from './submission/vocabularies/models/vocabulary.model'; -import { VocabularyEntry } from './submission/vocabularies/models/vocabulary-entry.model'; -import { VocabularyEntryDetail } from './submission/vocabularies/models/vocabulary-entry-detail.model'; -import { VocabularyDataService } from './submission/vocabularies/vocabulary.data.service'; -import { VocabularyService } from './submission/vocabularies/vocabulary.service'; -import { VocabularyEntryDetailsDataService } from './submission/vocabularies/vocabulary-entry-details.data.service'; -import { WorkflowItemDataService } from './submission/workflowitem-data.service'; -import { WorkspaceitemDataService } from './submission/workspaceitem-data.service'; -import { SupervisionOrderDataService } from './supervision-order/supervision-order-data.service'; -import { ClaimedTaskDataService } from './tasks/claimed-task-data.service'; -import { AdvancedWorkflowInfo } from './tasks/models/advanced-workflow-info.model'; -import { ClaimedTask } from './tasks/models/claimed-task-object.model'; -import { PoolTask } from './tasks/models/pool-task-object.model'; -import { RatingAdvancedWorkflowInfo } from './tasks/models/rating-advanced-workflow-info.model'; -import { SelectReviewerAdvancedWorkflowInfo } from './tasks/models/select-reviewer-advanced-workflow-info.model'; -import { TaskObject } from './tasks/models/task-object.model'; -import { WorkflowAction } from './tasks/models/workflow-action-object.model'; -import { PoolTaskDataService } from './tasks/pool-task-data.service'; -import { TaskResponseParsingService } from './tasks/task-response-parsing.service'; - -/** - * When not in production, endpoint responses can be mocked for testing purposes - * If there is no mock version available for the endpoint, the actual REST response will be used just like in production mode - */ -export const restServiceFactory = (mocks: ResponseMapMock, http: HttpClient) => { - if (environment.production) { - return new DspaceRestService(http); - } else { - return new EndpointMockingRestService(mocks, http); - } -}; - -const IMPORTS = [ - CommonModule, - StoreModule.forFeature('core', coreReducers, storeModuleConfig as StoreConfig), - EffectsModule.forFeature(coreEffects), -]; - -const DECLARATIONS = []; - -const EXPORTS = []; - -const PROVIDERS = [ - AuthenticatedGuard, - CommunityDataService, - CollectionDataService, - SiteDataService, - DSOResponseParsingService, - { provide: MOCK_RESPONSE_MAP, useValue: mockResponseMap }, - { provide: DspaceRestService, useFactory: restServiceFactory, deps: [MOCK_RESPONSE_MAP, HttpClient] }, - EPersonDataService, - LinkHeadService, - HALEndpointService, - HostWindowService, - ItemDataService, - SubmissionDuplicateDataService, - MetadataService, - ObjectCacheService, - PaginationComponentOptions, - ResourcePolicyDataService, - RegistryService, - BitstreamFormatDataService, - RemoteDataBuildService, - EndpointMapResponseParsingService, - FacetValueResponseParsingService, - FacetConfigResponseParsingService, - DebugResponseParsingService, - SearchResponseParsingService, - MyDSpaceResponseParsingService, - ServerResponseService, - BrowseService, - AccessStatusDataService, - SubmissionCcLicenseDataService, - SubmissionCcLicenseUrlDataService, - SubmissionFormsConfigDataService, - SubmissionRestService, - SubmissionResponseParsingService, - SubmissionJsonPatchOperationsService, - JsonPatchOperationsBuilder, - UUIDService, - NotificationsService, - WorkspaceitemDataService, - WorkflowItemDataService, - DSpaceObjectDataService, - ConfigurationDataService, - DSOChangeAnalyzer, - DefaultChangeAnalyzer, - ArrayMoveChangeAnalyzer, - ObjectSelectService, - MenuService, - ObjectUpdatesService, - SearchService, - RelationshipDataService, - MyDSpaceGuard, - RoleService, - TaskResponseParsingService, - ClaimedTaskDataService, - PoolTaskDataService, - BitstreamDataService, - EntityTypeDataService, - ContentSourceResponseParsingService, - ItemTemplateDataService, - SearchService, - SidebarService, - SearchFilterService, - SearchFilterService, - SearchConfigurationService, - SelectableListService, - RelationshipTypeDataService, - ExternalSourceDataService, - LookupRelationService, - VersionDataService, - VersionHistoryDataService, - WorkflowActionDataService, - ProcessDataService, - ScriptDataService, - FeatureDataService, - AuthorizationDataService, - SiteAdministratorGuard, - SiteRegisterGuard, - MetadataSchemaDataService, - MetadataFieldDataService, - TokenResponseParsingService, - ReloadGuard, - EndUserAgreementCurrentUserGuard, - EndUserAgreementCookieGuard, - EndUserAgreementService, - RootDataService, - NotificationsService, - FilteredDiscoveryPageResponseParsingService, - { provide: NativeWindowService, useFactory: NativeWindowFactory }, - VocabularyService, - VocabularyDataService, - VocabularyEntryDetailsDataService, - SequenceService, - GroupDataService, - FeedbackDataService, - ResearcherProfileDataService, - ProfileClaimService, - OrcidAuthService, - OrcidQueueDataService, - OrcidHistoryDataService, - SupervisionOrderDataService, - CorrectionTypeDataService, - LdnServicesService, - LdnItemfiltersService, - CoarNotifyConfigDataService, - NotifyRequestsStatusDataService, -]; - -/** - * Declaration needed to make sure all decorator functions are called in time - */ -export const models = - [ - Root, - DSpaceObject, - Bundle, - Bitstream, - BitstreamFormat, - Item, - Site, - Collection, - Community, - EPerson, - Group, - ResourcePolicy, - MetadataSchema, - MetadataField, - License, - WorkflowItem, - WorkspaceItem, - SubmissionCcLicence, - SubmissionCcLicenceUrl, - SubmissionDefinitionsModel, - SubmissionFormsModel, - SubmissionSectionModel, - SubmissionUploadsModel, - AuthStatus, - BrowseEntry, - BrowseDefinition, - NonHierarchicalBrowseDefinition, - FlatBrowseDefinition, - ValueListBrowseDefinition, - HierarchicalBrowseDefinition, - ClaimedTask, - TaskObject, - PoolTask, - Relationship, - RelationshipType, - ItemType, - ExternalSource, - ExternalSourceEntry, - Script, - Process, - Version, - VersionHistory, - WorkflowAction, - AdvancedWorkflowInfo, - RatingAdvancedWorkflowInfo, - SelectReviewerAdvancedWorkflowInfo, - TemplateItem, - Feature, - Authorization, - Registration, - Vocabulary, - VocabularyEntry, - VocabularyEntryDetail, - ConfigurationProperty, - ShortLivedToken, - Registration, - UsageReport, - QualityAssuranceTopicObject, - QualityAssuranceEventObject, - Root, - SearchConfig, - SubmissionAccessesModel, - QualityAssuranceSourceObject, - AccessStatusObject, - ResearcherProfile, - OrcidQueue, - OrcidHistory, - AccessStatusObject, - IdentifierData, - Subscription, - ItemRequest, - BulkAccessConditionOptions, - SuggestionTarget, - SuggestionSource, - LdnService, - Itemfilter, - SubmissionCoarNotifyConfig, - NotifyRequestsStatus, - AdminNotifyMessage, - ]; - -@NgModule({ - imports: [ - ...IMPORTS, - ], - declarations: [ - ...DECLARATIONS, - ], - exports: [ - ...EXPORTS, - ], - providers: [ - ...PROVIDERS, - ], -}) - -export class CoreModule { - static forRoot(): ModuleWithProviders { - return { - ngModule: CoreModule, - providers: [ - ...PROVIDERS, - ], - }; - } - - constructor(@Optional() @SkipSelf() parentModule: CoreModule) { - if (isNotEmpty(parentModule)) { - throw new Error('CoreModule is already loaded. Import it in the AppModule only'); - } - } -} diff --git a/src/app/core/data-services-map.ts b/src/app/core/data-services-map.ts new file mode 100644 index 0000000000..c9ebbc5ffc --- /dev/null +++ b/src/app/core/data-services-map.ts @@ -0,0 +1,139 @@ +import { LazyDataServicesMap } from '../../config/app-config.interface'; +import { + LDN_SERVICE, + LDN_SERVICE_CONSTRAINT_FILTERS, +} from '../admin/admin-ldn-services/ldn-services-model/ldn-service.resource-type'; +import { ADMIN_NOTIFY_MESSAGE } from '../admin/admin-notify-dashboard/models/admin-notify-message.resource-type'; +import { NOTIFYREQUEST } from '../item-page/simple/notify-requests-status/notify-requests-status.resource-type'; +import { PROCESS } from '../process-page/processes/process.resource-type'; +import { SCRIPT } from '../process-page/scripts/script.resource-type'; +import { ACCESS_STATUS } from '../shared/object-collection/shared/badges/access-status-badge/access-status.resource-type'; +import { DUPLICATE } from '../shared/object-list/duplicate-data/duplicate.resource-type'; +import { IDENTIFIERS } from '../shared/object-list/identifier-data/identifier-data.resource-type'; +import { SUBSCRIPTION } from '../shared/subscriptions/models/subscription.resource-type'; +import { SUBMISSION_COAR_NOTIFY_CONFIG } from '../submission/sections/section-coar-notify/section-coar-notify-service.resource-type'; +import { SYSTEMWIDEALERT } from '../system-wide-alert/system-wide-alert.resource-type'; +import { + BULK_ACCESS_CONDITION_OPTIONS, + SUBMISSION_ACCESSES_TYPE, + SUBMISSION_FORMS_TYPE, + SUBMISSION_UPLOADS_TYPE, +} from './config/models/config-type'; +import { ROOT } from './data/root.resource-type'; +import { EPERSON } from './eperson/models/eperson.resource-type'; +import { GROUP } from './eperson/models/group.resource-type'; +import { WORKFLOWITEM } from './eperson/models/workflowitem.resource-type'; +import { WORKSPACEITEM } from './eperson/models/workspaceitem.resource-type'; +import { FEEDBACK } from './feedback/models/feedback.resource-type'; +import { METADATA_FIELD } from './metadata/metadata-field.resource-type'; +import { METADATA_SCHEMA } from './metadata/metadata-schema.resource-type'; +import { QUALITY_ASSURANCE_EVENT_OBJECT } from './notifications/qa/models/quality-assurance-event-object.resource-type'; +import { QUALITY_ASSURANCE_SOURCE_OBJECT } from './notifications/qa/models/quality-assurance-source-object.resource-type'; +import { QUALITY_ASSURANCE_TOPIC_OBJECT } from './notifications/qa/models/quality-assurance-topic-object.resource-type'; +import { SUGGESTION } from './notifications/suggestions/models/suggestion-objects.resource-type'; +import { SUGGESTION_SOURCE } from './notifications/suggestions/models/suggestion-source-object.resource-type'; +import { SUGGESTION_TARGET } from './notifications/suggestions/models/suggestion-target-object.resource-type'; +import { ORCID_HISTORY } from './orcid/model/orcid-history.resource-type'; +import { ORCID_QUEUE } from './orcid/model/orcid-queue.resource-type'; +import { RESEARCHER_PROFILE } from './profile/model/researcher-profile.resource-type'; +import { RESOURCE_POLICY } from './resource-policy/models/resource-policy.resource-type'; +import { AUTHORIZATION } from './shared/authorization.resource-type'; +import { BITSTREAM } from './shared/bitstream.resource-type'; +import { BITSTREAM_FORMAT } from './shared/bitstream-format.resource-type'; +import { BROWSE_DEFINITION } from './shared/browse-definition.resource-type'; +import { BUNDLE } from './shared/bundle.resource-type'; +import { COLLECTION } from './shared/collection.resource-type'; +import { COMMUNITY } from './shared/community.resource-type'; +import { CONFIG_PROPERTY } from './shared/config-property.resource-type'; +import { DSPACE_OBJECT } from './shared/dspace-object.resource-type'; +import { FEATURE } from './shared/feature.resource-type'; +import { ITEM } from './shared/item.resource-type'; +import { ITEM_TYPE } from './shared/item-relationships/item-type.resource-type'; +import { RELATIONSHIP } from './shared/item-relationships/relationship.resource-type'; +import { RELATIONSHIP_TYPE } from './shared/item-relationships/relationship-type.resource-type'; +import { LICENSE } from './shared/license.resource-type'; +import { SITE } from './shared/site.resource-type'; +import { VERSION } from './shared/version.resource-type'; +import { VERSION_HISTORY } from './shared/version-history.resource-type'; +import { USAGE_REPORT } from './statistics/models/usage-report.resource-type'; +import { CorrectionType } from './submission/models/correctiontype.model'; +import { SUBMISSION_CC_LICENSE } from './submission/models/submission-cc-licence.resource-type'; +import { SUBMISSION_CC_LICENSE_URL } from './submission/models/submission-cc-licence-link.resource-type'; +import { + VOCABULARY, + VOCABULARY_ENTRY, + VOCABULARY_ENTRY_DETAIL, +} from './submission/vocabularies/models/vocabularies.resource-type'; +import { SUPERVISION_ORDER } from './supervision-order/models/supervision-order.resource-type'; +import { CLAIMED_TASK } from './tasks/models/claimed-task-object.resource-type'; +import { POOL_TASK } from './tasks/models/pool-task-object.resource-type'; +import { WORKFLOW_ACTION } from './tasks/models/workflow-action-object.resource-type'; + +export const LAZY_DATA_SERVICES: LazyDataServicesMap = new Map([ + [AUTHORIZATION.value, () => import('./data/feature-authorization/authorization-data.service').then(m => m.AuthorizationDataService)], + [BROWSE_DEFINITION.value, () => import('./browse/browse-definition-data.service').then(m => m.BrowseDefinitionDataService)], + [BULK_ACCESS_CONDITION_OPTIONS.value, () => import('./config/bulk-access-config-data.service').then(m => m.BulkAccessConfigDataService)], + [METADATA_SCHEMA.value, () => import('./data/metadata-schema-data.service').then(m => m.MetadataSchemaDataService)], + [SUBMISSION_UPLOADS_TYPE.value, () => import('./config/submission-uploads-config-data.service').then(m => m.SubmissionUploadsConfigDataService)], + [BITSTREAM.value, () => import('./data/bitstream-data.service').then(m => m.BitstreamDataService)], + [SUBMISSION_ACCESSES_TYPE.value, () => import('./config/submission-accesses-config-data.service').then(m => m.SubmissionAccessesConfigDataService)], + [SYSTEMWIDEALERT.value, () => import('./data/system-wide-alert-data.service').then(m => m.SystemWideAlertDataService)], + [USAGE_REPORT.value, () => import('./statistics/usage-report-data.service').then(m => m.UsageReportDataService)], + [ACCESS_STATUS.value, () => import('./data/access-status-data.service').then(m => m.AccessStatusDataService)], + [COLLECTION.value, () => import('./data/collection-data.service').then(m => m.CollectionDataService)], + [CLAIMED_TASK.value, () => import('./tasks/claimed-task-data.service').then(m => m.ClaimedTaskDataService)], + [VOCABULARY_ENTRY.value, () => import('./data/href-only-data.service').then(m => m.HrefOnlyDataService)], + [ITEM_TYPE.value, () => import('./data/href-only-data.service').then(m => m.HrefOnlyDataService)], + [LICENSE.value, () => import('./data/href-only-data.service').then(m => m.HrefOnlyDataService)], + [SUBSCRIPTION.value, () => import('../shared/subscriptions/subscriptions-data.service').then(m => m.SubscriptionsDataService)], + [COMMUNITY.value, () => import('./data/community-data.service').then(m => m.CommunityDataService)], + [VOCABULARY.value, () => import('./submission/vocabularies/vocabulary.data.service').then(m => m.VocabularyDataService)], + [BUNDLE.value, () => import('./data/bundle-data.service').then(m => m.BundleDataService)], + [CONFIG_PROPERTY.value, () => import('./data/configuration-data.service').then(m => m.ConfigurationDataService)], + [POOL_TASK.value, () => import('./tasks/pool-task-data.service').then(m => m.PoolTaskDataService)], + [CLAIMED_TASK.value, () => import('./tasks/claimed-task-data.service').then(m => m.ClaimedTaskDataService)], + [SUPERVISION_ORDER.value, () => import('./supervision-order/supervision-order-data.service').then(m => m.SupervisionOrderDataService)], + [WORKSPACEITEM.value, () => import('./submission/workspaceitem-data.service').then(m => m.WorkspaceitemDataService)], + [WORKFLOWITEM.value, () => import('./submission/workflowitem-data.service').then(m => m.WorkflowItemDataService)], + [VOCABULARY.value, () => import('./submission/vocabularies/vocabulary.data.service').then(m => m.VocabularyDataService)], + [VOCABULARY_ENTRY_DETAIL.value, () => import('./submission/vocabularies/vocabulary-entry-details.data.service').then(m => m.VocabularyEntryDetailsDataService)], + [SUBMISSION_CC_LICENSE_URL.value, () => import('./submission/submission-cc-license-url-data.service').then(m => m.SubmissionCcLicenseUrlDataService)], + [SUBMISSION_CC_LICENSE.value, () => import('./submission/submission-cc-license-data.service').then(m => m.SubmissionCcLicenseDataService)], + [USAGE_REPORT.value, () => import('./statistics/usage-report-data.service').then(m => m.UsageReportDataService)], + [RESOURCE_POLICY.value, () => import('./resource-policy/resource-policy-data.service').then(m => m.ResourcePolicyDataService)], + [RESEARCHER_PROFILE.value, () => import('./profile/researcher-profile-data.service').then(m => m.ResearcherProfileDataService)], + [ORCID_QUEUE.value, () => import('./orcid/orcid-queue-data.service').then(m => m.OrcidQueueDataService)], + [ORCID_HISTORY.value, () => import('./orcid/orcid-history-data.service').then(m => m.OrcidHistoryDataService)], + [FEEDBACK.value, () => import('./feedback/feedback-data.service').then(m => m.FeedbackDataService)], + [GROUP.value, () => import('./eperson/group-data.service').then(m => m.GroupDataService)], + [EPERSON.value, () => import('./eperson/eperson-data.service').then(m => m.EPersonDataService)], + [WORKFLOW_ACTION.value, () => import('./data/workflow-action-data.service').then(m => m.WorkflowActionDataService)], + [VERSION_HISTORY.value, () => import('./data/version-history-data.service').then(m => m.VersionHistoryDataService)], + [SITE.value, () => import('./data/site-data.service').then(m => m.SiteDataService)], + [ROOT.value, () => import('./data/root-data.service').then(m => m.RootDataService)], + [RELATIONSHIP_TYPE.value, () => import('./data/relationship-type-data.service').then(m => m.RelationshipTypeDataService)], + [RELATIONSHIP.value, () => import('./data/relationship-data.service').then(m => m.RelationshipDataService)], + [SCRIPT.value, () => import('./data/processes/script-data.service').then(m => m.ScriptDataService)], + [PROCESS.value, () => import('./data/processes/process-data.service').then(m => m.ProcessDataService)], + [METADATA_FIELD.value, () => import('./data/metadata-field-data.service').then(m => m.MetadataFieldDataService)], + [ITEM.value, () => import('./data/item-data.service').then(m => m.ItemDataService)], + [VERSION.value, () => import('./data/version-data.service').then(m => m.VersionDataService)], + [IDENTIFIERS.value, () => import('./data/identifier-data.service').then(m => m.IdentifierDataService)], + [FEATURE.value, () => import('./data/feature-authorization/authorization-data.service').then(m => m.AuthorizationDataService)], + [DSPACE_OBJECT.value, () => import('./data/dspace-object-data.service').then(m => m.DSpaceObjectDataService)], + [BITSTREAM_FORMAT.value, () => import('./data/bitstream-format-data.service').then(m => m.BitstreamFormatDataService)], + [SUBMISSION_COAR_NOTIFY_CONFIG.value, () => import('../submission/sections/section-coar-notify/coar-notify-config-data.service').then(m => m.CoarNotifyConfigDataService)], + [LDN_SERVICE_CONSTRAINT_FILTERS.value, () => import('../admin/admin-ldn-services/ldn-services-data/ldn-itemfilters-data.service').then(m => m.LdnItemfiltersService)], + [LDN_SERVICE.value, () => import('../admin/admin-ldn-services/ldn-services-data/ldn-services-data.service').then(m => m.LdnServicesService)], + [ADMIN_NOTIFY_MESSAGE.value, () => import('../admin/admin-notify-dashboard/services/admin-notify-messages.service').then(m => m.AdminNotifyMessagesService)], + [SUBMISSION_FORMS_TYPE.value, () => import('./config/submission-forms-config-data.service').then(m => m.SubmissionFormsConfigDataService)], + [NOTIFYREQUEST.value, () => import('./data/notify-services-status-data.service').then(m => m.NotifyRequestsStatusDataService)], + [QUALITY_ASSURANCE_EVENT_OBJECT.value, () => import('./notifications/qa/events/quality-assurance-event-data.service').then(m => m.QualityAssuranceEventDataService)], + [QUALITY_ASSURANCE_SOURCE_OBJECT.value, () => import('./notifications/qa/source/quality-assurance-source-data.service').then(m => m.QualityAssuranceSourceDataService)], + [QUALITY_ASSURANCE_TOPIC_OBJECT.value, () => import('./notifications/qa/topics/quality-assurance-topic-data.service').then(m => m.QualityAssuranceTopicDataService)], + [SUGGESTION.value, () => import('./notifications/suggestions/suggestion-data.service').then(m => m.SuggestionDataService)], + [SUGGESTION_SOURCE.value, () => import('./notifications/suggestions/source/suggestion-source-data.service').then(m => m.SuggestionSourceDataService)], + [SUGGESTION_TARGET.value, () => import('./notifications/suggestions/target/suggestion-target-data.service').then(m => m.SuggestionTargetDataService)], + [DUPLICATE.value, () => import('./submission/submission-duplicate-data.service').then(m => m.SubmissionDuplicateDataService)], + [CorrectionType.type.value, () => import('./submission/correctiontype-data.service').then(m => m.CorrectionTypeDataService)], +]); diff --git a/src/app/core/data/access-status-data.service.spec.ts b/src/app/core/data/access-status-data.service.spec.ts index 1240585027..ed587c26d2 100644 --- a/src/app/core/data/access-status-data.service.spec.ts +++ b/src/app/core/data/access-status-data.service.spec.ts @@ -63,12 +63,12 @@ describe('AccessStatusDataService', () => { /** * Create an AccessStatusDataService used for testing - * @param reponse$ Supply a RemoteData to be returned by the REST API (optional) + * @param response$ Supply a RemoteData to be returned by the REST API (optional) */ - function createService(reponse$?: Observable>) { + function createService(response$?: Observable>) { requestService = getMockRequestService(); - let buildResponse$ = reponse$; - if (hasNoValue(reponse$)) { + let buildResponse$ = response$; + if (hasNoValue(response$)) { buildResponse$ = createSuccessfulRemoteDataObject$({}); } rdbService = jasmine.createSpyObj('rdbService', { diff --git a/src/app/core/data/access-status-data.service.ts b/src/app/core/data/access-status-data.service.ts index 56db0e55e7..6d8acb1c8b 100644 --- a/src/app/core/data/access-status-data.service.ts +++ b/src/app/core/data/access-status-data.service.ts @@ -1,22 +1,19 @@ import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { AccessStatusObject } from 'src/app/shared/object-collection/shared/badges/access-status-badge/access-status.model'; -import { ACCESS_STATUS } from 'src/app/shared/object-collection/shared/badges/access-status-badge/access-status.resource-type'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { ObjectCacheService } from '../cache/object-cache.service'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { Item } from '../shared/item.model'; import { BaseDataService } from './base/base-data.service'; -import { dataService } from './base/data-service.decorator'; import { RemoteData } from './remote-data'; import { RequestService } from './request.service'; /** * Data service responsible for retrieving the access status of Items */ -@Injectable() -@dataService(ACCESS_STATUS) +@Injectable({ providedIn: 'root' }) export class AccessStatusDataService extends BaseDataService { constructor( diff --git a/src/app/core/data/array-move-change-analyzer.service.ts b/src/app/core/data/array-move-change-analyzer.service.ts index 95d86f1032..bd5fc8dedb 100644 --- a/src/app/core/data/array-move-change-analyzer.service.ts +++ b/src/app/core/data/array-move-change-analyzer.service.ts @@ -7,7 +7,7 @@ import { hasValue } from '../../shared/empty.util'; /** * A class to determine move operations between two arrays */ -@Injectable() +@Injectable({ providedIn: 'root' }) export class ArrayMoveChangeAnalyzer { /** diff --git a/src/app/core/data/base/base-data.service.spec.ts b/src/app/core/data/base/base-data.service.spec.ts index 3f44ad5e5a..6f32395667 100644 --- a/src/app/core/data/base/base-data.service.spec.ts +++ b/src/app/core/data/base/base-data.service.spec.ts @@ -5,6 +5,7 @@ * * http://www.dspace.org/license/ */ +// eslint-disable-next-line max-classes-per-file import { fakeAsync, tick, @@ -26,12 +27,20 @@ import { HALEndpointServiceStub } from '../../../shared/testing/hal-endpoint-ser import { ObjectCacheServiceStub } from '../../../shared/testing/object-cache-service.stub'; import { createPaginatedList } from '../../../shared/testing/utils.test'; import { followLink } from '../../../shared/utils/follow-link-config.model'; +import { + link, + typedObject, +} from '../../cache/builders/build-decorators'; import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service'; import { ObjectCacheEntry } from '../../cache/object-cache.reducer'; import { ObjectCacheService } from '../../cache/object-cache.service'; +import { BITSTREAM } from '../../shared/bitstream.resource-type'; +import { COLLECTION } from '../../shared/collection.resource-type'; import { HALEndpointService } from '../../shared/hal-endpoint.service'; import { HALLink } from '../../shared/hal-link.model'; +import { ResourceType } from '../../shared/resource-type'; import { FindListOptions } from '../find-list-options.model'; +import { PaginatedList } from '../paginated-list.model'; import { RemoteData } from '../remote-data'; import { RequestService } from '../request.service'; import { RequestEntryState } from '../request-entry-state.model'; @@ -56,6 +65,25 @@ class TestService extends BaseDataService { } } +@typedObject +class BaseData { + static type = new ResourceType('test'); + + foo: string; + + _links: { + followLink1: HALLink; + followLink2: HALLink[]; + self: HALLink; + }; + + @link(COLLECTION) + followLink1: Observable; + + @link(BITSTREAM, true, 'followLink2') + followLink2CustomVariableName: Observable>; +} + describe('BaseDataService', () => { let service: TestService; let requestService; @@ -65,8 +93,9 @@ describe('BaseDataService', () => { let selfLink; let linksToFollow; let testScheduler; - let remoteDataMocks: { [responseType: string]: RemoteData }; - let remoteDataPageMocks: { [responseType: string]: RemoteData }; + let remoteDataTimestamp: number; + let remoteDataMocks: { [responseType: string]: RemoteData }; + let remoteDataPageMocks: { [responseType: string]: RemoteData> }; function initTestService(): TestService { requestService = getMockRequestService(); @@ -85,12 +114,14 @@ describe('BaseDataService', () => { expect(actual).toEqual(expected); }); - const timeStamp = new Date().getTime(); + // The response's lastUpdated equals the time of 60 seconds after the test started, ensuring they are not perceived + // as cached values. + remoteDataTimestamp = new Date().getTime() + 60 * 1000; const msToLive = 15 * 60 * 1000; - const payload = { + const payload: BaseData = Object.assign(new BaseData(), { foo: 'bar', - followLink1: {}, - followLink2: {}, + followLink1: observableOf({}), + followLink2CustomVariableName: observableOf(createPaginatedList()), _links: { self: Object.assign(new HALLink(), { href: 'self-test-link', @@ -107,27 +138,27 @@ describe('BaseDataService', () => { }), ], }, - }; + }); const statusCodeSuccess = 200; const statusCodeError = 404; const errorMessage = 'not found'; remoteDataMocks = { - RequestPending: new RemoteData(undefined, msToLive, timeStamp, RequestEntryState.RequestPending, undefined, undefined, undefined), - ResponsePending: new RemoteData(undefined, msToLive, timeStamp, RequestEntryState.ResponsePending, undefined, undefined, undefined), - ResponsePendingStale: new RemoteData(undefined, msToLive, timeStamp, RequestEntryState.ResponsePendingStale, undefined, undefined, undefined), - Success: new RemoteData(timeStamp, msToLive, timeStamp, RequestEntryState.Success, undefined, payload, statusCodeSuccess), - SuccessStale: new RemoteData(timeStamp, msToLive, timeStamp, RequestEntryState.SuccessStale, undefined, payload, statusCodeSuccess), - Error: new RemoteData(timeStamp, msToLive, timeStamp, RequestEntryState.Error, errorMessage, undefined, statusCodeError), - ErrorStale: new RemoteData(timeStamp, msToLive, timeStamp, RequestEntryState.ErrorStale, errorMessage, undefined, statusCodeError), + RequestPending: new RemoteData(undefined, msToLive, remoteDataTimestamp, RequestEntryState.RequestPending, undefined, undefined, undefined), + ResponsePending: new RemoteData(undefined, msToLive, remoteDataTimestamp, RequestEntryState.ResponsePending, undefined, undefined, undefined), + ResponsePendingStale: new RemoteData(undefined, msToLive, remoteDataTimestamp, RequestEntryState.ResponsePendingStale, undefined, undefined, undefined), + Success: new RemoteData(remoteDataTimestamp, msToLive, remoteDataTimestamp, RequestEntryState.Success, undefined, payload, statusCodeSuccess), + SuccessStale: new RemoteData(remoteDataTimestamp, msToLive, remoteDataTimestamp, RequestEntryState.SuccessStale, undefined, payload, statusCodeSuccess), + Error: new RemoteData(remoteDataTimestamp, msToLive, remoteDataTimestamp, RequestEntryState.Error, errorMessage, undefined, statusCodeError), + ErrorStale: new RemoteData(remoteDataTimestamp, msToLive, remoteDataTimestamp, RequestEntryState.ErrorStale, errorMessage, undefined, statusCodeError), }; remoteDataPageMocks = { - RequestPending: new RemoteData(undefined, msToLive, timeStamp, RequestEntryState.RequestPending, undefined, undefined, undefined), - ResponsePending: new RemoteData(undefined, msToLive, timeStamp, RequestEntryState.ResponsePending, undefined, undefined, undefined), - ResponsePendingStale: new RemoteData(undefined, msToLive, timeStamp, RequestEntryState.ResponsePendingStale, undefined, undefined, undefined), - Success: new RemoteData(timeStamp, msToLive, timeStamp, RequestEntryState.Success, undefined, createPaginatedList([payload]), statusCodeSuccess), - SuccessStale: new RemoteData(timeStamp, msToLive, timeStamp, RequestEntryState.SuccessStale, undefined, createPaginatedList([payload]), statusCodeSuccess), - Error: new RemoteData(timeStamp, msToLive, timeStamp, RequestEntryState.Error, errorMessage, undefined, statusCodeError), - ErrorStale: new RemoteData(timeStamp, msToLive, timeStamp, RequestEntryState.ErrorStale, errorMessage, undefined, statusCodeError), + RequestPending: new RemoteData(undefined, msToLive, remoteDataTimestamp, RequestEntryState.RequestPending, undefined, undefined, undefined), + ResponsePending: new RemoteData(undefined, msToLive, remoteDataTimestamp, RequestEntryState.ResponsePending, undefined, undefined, undefined), + ResponsePendingStale: new RemoteData(undefined, msToLive, remoteDataTimestamp, RequestEntryState.ResponsePendingStale, undefined, undefined, undefined), + Success: new RemoteData(remoteDataTimestamp, msToLive, remoteDataTimestamp, RequestEntryState.Success, undefined, createPaginatedList([payload]), statusCodeSuccess), + SuccessStale: new RemoteData(remoteDataTimestamp, msToLive, remoteDataTimestamp, RequestEntryState.SuccessStale, undefined, createPaginatedList([payload]), statusCodeSuccess), + Error: new RemoteData(remoteDataTimestamp, msToLive, remoteDataTimestamp, RequestEntryState.Error, errorMessage, undefined, statusCodeError), + ErrorStale: new RemoteData(remoteDataTimestamp, msToLive, remoteDataTimestamp, RequestEntryState.ErrorStale, errorMessage, undefined, statusCodeError), }; return new TestService( @@ -361,11 +392,15 @@ describe('BaseDataService', () => { spyOn(service as any, 'reRequestStaleRemoteData').and.callFake(() => (source) => source); }); - - it(`should not emit a cached completed RemoteData, but only start emitting after the state first changes to RequestPending`, () => { + it('should not emit a cached completed RemoteData', () => { + // Old cached value from 1 minute before the test started + const oldCachedSucceededData: RemoteData = Object.assign({}, remoteDataPageMocks.Success, { + timeCompleted: remoteDataTimestamp - 2 * 60 * 1000, + lastUpdated: remoteDataTimestamp - 2 * 60 * 1000, + } as RemoteData); testScheduler.run(({ cold, expectObservable }) => { spyOn(rdbService, 'buildSingle').and.returnValue(cold('a-b-c-d-e', { - a: remoteDataMocks.Success, + a: oldCachedSucceededData, b: remoteDataMocks.RequestPending, c: remoteDataMocks.ResponsePending, d: remoteDataMocks.Success, @@ -383,6 +418,22 @@ describe('BaseDataService', () => { }); }); + it('should emit the first completed RemoteData since the request was made', () => { + testScheduler.run(({ cold, expectObservable }) => { + spyOn(rdbService, 'buildSingle').and.returnValue(cold('a-b', { + a: remoteDataMocks.Success, + b: remoteDataMocks.SuccessStale, + })); + const expected = 'a-b'; + const values = { + a: remoteDataMocks.Success, + b: remoteDataMocks.SuccessStale, + }; + + expectObservable(service.findByHref(selfLink, false, true, ...linksToFollow)).toBe(expected, values); + }); + }); + it(`should not emit a cached stale RemoteData, but only start emitting after the state first changes to RequestPending`, () => { testScheduler.run(({ cold, expectObservable }) => { spyOn(rdbService, 'buildSingle').and.returnValue(cold('a-b-c-d-e-f-g', { @@ -411,17 +462,12 @@ describe('BaseDataService', () => { it('should link all the followLinks of a cached object by calling addDependency', () => { spyOn(objectCache, 'addDependency').and.callThrough(); testScheduler.run(({ cold, expectObservable, flush }) => { - spyOn(rdbService, 'buildSingle').and.returnValue(cold('a-b-c-d', { + spyOn(rdbService, 'buildSingle').and.returnValue(cold('a', { a: remoteDataMocks.Success, - b: remoteDataMocks.RequestPending, - c: remoteDataMocks.ResponsePending, - d: remoteDataMocks.Success, })); - const expected = '--b-c-d'; + const expected = 'a'; const values = { - b: remoteDataMocks.RequestPending, - c: remoteDataMocks.ResponsePending, - d: remoteDataMocks.Success, + a: remoteDataMocks.Success, }; expectObservable(service.findByHref(selfLink, false, false, ...linksToFollow)).toBe(expected, values); @@ -570,11 +616,15 @@ describe('BaseDataService', () => { spyOn(service as any, 'reRequestStaleRemoteData').and.callFake(() => (source) => source); }); - - it(`should not emit a cached completed RemoteData, but only start emitting after the state first changes to RequestPending`, () => { + it('should not emit a cached completed RemoteData', () => { testScheduler.run(({ cold, expectObservable }) => { + // Old cached value from 1 minute before the test started + const oldCachedSucceededData: RemoteData = Object.assign({}, remoteDataPageMocks.Success, { + timeCompleted: remoteDataTimestamp - 2 * 60 * 1000, + lastUpdated: remoteDataTimestamp - 2 * 60 * 1000, + } as RemoteData); spyOn(rdbService, 'buildList').and.returnValue(cold('a-b-c-d-e', { - a: remoteDataPageMocks.Success, + a: oldCachedSucceededData, b: remoteDataPageMocks.RequestPending, c: remoteDataPageMocks.ResponsePending, d: remoteDataPageMocks.Success, @@ -592,6 +642,22 @@ describe('BaseDataService', () => { }); }); + it('should emit the first completed RemoteData since the request was made', () => { + testScheduler.run(({ cold, expectObservable }) => { + spyOn(rdbService, 'buildList').and.returnValue(cold('a-b', { + a: remoteDataPageMocks.Success, + b: remoteDataPageMocks.SuccessStale, + })); + const expected = 'a-b'; + const values = { + a: remoteDataPageMocks.Success, + b: remoteDataPageMocks.SuccessStale, + }; + + expectObservable(service.findListByHref(selfLink, findListOptions, false, true, ...linksToFollow)).toBe(expected, values); + }); + }); + it(`should not emit a cached stale RemoteData, but only start emitting after the state first changes to RequestPending`, () => { testScheduler.run(({ cold, expectObservable }) => { spyOn(rdbService, 'buildList').and.returnValue(cold('a-b-c-d-e-f-g', { diff --git a/src/app/core/data/base/base-data.service.ts b/src/app/core/data/base/base-data.service.ts index d09ee21ee0..df4e9b7a48 100644 --- a/src/app/core/data/base/base-data.service.ts +++ b/src/app/core/data/base/base-data.service.ts @@ -28,11 +28,16 @@ import { isNotEmptyOperator, } from '../../../shared/empty.util'; import { FollowLinkConfig } from '../../../shared/utils/follow-link-config.model'; +import { + getLinkDefinition, + LinkDefinition, +} from '../../cache/builders/build-decorators'; import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service'; import { CacheableObject } from '../../cache/cacheable-object.model'; import { RequestParam } from '../../cache/models/request-param.model'; import { ObjectCacheEntry } from '../../cache/object-cache.reducer'; import { ObjectCacheService } from '../../cache/object-cache.service'; +import { GenericConstructor } from '../../shared/generic-constructor'; import { HALEndpointService } from '../../shared/hal-endpoint.service'; import { HALLink } from '../../shared/hal-link.model'; import { getFirstCompletedRemoteData } from '../../shared/operators'; @@ -285,6 +290,7 @@ export class BaseDataService implements HALDataServic map((href: string) => this.buildHrefFromFindOptions(href, {}, [], ...linksToFollow)), ); + const startTime: number = new Date().getTime(); this.createAndSendGetRequest(requestHref$, useCachedVersionIfAvailable); const response$: Observable> = this.rdbService.buildSingle(requestHref$, ...linksToFollow).pipe( @@ -292,7 +298,7 @@ export class BaseDataService implements HALDataServic // call it isn't immediately returned, but we wait until the remote data for the new request // is created. If useCachedVersionIfAvailable is false it also ensures you don't get a // cached completed object - skipWhile((rd: RemoteData) => rd.isStale || (!useCachedVersionIfAvailable && rd.hasCompleted)), + skipWhile((rd: RemoteData) => rd.isStale || (!useCachedVersionIfAvailable && rd.lastUpdated < startTime)), this.reRequestStaleRemoteData(reRequestOnStale, () => this.findByHref(href$, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow)), ); @@ -300,9 +306,10 @@ export class BaseDataService implements HALDataServic // Ensure all followLinks from the cached object are automatically invalidated when invalidating the cached object tap((remoteDataObject: RemoteData) => { if (hasValue(remoteDataObject?.payload?._links)) { - for (const followLinkName of Object.keys(remoteDataObject.payload._links)) { - // only add the followLinks if they are embedded - if (hasValue(remoteDataObject.payload[followLinkName]) && followLinkName !== 'self') { + for (const followLinkName of Object.keys(remoteDataObject.payload._links) as (keyof typeof remoteDataObject.payload._links)[]) { + // only add the followLinks if they are embedded, and we get only links from the linkMap with the correct name + const linkDefinition: LinkDefinition = getLinkDefinition(remoteDataObject.payload.constructor as GenericConstructor, followLinkName); + if (linkDefinition?.propertyName && hasValue(remoteDataObject.payload[linkDefinition.propertyName]) && followLinkName !== 'self') { // followLink can be either an individual HALLink or a HALLink[] const followLinksList: HALLink[] = [].concat(remoteDataObject.payload._links[followLinkName]); for (const individualFollowLink of followLinksList) { @@ -338,6 +345,7 @@ export class BaseDataService implements HALDataServic map((href: string) => this.buildHrefFromFindOptions(href, options, [], ...linksToFollow)), ); + const startTime: number = new Date().getTime(); this.createAndSendGetRequest(requestHref$, useCachedVersionIfAvailable); const response$: Observable>> = this.rdbService.buildList(requestHref$, ...linksToFollow).pipe( @@ -345,7 +353,7 @@ export class BaseDataService implements HALDataServic // call it isn't immediately returned, but we wait until the remote data for the new request // is created. If useCachedVersionIfAvailable is false it also ensures you don't get a // cached completed object - skipWhile((rd: RemoteData>) => rd.isStale || (!useCachedVersionIfAvailable && rd.hasCompleted)), + skipWhile((rd: RemoteData>) => rd.isStale || (!useCachedVersionIfAvailable && rd.lastUpdated < startTime)), this.reRequestStaleRemoteData(reRequestOnStale, () => this.findListByHref(href$, options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow)), ); @@ -355,9 +363,10 @@ export class BaseDataService implements HALDataServic if (hasValue(remoteDataObject?.payload?.page)) { for (const object of remoteDataObject.payload.page) { if (hasValue(object?._links)) { - for (const followLinkName of Object.keys(object._links)) { - // only add the followLinks if they are embedded - if (hasValue(object[followLinkName]) && followLinkName !== 'self') { + for (const followLinkName of Object.keys(object._links) as (keyof typeof object._links)[]) { + // only add the followLinks if they are embedded, and we get only links from the linkMap with the correct name + const linkDefinition: LinkDefinition> = getLinkDefinition(object.constructor as GenericConstructor>, followLinkName); + if (linkDefinition?.propertyName && followLinkName !== 'self' && hasValue(object[linkDefinition.propertyName])) { // followLink can be either an individual HALLink or a HALLink[] const followLinksList: HALLink[] = [].concat(object._links[followLinkName]); for (const individualFollowLink of followLinksList) { diff --git a/src/app/core/data/base/data-service.decorator.spec.ts b/src/app/core/data/base/data-service.decorator.spec.ts deleted file mode 100644 index 296371be69..0000000000 --- a/src/app/core/data/base/data-service.decorator.spec.ts +++ /dev/null @@ -1,60 +0,0 @@ -/* eslint-disable max-classes-per-file */ -/** - * 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 { v4 as uuidv4 } from 'uuid'; - -import { ResourceType } from '../../shared/resource-type'; -import { BaseDataService } from './base-data.service'; -import { - dataService, - getDataServiceFor, -} from './data-service.decorator'; -import { HALDataService } from './hal-data-service.interface'; - -class TestService extends BaseDataService { -} - -class AnotherTestService implements HALDataService { - public findListByHref(href$, findListOptions, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow): any { - return undefined; - } - - public findByHref(href$, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow): any { - return undefined; - } -} - -let testType; - -describe('@dataService/getDataServiceFor', () => { - beforeEach(() => { - testType = new ResourceType(`testType-${uuidv4()}`); - }); - - it('should register a resourcetype for a dataservice', () => { - dataService(testType)(TestService); - expect(getDataServiceFor(testType)).toBe(TestService); - }); - - describe(`when the resource type isn't specified`, () => { - it(`should throw an error`, () => { - expect(() => { - dataService(undefined)(TestService); - }).toThrow(); - }); - }); - - describe(`when there already is a registered dataservice for a resourcetype`, () => { - it(`should throw an error`, () => { - dataService(testType)(TestService); - expect(() => { - dataService(testType)(AnotherTestService); - }).toThrow(); - }); - }); -}); diff --git a/src/app/core/data/base/data-service.decorator.ts b/src/app/core/data/base/data-service.decorator.ts deleted file mode 100644 index 600fb5e3e3..0000000000 --- a/src/app/core/data/base/data-service.decorator.ts +++ /dev/null @@ -1,55 +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 { InjectionToken } from '@angular/core'; - -import { - hasNoValue, - hasValue, -} from '../../../shared/empty.util'; -import { CacheableObject } from '../../cache/cacheable-object.model'; -import { GenericConstructor } from '../../shared/generic-constructor'; -import { ResourceType } from '../../shared/resource-type'; -import { HALDataService } from './hal-data-service.interface'; - -export const DATA_SERVICE_FACTORY = new InjectionToken<(resourceType: ResourceType) => GenericConstructor>>('getDataServiceFor', { - providedIn: 'root', - factory: () => getDataServiceFor, -}); -const dataServiceMap = new Map(); - -/** - * A class decorator to indicate that this class is a data service for a given HAL resource type. - * - * In most cases, a data service should extend {@link BaseDataService}. - * At the very least it must implement {@link HALDataService} in order for it to work with {@link LinkService}. - * - * @param resourceType the resource type the class is a dataservice for - */ -export function dataService(resourceType: ResourceType) { - return (target: GenericConstructor>): void => { - if (hasNoValue(resourceType)) { - throw new Error(`Invalid @dataService annotation on ${target}, resourceType needs to be defined`); - } - const existingDataservice = dataServiceMap.get(resourceType.value); - - if (hasValue(existingDataservice)) { - throw new Error(`Multiple dataservices for ${resourceType.value}: ${existingDataservice} and ${target}`); - } - - dataServiceMap.set(resourceType.value, target); - }; -} - -/** - * Return the dataservice matching the given resource type - * - * @param resourceType the resource type you want the matching dataservice for - */ -export function getDataServiceFor(resourceType: ResourceType): GenericConstructor> { - return dataServiceMap.get(resourceType.value); -} diff --git a/src/app/core/data/base/delete-data.spec.ts b/src/app/core/data/base/delete-data.spec.ts index 53d651402f..f3fa72a285 100644 --- a/src/app/core/data/base/delete-data.spec.ts +++ b/src/app/core/data/base/delete-data.spec.ts @@ -209,6 +209,11 @@ describe('DeleteDataImpl', () => { method: RestRequestMethod.DELETE, href: 'some-href?copyVirtualMetadata=a©VirtualMetadata=b©VirtualMetadata=c', })); + + const callback = (rdbService.buildFromRequestUUIDAndAwait as jasmine.Spy).calls.argsFor(0)[1]; + callback(); + expect(service.invalidateByHref).toHaveBeenCalledWith('some-href'); + done(); }); }); diff --git a/src/app/core/data/base/delete-data.ts b/src/app/core/data/base/delete-data.ts index e758ee40fa..c47a6af847 100644 --- a/src/app/core/data/base/delete-data.ts +++ b/src/app/core/data/base/delete-data.ts @@ -75,15 +75,16 @@ export class DeleteDataImpl extends IdentifiableDataS deleteByHref(href: string, copyVirtualMetadata?: string[]): Observable> { const requestId = this.requestService.generateRequestId(); + let deleteHref: string = href; if (copyVirtualMetadata) { copyVirtualMetadata.forEach((id) => - href += (href.includes('?') ? '&' : '?') + deleteHref += (deleteHref.includes('?') ? '&' : '?') + 'copyVirtualMetadata=' + id, ); } - const request = new DeleteRequest(requestId, href); + const request = new DeleteRequest(requestId, deleteHref); if (hasValue(this.responseMsToLive)) { request.responseMsToLive = this.responseMsToLive; } diff --git a/src/app/core/data/base/identifiable-data.service.spec.ts b/src/app/core/data/base/identifiable-data.service.spec.ts index 528d6c4945..9281a5c0eb 100644 --- a/src/app/core/data/base/identifiable-data.service.spec.ts +++ b/src/app/core/data/base/identifiable-data.service.spec.ts @@ -5,10 +5,7 @@ * * http://www.dspace.org/license/ */ -import { - Observable, - of as observableOf, -} from 'rxjs'; +import { of as observableOf } from 'rxjs'; import { TestScheduler } from 'rxjs/testing'; import { getMockRemoteDataBuildService } from '../../../shared/mocks/remote-data-build.service.mock'; @@ -18,14 +15,14 @@ import { followLink } from '../../../shared/utils/follow-link-config.model'; import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service'; import { ObjectCacheService } from '../../cache/object-cache.service'; import { HALEndpointService } from '../../shared/hal-endpoint.service'; -import { FindListOptions } from '../find-list-options.model'; import { RemoteData } from '../remote-data'; import { RequestService } from '../request.service'; import { RequestEntryState } from '../request-entry-state.model'; import { EMBED_SEPARATOR } from './base-data.service'; import { IdentifiableDataService } from './identifiable-data.service'; -const endpoint = 'https://rest.api/core'; +const base = 'https://rest.api/core'; +const endpoint = 'test'; class TestService extends IdentifiableDataService { constructor( @@ -34,11 +31,7 @@ class TestService extends IdentifiableDataService { protected objectCache: ObjectCacheService, protected halService: HALEndpointService, ) { - super(undefined, requestService, rdbService, objectCache, halService); - } - - public getBrowseEndpoint(options: FindListOptions = {}, linkPath: string = this.linkPath): Observable { - return observableOf(endpoint); + super(endpoint, requestService, rdbService, objectCache, halService); } } @@ -55,7 +48,7 @@ describe('IdentifiableDataService', () => { function initTestService(): TestService { requestService = getMockRequestService(); - halService = new HALEndpointServiceStub('url') as any; + halService = new HALEndpointServiceStub(base) as any; rdbService = getMockRemoteDataBuildService(); objectCache = { @@ -147,4 +140,12 @@ describe('IdentifiableDataService', () => { expect(result).toEqual(expected); }); }); + + describe('invalidateById', () => { + it('should invalidate the correct resource by href', () => { + spyOn(service, 'invalidateByHref').and.returnValue(observableOf(true)); + service.invalidateById('123'); + expect(service.invalidateByHref).toHaveBeenCalledWith(`${base}/${endpoint}/123`); + }); + }); }); diff --git a/src/app/core/data/base/identifiable-data.service.ts b/src/app/core/data/base/identifiable-data.service.ts index da3167903e..ba368d21ca 100644 --- a/src/app/core/data/base/identifiable-data.service.ts +++ b/src/app/core/data/base/identifiable-data.service.ts @@ -6,7 +6,11 @@ * http://www.dspace.org/license/ */ import { Observable } from 'rxjs'; -import { map } from 'rxjs/operators'; +import { + map, + switchMap, + take, +} from 'rxjs/operators'; import { FollowLinkConfig } from '../../../shared/utils/follow-link-config.model'; import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service'; @@ -81,4 +85,19 @@ export class IdentifiableDataService extends BaseData return this.getEndpoint().pipe( map((endpoint: string) => this.getIDHref(endpoint, resourceID, ...linksToFollow))); } + + /** + * Invalidate a cached resource by its identifier + * @param resourceID the identifier of the resource to invalidate + */ + invalidateById(resourceID: string): Observable { + const ok$ = this.getIDHrefObs(resourceID).pipe( + take(1), + switchMap((href) => this.invalidateByHref(href)), + ); + + ok$.subscribe(); + + return ok$; + } } diff --git a/src/app/core/data/bitstream-data.service.spec.ts b/src/app/core/data/bitstream-data.service.spec.ts index 95fe5f593f..6aa75065d4 100644 --- a/src/app/core/data/bitstream-data.service.spec.ts +++ b/src/app/core/data/bitstream-data.service.spec.ts @@ -16,6 +16,7 @@ import { NotificationsService } from '../../shared/notifications/notifications.s import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub'; import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { RequestParam } from '../cache/models/request-param.model'; import { ObjectCacheService } from '../cache/object-cache.service'; import { Bitstream } from '../shared/bitstream.model'; import { BitstreamFormat } from '../shared/bitstream-format.model'; @@ -138,27 +139,27 @@ describe('BitstreamDataService', () => { describe('findPrimaryBitstreamByItemAndName', () => { it('should return primary bitstream', () => { - const exprected$ = cold('(a|)', { a: bitstream1 } ); + const expected$ = cold('(a|)', { a: bitstream1 } ); const bundle = Object.assign(new Bundle(), { primaryBitstream: observableOf(createSuccessfulRemoteDataObject(bitstream1)), }); spyOn(bundleDataService, 'findByItemAndName').and.returnValue(observableOf(createSuccessfulRemoteDataObject(bundle))); - expect(service.findPrimaryBitstreamByItemAndName(ItemMock, 'ORIGINAL')).toBeObservable(exprected$); + expect(service.findPrimaryBitstreamByItemAndName(ItemMock, 'ORIGINAL')).toBeObservable(expected$); }); it('should return null if primary bitstream has not be succeeded ', () => { - const exprected$ = cold('(a|)', { a: null } ); + const expected$ = cold('(a|)', { a: null } ); const bundle = Object.assign(new Bundle(), { primaryBitstream: observableOf(createFailedRemoteDataObject()), }); spyOn(bundleDataService, 'findByItemAndName').and.returnValue(observableOf(createSuccessfulRemoteDataObject(bundle))); - expect(service.findPrimaryBitstreamByItemAndName(ItemMock, 'ORIGINAL')).toBeObservable(exprected$); + expect(service.findPrimaryBitstreamByItemAndName(ItemMock, 'ORIGINAL')).toBeObservable(expected$); }); it('should return EMPTY if nothing where found', () => { - const exprected$ = cold('(|)', {} ); + const expected$ = cold('(|)', {} ); spyOn(bundleDataService, 'findByItemAndName').and.returnValue(observableOf(createFailedRemoteDataObject())); - expect(service.findPrimaryBitstreamByItemAndName(ItemMock, 'ORIGINAL')).toBeObservable(exprected$); + expect(service.findPrimaryBitstreamByItemAndName(ItemMock, 'ORIGINAL')).toBeObservable(expected$); }); }); @@ -176,4 +177,30 @@ describe('BitstreamDataService', () => { expect(service.invalidateByHref).toHaveBeenCalledWith('fake-bitstream2-self'); }); }); + + describe('findByItemHandle', () => { + it('should encode the filename correctly in the search parameters', () => { + const handle = '123456789/1234'; + const sequenceId = '5'; + const filename = 'file with spaces.pdf'; + const searchParams = [ + new RequestParam('handle', handle), + new RequestParam('sequenceId', sequenceId), + new RequestParam('filename', filename), + ]; + const linksToFollow: FollowLinkConfig[] = []; + + spyOn(service as any, 'getSearchByHref').and.callThrough(); + + service.getSearchByHref('byItemHandle', { searchParams }, ...linksToFollow).subscribe((href) => { + expect(service.getSearchByHref).toHaveBeenCalledWith( + 'byItemHandle', + { searchParams }, + ...linksToFollow, + ); + + expect(href).toBe(`${url}/bitstreams/search/byItemHandle?handle=123456789%2F1234&sequenceId=5&filename=file%20with%20spaces.pdf`); + }); + }); + }); }); diff --git a/src/app/core/data/bitstream-data.service.ts b/src/app/core/data/bitstream-data.service.ts index bc89a54649..9455a456fa 100644 --- a/src/app/core/data/bitstream-data.service.ts +++ b/src/app/core/data/bitstream-data.service.ts @@ -28,7 +28,6 @@ import { RequestParam } from '../cache/models/request-param.model'; import { ObjectCacheService } from '../cache/object-cache.service'; import { HttpOptions } from '../dspace-rest/dspace-rest.service'; import { Bitstream } from '../shared/bitstream.model'; -import { BITSTREAM } from '../shared/bitstream.resource-type'; import { BitstreamFormat } from '../shared/bitstream-format.model'; import { Bundle } from '../shared/bundle.model'; import { HALEndpointService } from '../shared/hal-endpoint.service'; @@ -37,7 +36,6 @@ import { NoContent } from '../shared/NoContent.model'; import { getFirstCompletedRemoteData } from '../shared/operators'; import { PageInfo } from '../shared/page-info.model'; import { sendRequest } from '../shared/request.operators'; -import { dataService } from './base/data-service.decorator'; import { DeleteData, DeleteDataImpl, @@ -70,10 +68,7 @@ import { RestRequestMethod } from './rest-request-method'; /** * A service to retrieve {@link Bitstream}s from the REST API */ -@Injectable({ - providedIn: 'root', -}) -@dataService(BITSTREAM) +@Injectable({ providedIn: 'root' }) export class BitstreamDataService extends IdentifiableDataService implements SearchData, PatchData, DeleteData { private searchData: SearchDataImpl; private patchData: PatchDataImpl; @@ -246,11 +241,12 @@ export class BitstreamDataService extends IdentifiableDataService imp * no valid cached version. Defaults to true * @param reRequestOnStale Whether or not the request should automatically be re- * requested after the response becomes stale + * @param options the {@link FindListOptions} for the request * @return {Observable} - * Return an observable that constains primary bitstream information or null + * Return an observable that contains primary bitstream information or null */ - public findPrimaryBitstreamByItemAndName(item: Item, bundleName: string, useCachedVersionIfAvailable = true, reRequestOnStale = true): Observable { - return this.bundleService.findByItemAndName(item, bundleName, useCachedVersionIfAvailable, reRequestOnStale, followLink('primaryBitstream')).pipe( + public findPrimaryBitstreamByItemAndName(item: Item, bundleName: string, useCachedVersionIfAvailable = true, reRequestOnStale = true, options?: FindListOptions): Observable { + return this.bundleService.findByItemAndName(item, bundleName, useCachedVersionIfAvailable, reRequestOnStale, options, followLink('primaryBitstream')).pipe( getFirstCompletedRemoteData(), switchMap((rd: RemoteData) => { if (!rd.hasSucceeded) { diff --git a/src/app/core/data/bitstream-format-data.service.ts b/src/app/core/data/bitstream-format-data.service.ts index 1006e4eae0..97b0fa961a 100644 --- a/src/app/core/data/bitstream-format-data.service.ts +++ b/src/app/core/data/bitstream-format-data.service.ts @@ -25,11 +25,9 @@ import { coreSelector } from '../core.selectors'; import { CoreState } from '../core-state.model'; import { Bitstream } from '../shared/bitstream.model'; import { BitstreamFormat } from '../shared/bitstream-format.model'; -import { BITSTREAM_FORMAT } from '../shared/bitstream-format.resource-type'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { NoContent } from '../shared/NoContent.model'; import { sendRequest } from '../shared/request.operators'; -import { dataService } from './base/data-service.decorator'; import { DeleteData, DeleteDataImpl, @@ -60,8 +58,7 @@ const selectedBitstreamFormatSelector = createSelector( /** * A service responsible for fetching/sending data from/to the REST API on the bitstreamformats endpoint */ -@Injectable() -@dataService(BITSTREAM_FORMAT) +@Injectable({ providedIn: 'root' }) export class BitstreamFormatDataService extends IdentifiableDataService implements FindAllData, DeleteData { protected linkPath = 'bitstreamformats'; diff --git a/src/app/core/data/bundle-data.service.ts b/src/app/core/data/bundle-data.service.ts index 84684c6603..79f877fadd 100644 --- a/src/app/core/data/bundle-data.service.ts +++ b/src/app/core/data/bundle-data.service.ts @@ -14,10 +14,8 @@ import { RemoteDataBuildService } from '../cache/builders/remote-data-build.serv import { ObjectCacheService } from '../cache/object-cache.service'; import { Bitstream } from '../shared/bitstream.model'; import { Bundle } from '../shared/bundle.model'; -import { BUNDLE } from '../shared/bundle.resource-type'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { Item } from '../shared/item.model'; -import { dataService } from './base/data-service.decorator'; import { IdentifiableDataService } from './base/identifiable-data.service'; import { PatchData, @@ -35,10 +33,7 @@ import { RestRequestMethod } from './rest-request-method'; /** * A service to retrieve {@link Bundle}s from the REST API */ -@Injectable( - { providedIn: 'root' }, -) -@dataService(BUNDLE) +@Injectable({ providedIn: 'root' }) export class BundleDataService extends IdentifiableDataService implements PatchData { private bitstreamsEndpoint = 'bitstreams'; @@ -83,10 +78,14 @@ export class BundleDataService extends IdentifiableDataService implement * requested after the response becomes stale * @param linksToFollow List of {@link FollowLinkConfig} that indicate which * {@link HALLink}s should be automatically resolved + * @param options the {@link FindListOptions} for the request */ // TODO should be implemented rest side - findByItemAndName(item: Item, bundleName: string, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]): Observable> { - return this.findAllByItem(item, { elementsPerPage: 9999 }, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow).pipe( + findByItemAndName(item: Item, bundleName: string, useCachedVersionIfAvailable = true, reRequestOnStale = true, options?: FindListOptions, ...linksToFollow: FollowLinkConfig[]): Observable> { + //Since we filter by bundleName where the pagination options are not indicated we need to load all the possible bundles. + // This is a workaround, in substitution of the previously recursive call with expand + const paginationOptions = options ?? { elementsPerPage: 9999 }; + return this.findAllByItem(item, paginationOptions, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow).pipe( map((rd: RemoteData>) => { if (hasValue(rd.payload) && hasValue(rd.payload.page)) { const matchingBundle = rd.payload.page.find((bundle: Bundle) => diff --git a/src/app/core/data/collection-data.service.spec.ts b/src/app/core/data/collection-data.service.spec.ts index 431fe941bb..f27695c326 100644 --- a/src/app/core/data/collection-data.service.spec.ts +++ b/src/app/core/data/collection-data.service.spec.ts @@ -151,7 +151,7 @@ describe('CollectionDataService', () => { expect(service.getAuthorizedCollection).toHaveBeenCalledWith(queryString); }); - it('should return a RemoteData> for the getAuthorizedCollection', () => { + it('should return a RemoteData> for the getAuthorizedCollection', () => { const result = service.getAuthorizedCollection(queryString); const expected = cold('a|', { a: paginatedListRD, @@ -166,7 +166,7 @@ describe('CollectionDataService', () => { expect(service.getAuthorizedCollectionByCommunity).toHaveBeenCalledWith(communityId, queryString); }); - it('should return a RemoteData> for the getAuthorizedCollectionByCommunity', () => { + it('should return a RemoteData> for the getAuthorizedCollectionByCommunity', () => { const result = service.getAuthorizedCollectionByCommunity(communityId, queryString); const expected = cold('a|', { a: paginatedListRD, @@ -206,12 +206,12 @@ describe('CollectionDataService', () => { /** * Create a CollectionDataService used for testing - * @param reponse$ Supply a RemoteData to be returned by the REST API (optional) + * @param response$ Supply a RemoteData to be returned by the REST API (optional) */ - function createService(reponse$?: Observable>) { + function createService(response$?: Observable>) { requestService = getMockRequestService(); - let buildResponse$ = reponse$; - if (hasNoValue(reponse$)) { + let buildResponse$ = response$; + if (hasNoValue(response$)) { buildResponse$ = createSuccessfulRemoteDataObject$({}); } rdbService = jasmine.createSpyObj('rdbService', { diff --git a/src/app/core/data/collection-data.service.ts b/src/app/core/data/collection-data.service.ts index a85d7c0798..b2d5476d21 100644 --- a/src/app/core/data/collection-data.service.ts +++ b/src/app/core/data/collection-data.service.ts @@ -24,13 +24,14 @@ import { ObjectCacheService } from '../cache/object-cache.service'; import { DSpaceSerializer } from '../dspace-rest/dspace.serializer'; import { HttpOptions } from '../dspace-rest/dspace-rest.service'; import { Collection } from '../shared/collection.model'; -import { COLLECTION } from '../shared/collection.resource-type'; import { Community } from '../shared/community.model'; import { ContentSource } from '../shared/content-source.model'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { Item } from '../shared/item.model'; -import { getFirstCompletedRemoteData } from '../shared/operators'; -import { dataService } from './base/data-service.decorator'; +import { + getAllCompletedRemoteData, + getFirstCompletedRemoteData, +} from '../shared/operators'; import { BitstreamDataService } from './bitstream-data.service'; import { ComColDataService } from './comcol-data.service'; import { CommunityDataService } from './community-data.service'; @@ -45,8 +46,7 @@ import { import { RequestService } from './request.service'; import { RestRequest } from './rest-request.model'; -@Injectable() -@dataService(COLLECTION) +@Injectable({ providedIn: 'root' }) export class CollectionDataService extends ComColDataService { protected errorTitle = 'collection.source.update.notifications.error.title'; protected contentSourceError = 'collection.source.update.notifications.error.content'; @@ -87,7 +87,8 @@ export class CollectionDataService extends ComColDataService { }); return this.searchBy(searchHref, options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow).pipe( - filter((collections: RemoteData>) => !collections.isResponsePending)); + getAllCompletedRemoteData(), + ); } /** @@ -117,7 +118,8 @@ export class CollectionDataService extends ComColDataService { }); return this.searchBy(searchHref, options, true, reRequestOnStale, ...linksToFollow).pipe( - filter((collections: RemoteData>) => !collections.isResponsePending)); + getAllCompletedRemoteData(), + ); } /** @@ -141,7 +143,8 @@ export class CollectionDataService extends ComColDataService { }); return this.searchBy(searchHref, options, reRequestOnStale).pipe( - filter((collections: RemoteData>) => !collections.isResponsePending)); + getAllCompletedRemoteData(), + ); } /** * Get all collections the user is authorized to submit to, by community and has the metadata @@ -172,7 +175,8 @@ export class CollectionDataService extends ComColDataService { }); return this.searchBy(searchHref, options, true, reRequestOnStale, ...linksToFollow).pipe( - filter((collections: RemoteData>) => !collections.isResponsePending)); + getAllCompletedRemoteData(), + ); } /** @@ -187,9 +191,8 @@ export class CollectionDataService extends ComColDataService { options.elementsPerPage = 1; return this.searchBy(searchHref, options).pipe( - filter((collections: RemoteData>) => !collections.isResponsePending), - take(1), - map((collections: RemoteData>) => collections.payload.totalElements > 0), + getFirstCompletedRemoteData(), + map((collections: RemoteData>) => collections?.payload?.totalElements > 0), ); } diff --git a/src/app/core/data/community-data.service.ts b/src/app/core/data/community-data.service.ts index ad06e9ee91..79dedf0c84 100644 --- a/src/app/core/data/community-data.service.ts +++ b/src/app/core/data/community-data.service.ts @@ -13,9 +13,7 @@ import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { ObjectCacheService } from '../cache/object-cache.service'; import { Community } from '../shared/community.model'; -import { COMMUNITY } from '../shared/community.resource-type'; import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { dataService } from './base/data-service.decorator'; import { BitstreamDataService } from './bitstream-data.service'; import { ComColDataService } from './comcol-data.service'; import { DSOChangeAnalyzer } from './dso-change-analyzer.service'; @@ -24,8 +22,7 @@ import { PaginatedList } from './paginated-list.model'; import { RemoteData } from './remote-data'; import { RequestService } from './request.service'; -@Injectable() -@dataService(COMMUNITY) +@Injectable({ providedIn: 'root' }) export class CommunityDataService extends ComColDataService { protected topLinkPath = 'search/top'; diff --git a/src/app/core/data/configuration-data.service.ts b/src/app/core/data/configuration-data.service.ts index 8293173c14..bb1bd19ff1 100644 --- a/src/app/core/data/configuration-data.service.ts +++ b/src/app/core/data/configuration-data.service.ts @@ -3,16 +3,13 @@ import { Observable } from 'rxjs'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { ObjectCacheService } from '../cache/object-cache.service'; -import { CONFIG_PROPERTY } from '../shared/config-property.resource-type'; import { ConfigurationProperty } from '../shared/configuration-property.model'; import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { dataService } from './base/data-service.decorator'; import { IdentifiableDataService } from './base/identifiable-data.service'; import { RemoteData } from './remote-data'; import { RequestService } from './request.service'; -@Injectable() -@dataService(CONFIG_PROPERTY) +@Injectable({ providedIn: 'root' }) /** * Data Service responsible for retrieving Configuration properties */ diff --git a/src/app/core/data/content-source-response-parsing.service.ts b/src/app/core/data/content-source-response-parsing.service.ts index c4eaa8418d..4c0fd789fb 100644 --- a/src/app/core/data/content-source-response-parsing.service.ts +++ b/src/app/core/data/content-source-response-parsing.service.ts @@ -8,7 +8,7 @@ import { MetadataConfig } from '../shared/metadata-config.model'; import { DspaceRestResponseParsingService } from './dspace-rest-response-parsing.service'; import { RestRequest } from './rest-request.model'; -@Injectable() +@Injectable({ providedIn: 'root' }) /** * A ResponseParsingService used to parse RawRestResponse coming from the REST API to a ContentSource object */ diff --git a/src/app/core/data/debug-response-parsing.service.ts b/src/app/core/data/debug-response-parsing.service.ts index 067afcee87..d6aeca7965 100644 --- a/src/app/core/data/debug-response-parsing.service.ts +++ b/src/app/core/data/debug-response-parsing.service.ts @@ -5,7 +5,7 @@ import { RawRestResponse } from '../dspace-rest/raw-rest-response.model'; import { ResponseParsingService } from './parsing.service'; import { RestRequest } from './rest-request.model'; -@Injectable() +@Injectable({ providedIn: 'root' }) export class DebugResponseParsingService implements ResponseParsingService { parse(request: RestRequest, data: RawRestResponse): RestResponse { console.log('request', request, 'data', data); diff --git a/src/app/core/data/default-change-analyzer.service.ts b/src/app/core/data/default-change-analyzer.service.ts index 94e1856543..fa08af018a 100644 --- a/src/app/core/data/default-change-analyzer.service.ts +++ b/src/app/core/data/default-change-analyzer.service.ts @@ -13,7 +13,7 @@ import { ChangeAnalyzer } from './change-analyzer'; * A class to determine what differs between two * CacheableObjects */ -@Injectable() +@Injectable({ providedIn: 'root' }) export class DefaultChangeAnalyzer implements ChangeAnalyzer { /** * Compare the metadata of two CacheableObject and return the differences as diff --git a/src/app/core/data/dso-change-analyzer.service.ts b/src/app/core/data/dso-change-analyzer.service.ts index 4f0ade27b1..95e7b5d69f 100644 --- a/src/app/core/data/dso-change-analyzer.service.ts +++ b/src/app/core/data/dso-change-analyzer.service.ts @@ -13,7 +13,7 @@ import { ChangeAnalyzer } from './change-analyzer'; * A class to determine what differs between two * DSpaceObjects */ -@Injectable() +@Injectable({ providedIn: 'root' }) export class DSOChangeAnalyzer implements ChangeAnalyzer { /** diff --git a/src/app/core/data/dso-redirect.service.ts b/src/app/core/data/dso-redirect.service.ts index da8fe7082d..28628a6246 100644 --- a/src/app/core/data/dso-redirect.service.ts +++ b/src/app/core/data/dso-redirect.service.ts @@ -73,7 +73,7 @@ class DsoByIdOrUUIDDataService extends IdentifiableDataService { * A service to handle redirects from identifier paths to DSO path * e.g.: redirect from /handle/... to /items/... */ -@Injectable() +@Injectable({ providedIn: 'root' }) export class DsoRedirectService { private dataService: DsoByIdOrUUIDDataService; @@ -91,7 +91,7 @@ export class DsoRedirectService { /** * Redirect to a DSpaceObject's path using the given identifier type and ID. * This is used to redirect paths like "/handle/[prefix]/[suffix]" to the object's path (e.g. /items/[uuid]). - * See LookupGuard for more examples. + * See lookupGuard for more examples. * * @param id the identifier of the object to retrieve * @param identifierType the type of the given identifier (defaults to UUID) diff --git a/src/app/core/data/dso-response-parsing.service.ts b/src/app/core/data/dso-response-parsing.service.ts index 6c9028bf6b..5cabba29eb 100644 --- a/src/app/core/data/dso-response-parsing.service.ts +++ b/src/app/core/data/dso-response-parsing.service.ts @@ -19,7 +19,7 @@ import { RestRequest } from './rest-request.model'; * @deprecated use DspaceRestResponseParsingService for new code, this is only left to support a * few legacy use cases, and should get removed eventually */ -@Injectable() +@Injectable({ providedIn: 'root' }) export class DSOResponseParsingService extends BaseResponseParsingService implements ResponseParsingService { protected toCache = true; diff --git a/src/app/core/data/dspace-object-data.service.ts b/src/app/core/data/dspace-object-data.service.ts index 15e405a132..bdebbd0582 100644 --- a/src/app/core/data/dspace-object-data.service.ts +++ b/src/app/core/data/dspace-object-data.service.ts @@ -3,14 +3,11 @@ import { Injectable } from '@angular/core'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { ObjectCacheService } from '../cache/object-cache.service'; import { DSpaceObject } from '../shared/dspace-object.model'; -import { DSPACE_OBJECT } from '../shared/dspace-object.resource-type'; import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { dataService } from './base/data-service.decorator'; import { IdentifiableDataService } from './base/identifiable-data.service'; import { RequestService } from './request.service'; -@Injectable() -@dataService(DSPACE_OBJECT) +@Injectable({ providedIn: 'root' }) export class DSpaceObjectDataService extends IdentifiableDataService { constructor( protected requestService: RequestService, diff --git a/src/app/core/data/dspace-rest-response-parsing.service.ts b/src/app/core/data/dspace-rest-response-parsing.service.ts index 0177a9813a..1cd286427f 100644 --- a/src/app/core/data/dspace-rest-response-parsing.service.ts +++ b/src/app/core/data/dspace-rest-response-parsing.service.ts @@ -120,6 +120,13 @@ export class DspaceRestResponseParsingService implements ResponseParsingService if (hasValue(match)) { embedAltUrl = new URLCombiner(embedAltUrl, `?size=${match.size}`).toString(); } + if (data._embedded[property] == null) { + // Embedded object is null, meaning it exists (not undefined), but had an empty response (204) -> cache it as null + this.addToObjectCache(null, request, data, embedAltUrl); + } else if (!isCacheableObject(data._embedded[property])) { + // Embedded object exists, but doesn't contain a self link -> cache it using the alternative link instead + this.objectCache.add(data._embedded[property], hasValue(request.responseMsToLive) ? request.responseMsToLive : environment.cache.msToLive.default, request.uuid, embedAltUrl); + } this.process(data._embedded[property], request, embedAltUrl); }); } @@ -237,7 +244,7 @@ export class DspaceRestResponseParsingService implements ResponseParsingService * @param alternativeURL an alternative url that can be used to retrieve the object */ addToObjectCache(co: CacheableObject, request: RestRequest, data: any, alternativeURL?: string): void { - if (!isCacheableObject(co)) { + if (hasValue(co) && !isCacheableObject(co)) { const type = hasValue(data) && hasValue(data.type) ? data.type : 'object'; let dataJSON: string; if (hasValue(data._embedded)) { @@ -251,7 +258,7 @@ export class DspaceRestResponseParsingService implements ResponseParsingService return; } - if (alternativeURL === co._links.self.href) { + if (hasValue(co) && alternativeURL === co._links.self.href) { alternativeURL = undefined; } diff --git a/src/app/core/data/endpoint-map-response-parsing.service.ts b/src/app/core/data/endpoint-map-response-parsing.service.ts index 69e9ec2de6..c7dd40b98b 100644 --- a/src/app/core/data/endpoint-map-response-parsing.service.ts +++ b/src/app/core/data/endpoint-map-response-parsing.service.ts @@ -20,7 +20,7 @@ import { RestRequest } from './rest-request.model'; * * When all endpoints are properly typed, it can be removed. */ -@Injectable() +@Injectable({ providedIn: 'root' }) export class EndpointMapResponseParsingService extends DspaceRestResponseParsingService { /** diff --git a/src/app/core/data/entity-type-data.service.ts b/src/app/core/data/entity-type-data.service.ts index 22cf1a1706..d47fadce17 100644 --- a/src/app/core/data/entity-type-data.service.ts +++ b/src/app/core/data/entity-type-data.service.ts @@ -1,7 +1,6 @@ import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { - filter, map, switchMap, take, @@ -14,6 +13,7 @@ import { HALEndpointService } from '../shared/hal-endpoint.service'; import { ItemType } from '../shared/item-relationships/item-type.model'; import { RelationshipType } from '../shared/item-relationships/relationship-type.model'; import { + getAllCompletedRemoteData, getFirstSucceededRemoteData, getRemoteDataPayload, } from '../shared/operators'; @@ -35,7 +35,7 @@ import { RequestService } from './request.service'; /** * Service handling all ItemType requests */ -@Injectable() +@Injectable({ providedIn: 'root' }) export class EntityTypeDataService extends BaseDataService implements FindAllData, SearchData { private findAllData: FindAllData; private searchData: SearchDataImpl; @@ -89,8 +89,7 @@ export class EntityTypeDataService extends BaseDataService implements getAllAuthorizedRelationshipType(options: FindListOptions = {}): Observable>> { const searchHref = 'findAllByAuthorizedCollection'; - return this.searchBy(searchHref, options).pipe( - filter((type: RemoteData>) => !type.isResponsePending)); + return this.searchBy(searchHref, options).pipe(getAllCompletedRemoteData()); } /** @@ -123,8 +122,7 @@ export class EntityTypeDataService extends BaseDataService implements getAllAuthorizedRelationshipTypeImport(options: FindListOptions = {}): Observable>> { const searchHref = 'findAllByAuthorizedExternalSource'; - return this.searchBy(searchHref, options).pipe( - filter((type: RemoteData>) => !type.isResponsePending)); + return this.searchBy(searchHref, options).pipe(getAllCompletedRemoteData()); } /** @@ -136,15 +134,8 @@ export class EntityTypeDataService extends BaseDataService implements currentPage: 1, }; return this.getAllAuthorizedRelationshipTypeImport(findListOptions).pipe( - map((result: RemoteData>) => { - let output: boolean; - if (result.payload) { - output = ( result.payload.page.length > 1 ); - } else { - output = false; - } - return output; - }), + take(1), + map((result: RemoteData>) => result?.payload?.totalElements > 1), ); } diff --git a/src/app/core/data/external-source-data.service.ts b/src/app/core/data/external-source-data.service.ts index 16eaeb9321..e7f123dd18 100644 --- a/src/app/core/data/external-source-data.service.ts +++ b/src/app/core/data/external-source-data.service.ts @@ -31,7 +31,7 @@ import { RequestService } from './request.service'; /** * A service handling all external source requests */ -@Injectable() +@Injectable({ providedIn: 'root' }) export class ExternalSourceDataService extends IdentifiableDataService implements SearchData { private searchData: SearchData; diff --git a/src/app/core/data/facet-config-response-parsing.service.ts b/src/app/core/data/facet-config-response-parsing.service.ts index b4086428ea..4ae22c34d8 100644 --- a/src/app/core/data/facet-config-response-parsing.service.ts +++ b/src/app/core/data/facet-config-response-parsing.service.ts @@ -8,7 +8,7 @@ import { RawRestResponse } from '../dspace-rest/raw-rest-response.model'; import { DspaceRestResponseParsingService } from './dspace-rest-response-parsing.service'; import { RestRequest } from './rest-request.model'; -@Injectable() +@Injectable({ providedIn: 'root' }) export class FacetConfigResponseParsingService extends DspaceRestResponseParsingService { parse(request: RestRequest, data: RawRestResponse): ParsedResponse { diff --git a/src/app/core/data/facet-value-response-parsing.service.ts b/src/app/core/data/facet-value-response-parsing.service.ts index 871f2a4965..5cd24770d8 100644 --- a/src/app/core/data/facet-value-response-parsing.service.ts +++ b/src/app/core/data/facet-value-response-parsing.service.ts @@ -8,7 +8,7 @@ import { RawRestResponse } from '../dspace-rest/raw-rest-response.model'; import { DspaceRestResponseParsingService } from './dspace-rest-response-parsing.service'; import { RestRequest } from './rest-request.model'; -@Injectable() +@Injectable({ providedIn: 'root' }) export class FacetValueResponseParsingService extends DspaceRestResponseParsingService { parse(request: RestRequest, data: RawRestResponse): ParsedResponse { const payload = data.payload; diff --git a/src/app/core/data/feature-authorization/authorization-data.service.spec.ts b/src/app/core/data/feature-authorization/authorization-data.service.spec.ts index 4048efe4ff..4ada344beb 100644 --- a/src/app/core/data/feature-authorization/authorization-data.service.spec.ts +++ b/src/app/core/data/feature-authorization/authorization-data.service.spec.ts @@ -72,7 +72,7 @@ describe('AuthorizationDataService', () => { const ePersonUuid = 'fake-eperson-uuid'; function createExpected(providedObjectUrl: string, providedEPersonUuid?: string, providedFeatureId?: FeatureID): FindListOptions { - const searchParams = [new RequestParam('uri', providedObjectUrl)]; + const searchParams = [new RequestParam('uri', providedObjectUrl, false)]; if (hasValue(providedFeatureId)) { searchParams.push(new RequestParam('feature', providedFeatureId)); } diff --git a/src/app/core/data/feature-authorization/authorization-data.service.ts b/src/app/core/data/feature-authorization/authorization-data.service.ts index cd8705d2fb..e5efbae671 100644 --- a/src/app/core/data/feature-authorization/authorization-data.service.ts +++ b/src/app/core/data/feature-authorization/authorization-data.service.ts @@ -22,11 +22,9 @@ import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.s import { RequestParam } from '../../cache/models/request-param.model'; import { ObjectCacheService } from '../../cache/object-cache.service'; import { Authorization } from '../../shared/authorization.model'; -import { AUTHORIZATION } from '../../shared/authorization.resource-type'; import { HALEndpointService } from '../../shared/hal-endpoint.service'; import { getFirstCompletedRemoteData } from '../../shared/operators'; import { BaseDataService } from '../base/base-data.service'; -import { dataService } from '../base/data-service.decorator'; import { SearchData, SearchDataImpl, @@ -43,8 +41,7 @@ import { FeatureID } from './feature-id'; /** * A service to retrieve {@link Authorization}s from the REST API */ -@Injectable() -@dataService(AUTHORIZATION) +@Injectable({ providedIn: 'root' }) export class AuthorizationDataService extends BaseDataService implements SearchData { protected linkPath = 'authorizations'; protected searchByObjectPath = 'object'; @@ -150,7 +147,8 @@ export class AuthorizationDataService extends BaseDataService imp if (isNotEmpty(options.searchParams)) { params = [...options.searchParams]; } - params.push(new RequestParam('uri', objectUrl)); + // TODO fix encode the uri parameter in the self link in the backend and set encodeValue to true afterwards + params.push(new RequestParam('uri', objectUrl, false)); if (hasValue(featureId)) { params.push(new RequestParam('feature', featureId)); } diff --git a/src/app/core/data/feature-authorization/authorization-utils.ts b/src/app/core/data/feature-authorization/authorization-utils.ts index f763b1a38d..cd4bc452a5 100644 --- a/src/app/core/data/feature-authorization/authorization-utils.ts +++ b/src/app/core/data/feature-authorization/authorization-utils.ts @@ -91,5 +91,5 @@ export const oneAuthorizationMatchesFeature = (featureID: FeatureID) => return observableOf([]); } }), - map((features: Feature[]) => features.filter((feature: Feature) => feature.id === featureID).length > 0), + map((features: Feature[]) => features.filter((feature: Feature) => feature.id === featureID.valueOf()).length > 0), ); diff --git a/src/app/core/data/feature-authorization/feature-authorization-guard/collection-administrator.guard.ts b/src/app/core/data/feature-authorization/feature-authorization-guard/collection-administrator.guard.ts index 5af9f18b33..1b1b4a9d6c 100644 --- a/src/app/core/data/feature-authorization/feature-authorization-guard/collection-administrator.guard.ts +++ b/src/app/core/data/feature-authorization/feature-authorization-guard/collection-administrator.guard.ts @@ -1,35 +1,13 @@ -import { Injectable } from '@angular/core'; -import { - ActivatedRouteSnapshot, - Router, - RouterStateSnapshot, -} from '@angular/router'; -import { - Observable, - of as observableOf, -} from 'rxjs'; +import { CanActivateFn } from '@angular/router'; +import { of as observableOf } from 'rxjs'; -import { AuthService } from '../../../auth/auth.service'; -import { AuthorizationDataService } from '../authorization-data.service'; import { FeatureID } from '../feature-id'; -import { SingleFeatureAuthorizationGuard } from './single-feature-authorization.guard'; +import { singleFeatureAuthorizationGuard } from './single-feature-authorization.guard'; /** * Prevent unauthorized activating and loading of routes when the current authenticated user * isn't a Collection administrator + * Check group management rights */ -@Injectable({ - providedIn: 'root', -}) -export class CollectionAdministratorGuard extends SingleFeatureAuthorizationGuard { - constructor(protected authorizationService: AuthorizationDataService, protected router: Router, protected authService: AuthService) { - super(authorizationService, router, authService); - } - - /** - * Check group management rights - */ - getFeatureID(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { - return observableOf(FeatureID.IsCollectionAdmin); - } -} +export const collectionAdministratorGuard: CanActivateFn = + singleFeatureAuthorizationGuard(() => observableOf(FeatureID.IsCollectionAdmin)); diff --git a/src/app/core/data/feature-authorization/feature-authorization-guard/community-administrator.guard.ts b/src/app/core/data/feature-authorization/feature-authorization-guard/community-administrator.guard.ts index 2092fce110..6d7dac314e 100644 --- a/src/app/core/data/feature-authorization/feature-authorization-guard/community-administrator.guard.ts +++ b/src/app/core/data/feature-authorization/feature-authorization-guard/community-administrator.guard.ts @@ -1,35 +1,13 @@ -import { Injectable } from '@angular/core'; -import { - ActivatedRouteSnapshot, - Router, - RouterStateSnapshot, -} from '@angular/router'; -import { - Observable, - of as observableOf, -} from 'rxjs'; +import { CanActivateFn } from '@angular/router'; +import { of as observableOf } from 'rxjs'; -import { AuthService } from '../../../auth/auth.service'; -import { AuthorizationDataService } from '../authorization-data.service'; import { FeatureID } from '../feature-id'; -import { SingleFeatureAuthorizationGuard } from './single-feature-authorization.guard'; +import { singleFeatureAuthorizationGuard } from './single-feature-authorization.guard'; /** * Prevent unauthorized activating and loading of routes when the current authenticated user * isn't a Community administrator + * Check group management rights */ -@Injectable({ - providedIn: 'root', -}) -export class CommunityAdministratorGuard extends SingleFeatureAuthorizationGuard { - constructor(protected authorizationService: AuthorizationDataService, protected router: Router, protected authService: AuthService) { - super(authorizationService, router, authService); - } - - /** - * Check group management rights - */ - getFeatureID(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { - return observableOf(FeatureID.IsCommunityAdmin); - } -} +export const communityAdministratorGuard: CanActivateFn = + singleFeatureAuthorizationGuard(() => observableOf(FeatureID.IsCommunityAdmin)); diff --git a/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard.spec.ts b/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard.spec.ts index 234c2e1628..18292bb943 100644 --- a/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard.spec.ts +++ b/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard.spec.ts @@ -1,8 +1,8 @@ +import { TestBed } from '@angular/core/testing'; import { - ActivatedRouteSnapshot, - Resolve, + ResolveFn, Router, - RouterStateSnapshot, + UrlTree, } from '@angular/router'; import { Observable, @@ -15,35 +15,24 @@ import { DSpaceObject } from '../../../shared/dspace-object.model'; import { RemoteData } from '../../remote-data'; import { AuthorizationDataService } from '../authorization-data.service'; import { FeatureID } from '../feature-id'; -import { DsoPageSingleFeatureGuard } from './dso-page-single-feature.guard'; +import { dsoPageSingleFeatureGuard } from './dso-page-single-feature.guard'; +import { + defaultDSOGetObjectUrl, + getRouteWithDSOId, +} from './dso-page-some-feature.guard'; -/** - * Test implementation of abstract class DsoPageSingleFeatureGuard - */ -class DsoPageSingleFeatureGuardImpl extends DsoPageSingleFeatureGuard { - constructor(protected resolver: Resolve>, - protected authorizationService: AuthorizationDataService, - protected router: Router, - protected authService: AuthService, - protected featureID: FeatureID) { - super(resolver, authorizationService, router, authService); - } - - getFeatureID(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { - return observableOf(this.featureID); - } -} describe('DsoPageSingleFeatureGuard', () => { - let guard: DsoPageSingleFeatureGuard; let authorizationService: AuthorizationDataService; let router: Router; let authService: AuthService; - let resolver: Resolve>; + let resolver: ResolveFn>; let object: DSpaceObject; let route; let parentRoute; + let featureId: FeatureID; + function init() { object = { self: 'test-selflink', @@ -55,9 +44,7 @@ describe('DsoPageSingleFeatureGuard', () => { router = jasmine.createSpyObj('router', { parseUrl: {}, }); - resolver = jasmine.createSpyObj('resolver', { - resolve: createSuccessfulRemoteDataObject$(object), - }); + resolver = () => createSuccessfulRemoteDataObject$(object); authService = jasmine.createSpyObj('authService', { isAuthenticated: observableOf(true), }); @@ -71,16 +58,25 @@ describe('DsoPageSingleFeatureGuard', () => { }, parent: parentRoute, }; - guard = new DsoPageSingleFeatureGuardImpl(resolver, authorizationService, router, authService, undefined); + + featureId = FeatureID.LoginOnBehalfOf; + + TestBed.configureTestingModule({ + providers: [ + { provide: AuthorizationDataService, useValue: authorizationService }, + { provide: Router, useValue: router }, + { provide: AuthService, useValue: authService }, + ], + }); } beforeEach(() => { init(); }); - describe('getObjectUrl', () => { + describe('defaultDSOGetObjectUrl', () => { it('should return the resolved object\'s selflink', (done) => { - guard.getObjectUrl(route, undefined).subscribe((selflink) => { + defaultDSOGetObjectUrl(resolver)(route, undefined).subscribe((selflink) => { expect(selflink).toEqual(object.self); done(); }); @@ -89,8 +85,23 @@ describe('DsoPageSingleFeatureGuard', () => { describe('getRouteWithDSOId', () => { it('should return the route that has the UUID of the DSO', () => { - const foundRoute = (guard as any).getRouteWithDSOId(route); + const foundRoute = getRouteWithDSOId(route); expect(foundRoute).toBe(parentRoute); }); }); + + describe('dsoPageSingleFeatureGuard', () => { + it('should call authorizationService.isAuthenticated with the appropriate arguments', (done) => { + const result$ = TestBed.runInInjectionContext(() => { + return dsoPageSingleFeatureGuard( + () => resolver, () => observableOf(featureId), + )(route, { url: 'current-url' } as any); + }) as Observable; + + result$.subscribe(() => { + expect(authorizationService.isAuthorized).toHaveBeenCalledWith(featureId, object.self, undefined); + done(); + }); + }); + }); }); diff --git a/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard.ts b/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard.ts index 1f75df846b..5073a38653 100644 --- a/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard.ts +++ b/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard.ts @@ -1,31 +1,27 @@ import { ActivatedRouteSnapshot, + CanActivateFn, + ResolveFn, RouterStateSnapshot, } from '@angular/router'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; import { DSpaceObject } from '../../../shared/dspace-object.model'; +import { RemoteData } from '../../remote-data'; import { FeatureID } from '../feature-id'; -import { DsoPageSomeFeatureGuard } from './dso-page-some-feature.guard'; +import { dsoPageSomeFeatureGuard } from './dso-page-some-feature.guard'; +import { SingleFeatureGuardParamFn } from './single-feature-authorization.guard'; /** * Abstract Guard for preventing unauthorized access to {@link DSpaceObject} pages that require rights for a specific feature * This guard utilizes a resolver to retrieve the relevant object to check authorizations for */ -export abstract class DsoPageSingleFeatureGuard extends DsoPageSomeFeatureGuard { - /** - * The features to check authorization for - */ - getFeatureIDs(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { - return this.getFeatureID(route, state).pipe( - map((featureID) => [featureID]), - ); - } - - /** - * The type of feature to check authorization for - * Override this method to define a feature - */ - abstract getFeatureID(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable; -} +export const dsoPageSingleFeatureGuard = ( + getResolveFn: () => ResolveFn>, + getFeatureID: SingleFeatureGuardParamFn, +): CanActivateFn => dsoPageSomeFeatureGuard( + getResolveFn, + (route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable => getFeatureID(route, state).pipe( + map((featureID: FeatureID) => [featureID]), + )); diff --git a/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-some-feature.guard.spec.ts b/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-some-feature.guard.spec.ts index 61e236188d..08f1c96b29 100644 --- a/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-some-feature.guard.spec.ts +++ b/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-some-feature.guard.spec.ts @@ -1,8 +1,8 @@ +import { TestBed } from '@angular/core/testing'; import { - ActivatedRouteSnapshot, - Resolve, + ResolveFn, Router, - RouterStateSnapshot, + UrlTree, } from '@angular/router'; import { Observable, @@ -15,49 +15,36 @@ import { DSpaceObject } from '../../../shared/dspace-object.model'; import { RemoteData } from '../../remote-data'; import { AuthorizationDataService } from '../authorization-data.service'; import { FeatureID } from '../feature-id'; -import { DsoPageSomeFeatureGuard } from './dso-page-some-feature.guard'; +import { + defaultDSOGetObjectUrl, + dsoPageSomeFeatureGuard, + getRouteWithDSOId, +} from './dso-page-some-feature.guard'; -/** - * Test implementation of abstract class DsoPageSomeFeatureGuard - */ -class DsoPageSomeFeatureGuardImpl extends DsoPageSomeFeatureGuard { - constructor(protected resolver: Resolve>, - protected authorizationService: AuthorizationDataService, - protected router: Router, - protected authService: AuthService, - protected featureIDs: FeatureID[]) { - super(resolver, authorizationService, router, authService); - } - getFeatureIDs(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { - return observableOf(this.featureIDs); - } -} - -describe('DsoPageSomeFeatureGuard', () => { - let guard: DsoPageSomeFeatureGuard; +describe('dsoPageSomeFeatureGuard and its functions', () => { let authorizationService: AuthorizationDataService; let router: Router; let authService: AuthService; - let resolver: Resolve>; + let resolver: ResolveFn>; let object: DSpaceObject; let route; let parentRoute; + let featureIds: FeatureID[]; + function init() { object = { self: 'test-selflink', } as DSpaceObject; - + featureIds = [FeatureID.LoginOnBehalfOf, FeatureID.CanDelete]; authorizationService = jasmine.createSpyObj('authorizationService', { isAuthorized: observableOf(true), }); router = jasmine.createSpyObj('router', { parseUrl: {}, }); - resolver = jasmine.createSpyObj('resolver', { - resolve: createSuccessfulRemoteDataObject$(object), - }); + resolver = () => createSuccessfulRemoteDataObject$(object); authService = jasmine.createSpyObj('authService', { isAuthenticated: observableOf(true), }); @@ -71,16 +58,25 @@ describe('DsoPageSomeFeatureGuard', () => { }, parent: parentRoute, }; - guard = new DsoPageSomeFeatureGuardImpl(resolver, authorizationService, router, authService, []); + + TestBed.configureTestingModule({ + providers: [ + { provide: AuthorizationDataService, useValue: authorizationService }, + { provide: Router, useValue: router }, + { provide: AuthService, useValue: authService }, + ], + }); + } beforeEach(() => { init(); }); - describe('getObjectUrl', () => { + + describe('defaultDSOGetObjectUrl', () => { it('should return the resolved object\'s selflink', (done) => { - guard.getObjectUrl(route, undefined).subscribe((selflink) => { + defaultDSOGetObjectUrl(resolver)(route, undefined).subscribe((selflink) => { expect(selflink).toEqual(object.self); done(); }); @@ -89,8 +85,26 @@ describe('DsoPageSomeFeatureGuard', () => { describe('getRouteWithDSOId', () => { it('should return the route that has the UUID of the DSO', () => { - const foundRoute = (guard as any).getRouteWithDSOId(route); + const foundRoute = getRouteWithDSOId(route); expect(foundRoute).toBe(parentRoute); }); }); + + + describe('dsoPageSomeFeatureGuard', () => { + it('should call authorizationService.isAuthenticated with the appropriate arguments', (done) => { + const result$ = TestBed.runInInjectionContext(() => { + return dsoPageSomeFeatureGuard( + () => resolver, () => observableOf(featureIds), + )(route, { url: 'current-url' } as any); + }) as Observable; + + result$.subscribe(() => { + featureIds.forEach((featureId: FeatureID) => { + expect(authorizationService.isAuthorized).toHaveBeenCalledWith(featureId, object.self, undefined); + }); + done(); + }); + }); + }); }); diff --git a/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-some-feature.guard.ts b/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-some-feature.guard.ts index eff2da2102..7469f113b4 100644 --- a/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-some-feature.guard.ts +++ b/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-some-feature.guard.ts @@ -1,7 +1,7 @@ import { ActivatedRouteSnapshot, - Resolve, - Router, + CanActivateFn, + ResolveFn, RouterStateSnapshot, } from '@angular/router'; import { Observable } from 'rxjs'; @@ -11,45 +11,50 @@ import { hasNoValue, hasValue, } from '../../../../shared/empty.util'; -import { AuthService } from '../../../auth/auth.service'; import { DSpaceObject } from '../../../shared/dspace-object.model'; import { getAllSucceededRemoteDataPayload } from '../../../shared/operators'; import { RemoteData } from '../../remote-data'; -import { AuthorizationDataService } from '../authorization-data.service'; -import { SomeFeatureAuthorizationGuard } from './some-feature-authorization.guard'; +import { FeatureID } from '../feature-id'; +import { + someFeatureAuthorizationGuard, + SomeFeatureGuardParamFn, + StringGuardParamFn, +} from './some-feature-authorization.guard'; + +export declare type DSOGetObjectURlFn = (resolve: ResolveFn>) => StringGuardParamFn; + /** - * Abstract Guard for preventing unauthorized access to {@link DSpaceObject} pages that require rights for any specific feature in a list - * This guard utilizes a resolver to retrieve the relevant object to check authorizations for + * Method to resolve resolve (parent) route that contains the UUID of the DSO + * @param route The current route */ -export abstract class DsoPageSomeFeatureGuard extends SomeFeatureAuthorizationGuard { - constructor(protected resolver: Resolve>, - protected authorizationService: AuthorizationDataService, - protected router: Router, - protected authService: AuthService) { - super(authorizationService, router, authService); +export const getRouteWithDSOId = (route: ActivatedRouteSnapshot): ActivatedRouteSnapshot => { + let routeWithDSOId = route; + while (hasNoValue(routeWithDSOId.params.id) && hasValue(routeWithDSOId.parent)) { + routeWithDSOId = routeWithDSOId.parent; } + return routeWithDSOId; +}; - /** - * Check authorization rights for the object resolved using the provided resolver - */ - getObjectUrl(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { - const routeWithObjectID = this.getRouteWithDSOId(route); - return (this.resolver.resolve(routeWithObjectID, state) as Observable>).pipe( + + +export const defaultDSOGetObjectUrl: DSOGetObjectURlFn = (resolve: ResolveFn>): StringGuardParamFn => { + return (route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable => { + const routeWithObjectID = getRouteWithDSOId(route); + return (resolve(routeWithObjectID, state) as Observable>).pipe( getAllSucceededRemoteDataPayload(), map((dso) => dso.self), ); - } + }; +}; - /** - * Method to resolve resolve (parent) route that contains the UUID of the DSO - * @param route The current route - */ - protected getRouteWithDSOId(route: ActivatedRouteSnapshot): ActivatedRouteSnapshot { - let routeWithDSOId = route; - while (hasNoValue(routeWithDSOId.params.id) && hasValue(routeWithDSOId.parent)) { - routeWithDSOId = routeWithDSOId.parent; - } - return routeWithDSOId; - } -} +/** + * Guard for preventing unauthorized access to {@link DSpaceObject} pages that require rights for any specific feature in a list + * This guard utilizes a resolver to retrieve the relevant object to check authorizations for + */ +export const dsoPageSomeFeatureGuard = ( + getResolveFn: () => ResolveFn>, + getFeatureIDs: SomeFeatureGuardParamFn, + getObjectUrl: DSOGetObjectURlFn = defaultDSOGetObjectUrl, + getEPersonUuid?: StringGuardParamFn, +): CanActivateFn => someFeatureAuthorizationGuard((route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable => getFeatureIDs(route, state), getObjectUrl(getResolveFn()), getEPersonUuid); diff --git a/src/app/core/data/feature-authorization/feature-authorization-guard/group-administrator.guard.ts b/src/app/core/data/feature-authorization/feature-authorization-guard/group-administrator.guard.ts index 5f32e26851..9641d0aace 100644 --- a/src/app/core/data/feature-authorization/feature-authorization-guard/group-administrator.guard.ts +++ b/src/app/core/data/feature-authorization/feature-authorization-guard/group-administrator.guard.ts @@ -1,35 +1,12 @@ -import { Injectable } from '@angular/core'; -import { - ActivatedRouteSnapshot, - Router, - RouterStateSnapshot, -} from '@angular/router'; -import { - Observable, - of as observableOf, -} from 'rxjs'; +import { CanActivateFn } from '@angular/router'; +import { of as observableOf } from 'rxjs'; -import { AuthService } from '../../../auth/auth.service'; -import { AuthorizationDataService } from '../authorization-data.service'; import { FeatureID } from '../feature-id'; -import { SingleFeatureAuthorizationGuard } from './single-feature-authorization.guard'; +import { singleFeatureAuthorizationGuard } from './single-feature-authorization.guard'; /** * Prevent unauthorized activating and loading of routes when the current authenticated user doesn't have group * management rights */ -@Injectable({ - providedIn: 'root', -}) -export class GroupAdministratorGuard extends SingleFeatureAuthorizationGuard { - constructor(protected authorizationService: AuthorizationDataService, protected router: Router, protected authService: AuthService) { - super(authorizationService, router, authService); - } - - /** - * Check group management rights - */ - getFeatureID(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { - return observableOf(FeatureID.CanManageGroups); - } -} +export const groupAdministratorGuard: CanActivateFn = + singleFeatureAuthorizationGuard(() => observableOf(FeatureID.CanManageGroups)); diff --git a/src/app/core/data/feature-authorization/feature-authorization-guard/single-feature-authorization.guard.spec.ts b/src/app/core/data/feature-authorization/feature-authorization-guard/single-feature-authorization.guard.spec.ts index e789f8c473..7c15fa4cdf 100644 --- a/src/app/core/data/feature-authorization/feature-authorization-guard/single-feature-authorization.guard.spec.ts +++ b/src/app/core/data/feature-authorization/feature-authorization-guard/single-feature-authorization.guard.spec.ts @@ -1,7 +1,10 @@ import { - ActivatedRouteSnapshot, + TestBed, + waitForAsync, +} from '@angular/core/testing'; +import { Router, - RouterStateSnapshot, + UrlTree, } from '@angular/router'; import { Observable, @@ -11,37 +14,9 @@ import { import { AuthService } from '../../../auth/auth.service'; import { AuthorizationDataService } from '../authorization-data.service'; import { FeatureID } from '../feature-id'; -import { SingleFeatureAuthorizationGuard } from './single-feature-authorization.guard'; +import { singleFeatureAuthorizationGuard } from './single-feature-authorization.guard'; -/** - * Test implementation of abstract class SingleFeatureAuthorizationGuard - * Provide the return values of the overwritten getters as constructor arguments - */ -class SingleFeatureAuthorizationGuardImpl extends SingleFeatureAuthorizationGuard { - constructor(protected authorizationService: AuthorizationDataService, - protected router: Router, - protected authService: AuthService, - protected featureId: FeatureID, - protected objectUrl: string, - protected ePersonUuid: string) { - super(authorizationService, router, authService); - } - - getFeatureID(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { - return observableOf(this.featureId); - } - - getObjectUrl(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { - return observableOf(this.objectUrl); - } - - getEPersonUuid(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { - return observableOf(this.ePersonUuid); - } -} - -describe('SingleFeatureAuthorizationGuard', () => { - let guard: SingleFeatureAuthorizationGuard; +describe('singleFeatureAuthorizationGuard', () => { let authorizationService: AuthorizationDataService; let router: Router; let authService: AuthService; @@ -64,17 +39,36 @@ describe('SingleFeatureAuthorizationGuard', () => { authService = jasmine.createSpyObj('authService', { isAuthenticated: observableOf(true), }); - guard = new SingleFeatureAuthorizationGuardImpl(authorizationService, router, authService, featureId, objectUrl, ePersonUuid); + + TestBed.configureTestingModule({ + providers: [ + { provide: AuthorizationDataService, useValue: authorizationService }, + { provide: Router, useValue: router }, + { provide: AuthService, useValue: authService }, + ], + }); } - beforeEach(() => { + beforeEach(waitForAsync(() => { init(); - }); + })); describe('canActivate', () => { - it('should call authorizationService.isAuthenticated with the appropriate arguments', () => { - guard.canActivate(undefined, { url: 'current-url' } as any).subscribe(); - expect(authorizationService.isAuthorized).toHaveBeenCalledWith(featureId, objectUrl, ePersonUuid); + it('should call authorizationService.isAuthenticated with the appropriate arguments', (done: DoneFn) => { + const result$ = TestBed.runInInjectionContext(() => { + return singleFeatureAuthorizationGuard( + () => observableOf(featureId), + () => observableOf(objectUrl), + () => observableOf(ePersonUuid), + )(undefined, { url: 'current-url' } as any); + }) as Observable; + + + result$.subscribe(() => { + expect(authorizationService.isAuthorized).toHaveBeenCalledWith(featureId, objectUrl, ePersonUuid); + done(); + }); }); + }); }); diff --git a/src/app/core/data/feature-authorization/feature-authorization-guard/single-feature-authorization.guard.ts b/src/app/core/data/feature-authorization/feature-authorization-guard/single-feature-authorization.guard.ts index cd9f615aa7..995dcb6f5c 100644 --- a/src/app/core/data/feature-authorization/feature-authorization-guard/single-feature-authorization.guard.ts +++ b/src/app/core/data/feature-authorization/feature-authorization-guard/single-feature-authorization.guard.ts @@ -1,31 +1,35 @@ import { ActivatedRouteSnapshot, + CanActivateFn, RouterStateSnapshot, } from '@angular/router'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; import { FeatureID } from '../feature-id'; -import { SomeFeatureAuthorizationGuard } from './some-feature-authorization.guard'; +import { + someFeatureAuthorizationGuard, + StringGuardParamFn, +} from './some-feature-authorization.guard'; + +export declare type SingleFeatureGuardParamFn = (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => Observable; /** - * Abstract Guard for preventing unauthorized activating and loading of routes when a user - * doesn't have authorized rights on a specific feature and/or object. - * Override the desired getters in the parent class for checking specific authorization on a feature and/or object. + * Guard for preventing unauthorized activating and loading of routes when a user doesn't have + * authorized rights on a specific feature and/or object. + * + * @param getFeatureID The feature to check authorization for + * @param getObjectUrl The URL of the object to check if the user has authorized rights for, + * Optional, if not provided, the {@link Site}'s URL will be assumed + * @param getEPersonUuid The UUID of the user to check authorization rights for. + * Optional, if not provided, the authenticated user's UUID will be assumed. */ -export abstract class SingleFeatureAuthorizationGuard extends SomeFeatureAuthorizationGuard { - /** - * The features to check authorization for - */ - getFeatureIDs(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { - return this.getFeatureID(route, state).pipe( - map((featureID) => [featureID]), - ); - } - /** - * The type of feature to check authorization for - * Override this method to define a feature - */ - abstract getFeatureID(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable; -} +export const singleFeatureAuthorizationGuard = ( + getFeatureID: SingleFeatureGuardParamFn, + getObjectUrl?: StringGuardParamFn, + getEPersonUuid?: StringGuardParamFn, +): CanActivateFn => someFeatureAuthorizationGuard( + (route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable => getFeatureID(route, state).pipe( + map((featureID: FeatureID) => [featureID]), + ), getObjectUrl, getEPersonUuid); diff --git a/src/app/core/data/feature-authorization/feature-authorization-guard/site-administrator.guard.ts b/src/app/core/data/feature-authorization/feature-authorization-guard/site-administrator.guard.ts index 9a7c9de5c4..4caa1f806d 100644 --- a/src/app/core/data/feature-authorization/feature-authorization-guard/site-administrator.guard.ts +++ b/src/app/core/data/feature-authorization/feature-authorization-guard/site-administrator.guard.ts @@ -1,35 +1,12 @@ -import { Injectable } from '@angular/core'; -import { - ActivatedRouteSnapshot, - Router, - RouterStateSnapshot, -} from '@angular/router'; -import { - Observable, - of as observableOf, -} from 'rxjs'; +import { CanActivateFn } from '@angular/router'; +import { of as observableOf } from 'rxjs'; -import { AuthService } from '../../../auth/auth.service'; -import { AuthorizationDataService } from '../authorization-data.service'; import { FeatureID } from '../feature-id'; -import { SingleFeatureAuthorizationGuard } from './single-feature-authorization.guard'; +import { singleFeatureAuthorizationGuard } from './single-feature-authorization.guard'; /** * Prevent unauthorized activating and loading of routes when the current authenticated user doesn't have administrator * rights to the {@link Site} */ -@Injectable({ - providedIn: 'root', -}) -export class SiteAdministratorGuard extends SingleFeatureAuthorizationGuard { - constructor(protected authorizationService: AuthorizationDataService, protected router: Router, protected authService: AuthService) { - super(authorizationService, router, authService); - } - - /** - * Check administrator authorization rights - */ - getFeatureID(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { - return observableOf(FeatureID.AdministratorOf); - } -} +export const siteAdministratorGuard: CanActivateFn = + singleFeatureAuthorizationGuard(() => observableOf(FeatureID.AdministratorOf)); diff --git a/src/app/core/data/feature-authorization/feature-authorization-guard/site-register.guard.ts b/src/app/core/data/feature-authorization/feature-authorization-guard/site-register.guard.ts index bde2d1c14e..ee08532d38 100644 --- a/src/app/core/data/feature-authorization/feature-authorization-guard/site-register.guard.ts +++ b/src/app/core/data/feature-authorization/feature-authorization-guard/site-register.guard.ts @@ -1,35 +1,12 @@ -import { Injectable } from '@angular/core'; -import { - ActivatedRouteSnapshot, - Router, - RouterStateSnapshot, -} from '@angular/router'; -import { - Observable, - of as observableOf, -} from 'rxjs'; +import { CanActivateFn } from '@angular/router'; +import { of as observableOf } from 'rxjs'; -import { AuthService } from '../../../auth/auth.service'; -import { AuthorizationDataService } from '../authorization-data.service'; import { FeatureID } from '../feature-id'; -import { SingleFeatureAuthorizationGuard } from './single-feature-authorization.guard'; +import { singleFeatureAuthorizationGuard } from './single-feature-authorization.guard'; /** * Prevent unauthorized activating and loading of routes when the current authenticated user doesn't have registration * rights to the {@link Site} */ -@Injectable({ - providedIn: 'root', -}) -export class SiteRegisterGuard extends SingleFeatureAuthorizationGuard { - constructor(protected authorizationService: AuthorizationDataService, protected router: Router, protected authService: AuthService) { - super(authorizationService, router, authService); - } - - /** - * Check registration authorization rights - */ - getFeatureID(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { - return observableOf(FeatureID.EPersonRegistration); - } -} +export const siteRegisterGuard: CanActivateFn = + singleFeatureAuthorizationGuard(() => observableOf(FeatureID.EPersonRegistration)); diff --git a/src/app/core/data/feature-authorization/feature-authorization-guard/some-feature-authorization.guard.spec.ts b/src/app/core/data/feature-authorization/feature-authorization-guard/some-feature-authorization.guard.spec.ts index 53d77cadad..79e023bdd0 100644 --- a/src/app/core/data/feature-authorization/feature-authorization-guard/some-feature-authorization.guard.spec.ts +++ b/src/app/core/data/feature-authorization/feature-authorization-guard/some-feature-authorization.guard.spec.ts @@ -1,7 +1,10 @@ import { - ActivatedRouteSnapshot, + TestBed, + waitForAsync, +} from '@angular/core/testing'; +import { Router, - RouterStateSnapshot, + UrlTree, } from '@angular/router'; import { Observable, @@ -11,37 +14,9 @@ import { import { AuthService } from '../../../auth/auth.service'; import { AuthorizationDataService } from '../authorization-data.service'; import { FeatureID } from '../feature-id'; -import { SomeFeatureAuthorizationGuard } from './some-feature-authorization.guard'; - -/** - * Test implementation of abstract class SomeFeatureAuthorizationGuard - * Provide the return values of the overwritten getters as constructor arguments - */ -class SomeFeatureAuthorizationGuardImpl extends SomeFeatureAuthorizationGuard { - constructor(protected authorizationService: AuthorizationDataService, - protected router: Router, - protected authService: AuthService, - protected featureIds: FeatureID[], - protected objectUrl: string, - protected ePersonUuid: string) { - super(authorizationService, router, authService); - } - - getFeatureIDs(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { - return observableOf(this.featureIds); - } - - getObjectUrl(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { - return observableOf(this.objectUrl); - } - - getEPersonUuid(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { - return observableOf(this.ePersonUuid); - } -} +import { someFeatureAuthorizationGuard } from './some-feature-authorization.guard'; describe('SomeFeatureAuthorizationGuard', () => { - let guard: SomeFeatureAuthorizationGuard; let authorizationService: AuthorizationDataService; let router: Router; let authService: AuthService; @@ -62,18 +37,27 @@ describe('SomeFeatureAuthorizationGuard', () => { return observableOf(authorizedFeatureIds.indexOf(featureId) > -1); }, }); + router = jasmine.createSpyObj('router', { parseUrl: {}, }); + authService = jasmine.createSpyObj('authService', { isAuthenticated: observableOf(true), }); - guard = new SomeFeatureAuthorizationGuardImpl(authorizationService, router, authService, featureIds, objectUrl, ePersonUuid); + + TestBed.configureTestingModule({ + providers: [ + { provide: AuthorizationDataService, useValue: authorizationService }, + { provide: Router, useValue: router }, + { provide: AuthService, useValue: authService }, + ], + }); } - beforeEach(() => { + beforeEach(waitForAsync(() => { init(); - }); + })); describe('canActivate', () => { describe('when the user isn\'t authorized', () => { @@ -82,7 +66,16 @@ describe('SomeFeatureAuthorizationGuard', () => { }); it('should not return true', (done) => { - guard.canActivate(undefined, { url: 'current-url' } as any).subscribe((result) => { + + const result$ = TestBed.runInInjectionContext(() => { + return someFeatureAuthorizationGuard( + () => observableOf(featureIds), + () => observableOf(objectUrl), + () => observableOf(ePersonUuid), + )(undefined, { url: 'current-url' } as any); + }) as Observable; + + result$.subscribe((result) => { expect(result).not.toEqual(true); done(); }); @@ -95,7 +88,16 @@ describe('SomeFeatureAuthorizationGuard', () => { }); it('should return true', (done) => { - guard.canActivate(undefined, { url: 'current-url' } as any).subscribe((result) => { + + const result$ = TestBed.runInInjectionContext(() => { + return someFeatureAuthorizationGuard( + () => observableOf(featureIds), + () => observableOf(objectUrl), + () => observableOf(ePersonUuid), + )(undefined, { url: 'current-url' } as any); + }) as Observable; + + result$.subscribe((result) => { expect(result).toEqual(true); done(); }); @@ -108,7 +110,16 @@ describe('SomeFeatureAuthorizationGuard', () => { }); it('should return true', (done) => { - guard.canActivate(undefined, { url: 'current-url' } as any).subscribe((result) => { + + const result$ = TestBed.runInInjectionContext(() => { + return someFeatureAuthorizationGuard( + () => observableOf(featureIds), + () => observableOf(objectUrl), + () => observableOf(ePersonUuid), + )(undefined, { url: 'current-url' } as any); + }) as Observable; + + result$.subscribe((result) => { expect(result).toEqual(true); done(); }); diff --git a/src/app/core/data/feature-authorization/feature-authorization-guard/some-feature-authorization.guard.ts b/src/app/core/data/feature-authorization/feature-authorization-guard/some-feature-authorization.guard.ts index 0849c5a96a..53e5e582eb 100644 --- a/src/app/core/data/feature-authorization/feature-authorization-guard/some-feature-authorization.guard.ts +++ b/src/app/core/data/feature-authorization/feature-authorization-guard/some-feature-authorization.guard.ts @@ -1,6 +1,7 @@ +import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, - CanActivate, + CanActivateFn, Router, RouterStateSnapshot, UrlTree, @@ -17,49 +18,39 @@ import { returnForbiddenUrlTreeOrLoginOnAllFalse } from '../../../shared/authori import { AuthorizationDataService } from '../authorization-data.service'; import { FeatureID } from '../feature-id'; +export declare type SomeFeatureGuardParamFn = (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => Observable; +export declare type StringGuardParamFn = (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => Observable; +export const defaultStringGuardParamFn = () => observableOf(undefined); + /** - * Abstract Guard for preventing unauthorized activating and loading of routes when a user - * doesn't have authorized rights on any of the specified features and/or object. - * Override the desired getters in the parent class for checking specific authorization on a list of features and/or object. + * Guard for preventing unauthorized activating and loading of routes when a user doesn't have + * authorized rights on any of the specified features and/or object. + + * @param getFeatureIDs The features to check authorization for + * @param getObjectUrl The URL of the object to check if the user has authorized rights for, + * Optional, if not provided, the {@link Site}'s URL will be assumed + * @param getEPersonUuid The UUID of the user to check authorization rights for. + * Optional, if not provided, the authenticated user's UUID will be assumed. */ -export abstract class SomeFeatureAuthorizationGuard implements CanActivate { - constructor(protected authorizationService: AuthorizationDataService, - protected router: Router, - protected authService: AuthService) { - } - - /** - * True when user has authorization rights for the feature and object provided - * Redirect the user to the unauthorized page when they are not authorized for the given feature - */ - canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { - return observableCombineLatest(this.getFeatureIDs(route, state), this.getObjectUrl(route, state), this.getEPersonUuid(route, state)).pipe( - switchMap(([featureIDs, objectUrl, ePersonUuid]) => - observableCombineLatest(...featureIDs.map((featureID) => this.authorizationService.isAuthorized(featureID, objectUrl, ePersonUuid))), +export const someFeatureAuthorizationGuard = ( + getFeatureIDs: SomeFeatureGuardParamFn, + getObjectUrl: StringGuardParamFn = defaultStringGuardParamFn, + getEPersonUuid: StringGuardParamFn = defaultStringGuardParamFn, +): CanActivateFn => { + return (route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable => { + const authorizationService = inject(AuthorizationDataService); + const router = inject(Router); + const authService = inject(AuthService); + return observableCombineLatest([ + getFeatureIDs(route, state), + getObjectUrl(route, state), + getEPersonUuid(route, state), + ]).pipe( + switchMap(([featureIDs, objectUrl, ePersonUuid]: [FeatureID[], string, string]) => + observableCombineLatest(featureIDs.map((featureID) => authorizationService.isAuthorized(featureID, objectUrl, ePersonUuid))), ), - returnForbiddenUrlTreeOrLoginOnAllFalse(this.router, this.authService, state.url), + returnForbiddenUrlTreeOrLoginOnAllFalse(router, authService, state.url), ); - } + }; +}; - /** - * The features to check authorization for - * Override this method to define a list of features - */ - abstract getFeatureIDs(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable; - - /** - * The URL of the object to check if the user has authorized rights for - * Override this method to define an object URL. If not provided, the {@link Site}'s URL will be used - */ - getObjectUrl(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { - return observableOf(undefined); - } - - /** - * The UUID of the user to check authorization rights for - * Override this method to define an {@link EPerson} UUID. If not provided, the authenticated user's UUID will be used. - */ - getEPersonUuid(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { - return observableOf(undefined); - } -} diff --git a/src/app/core/data/feature-authorization/feature-authorization-guard/statistics-administrator.guard.ts b/src/app/core/data/feature-authorization/feature-authorization-guard/statistics-administrator.guard.ts index b301d550a1..21cafeaba3 100644 --- a/src/app/core/data/feature-authorization/feature-authorization-guard/statistics-administrator.guard.ts +++ b/src/app/core/data/feature-authorization/feature-authorization-guard/statistics-administrator.guard.ts @@ -1,35 +1,12 @@ -import { Injectable } from '@angular/core'; -import { - ActivatedRouteSnapshot, - Router, - RouterStateSnapshot, -} from '@angular/router'; -import { - Observable, - of as observableOf, -} from 'rxjs'; +import { CanActivateFn } from '@angular/router'; +import { of as observableOf } from 'rxjs'; -import { AuthService } from '../../../auth/auth.service'; -import { AuthorizationDataService } from '../authorization-data.service'; import { FeatureID } from '../feature-id'; -import { SingleFeatureAuthorizationGuard } from './single-feature-authorization.guard'; +import { singleFeatureAuthorizationGuard } from './single-feature-authorization.guard'; /** * Prevent unauthorized activating and loading of routes when the current authenticated user doesn't have group * management rights */ -@Injectable({ - providedIn: 'root', -}) -export class StatisticsAdministratorGuard extends SingleFeatureAuthorizationGuard { - constructor(protected authorizationService: AuthorizationDataService, protected router: Router, protected authService: AuthService) { - super(authorizationService, router, authService); - } - - /** - * Check group management rights - */ - getFeatureID(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { - return observableOf(FeatureID.CanViewUsageStatistics); - } -} +export const statisticsAdministratorGuard: CanActivateFn = + singleFeatureAuthorizationGuard(() => observableOf(FeatureID.CanViewUsageStatistics)); diff --git a/src/app/core/data/feature-authorization/feature-data.service.ts b/src/app/core/data/feature-authorization/feature-data.service.ts index 184e8c0be9..191d8b002c 100644 --- a/src/app/core/data/feature-authorization/feature-data.service.ts +++ b/src/app/core/data/feature-authorization/feature-data.service.ts @@ -3,17 +3,14 @@ import { Injectable } from '@angular/core'; import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service'; import { ObjectCacheService } from '../../cache/object-cache.service'; import { Feature } from '../../shared/feature.model'; -import { FEATURE } from '../../shared/feature.resource-type'; import { HALEndpointService } from '../../shared/hal-endpoint.service'; import { BaseDataService } from '../base/base-data.service'; -import { dataService } from '../base/data-service.decorator'; import { RequestService } from '../request.service'; /** * A service to retrieve {@link Feature}s from the REST API */ -@Injectable() -@dataService(FEATURE) +@Injectable({ providedIn: 'root' }) export class FeatureDataService extends BaseDataService { protected linkPath = 'features'; diff --git a/src/app/core/data/filtered-discovery-page-response-parsing.service.ts b/src/app/core/data/filtered-discovery-page-response-parsing.service.ts index fed0d70c58..e07f46e916 100644 --- a/src/app/core/data/filtered-discovery-page-response-parsing.service.ts +++ b/src/app/core/data/filtered-discovery-page-response-parsing.service.ts @@ -14,7 +14,7 @@ import { RestRequest } from './rest-request.model'; * A ResponseParsingService used to parse RawRestResponse coming from the REST API to a discovery query (string) * wrapped in a FilteredDiscoveryQueryResponse */ -@Injectable() +@Injectable({ providedIn: 'root' }) export class FilteredDiscoveryPageResponseParsingService extends BaseResponseParsingService implements ResponseParsingService { objectFactory = {}; toCache = false; diff --git a/src/app/core/data/href-only-data.service.ts b/src/app/core/data/href-only-data.service.ts index 83fb431e9b..dd40be8f7d 100644 --- a/src/app/core/data/href-only-data.service.ts +++ b/src/app/core/data/href-only-data.service.ts @@ -6,11 +6,7 @@ import { RemoteDataBuildService } from '../cache/builders/remote-data-build.serv import { CacheableObject } from '../cache/cacheable-object.model'; import { ObjectCacheService } from '../cache/object-cache.service'; import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { ITEM_TYPE } from '../shared/item-relationships/item-type.resource-type'; -import { LICENSE } from '../shared/license.resource-type'; -import { VOCABULARY_ENTRY } from '../submission/vocabularies/models/vocabularies.resource-type'; import { BaseDataService } from './base/base-data.service'; -import { dataService } from './base/data-service.decorator'; import { HALDataService } from './base/hal-data-service.interface'; import { FindListOptions } from './find-list-options.model'; import { PaginatedList } from './paginated-list.model'; @@ -33,12 +29,7 @@ import { RequestService } from './request.service'; * ``` * This means we cannot extend from {@link BaseDataService} directly because the method signatures would not match. */ -@Injectable({ - providedIn: 'root', -}) -@dataService(VOCABULARY_ENTRY) -@dataService(ITEM_TYPE) -@dataService(LICENSE) +@Injectable({ providedIn: 'root' }) export class HrefOnlyDataService implements HALDataService { /** * Works with a {@link BaseDataService} internally, but only exposes two of its methods diff --git a/src/app/core/data/identifier-data.service.ts b/src/app/core/data/identifier-data.service.ts index bf6172fd47..502b1fe710 100644 --- a/src/app/core/data/identifier-data.service.ts +++ b/src/app/core/data/identifier-data.service.ts @@ -13,7 +13,6 @@ import { import { NotificationsService } from '../../shared/notifications/notifications.service'; import { IdentifierData } from '../../shared/object-list/identifier-data/identifier-data.model'; -import { IDENTIFIERS } from '../../shared/object-list/identifier-data/identifier-data.resource-type'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { ObjectCacheService } from '../cache/object-cache.service'; import { CoreState } from '../core-state.model'; @@ -24,7 +23,6 @@ import { Item } from '../shared/item.model'; import { getFirstCompletedRemoteData } from '../shared/operators'; import { sendRequest } from '../shared/request.operators'; import { BaseDataService } from './base/base-data.service'; -import { dataService } from './base/data-service.decorator'; import { ConfigurationDataService } from './configuration-data.service'; import { DefaultChangeAnalyzer } from './default-change-analyzer.service'; import { RemoteData } from './remote-data'; @@ -37,8 +35,7 @@ import { RestRequest } from './rest-request.model'; * from the /identifiers endpoint, as well as the backend configuration that controls whether a 'Register DOI' * button appears for admins in the item status page */ -@Injectable() -@dataService(IDENTIFIERS) +@Injectable({ providedIn: 'root' }) export class IdentifierDataService extends BaseDataService { constructor( diff --git a/src/app/core/data/item-data.service.spec.ts b/src/app/core/data/item-data.service.spec.ts index 915c065a9e..dd60d94070 100644 --- a/src/app/core/data/item-data.service.spec.ts +++ b/src/app/core/data/item-data.service.spec.ts @@ -6,11 +6,11 @@ import { } from 'jasmine-marbles'; import { of as observableOf } from 'rxjs'; import { TestScheduler } from 'rxjs/testing'; -import { HALEndpointServiceStub } from 'src/app/shared/testing/hal-endpoint-service.stub'; import { getMockRemoteDataBuildService } from '../../shared/mocks/remote-data-build.service.mock'; import { getMockRequestService } from '../../shared/mocks/request.service.mock'; import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub'; import { BrowseService } from '../browse/browse.service'; import { ObjectCacheService } from '../cache/object-cache.service'; import { RestResponse } from '../cache/response.models'; diff --git a/src/app/core/data/item-data.service.ts b/src/app/core/data/item-data.service.ts index f071f6e1b8..e1f789b5da 100644 --- a/src/app/core/data/item-data.service.ts +++ b/src/app/core/data/item-data.service.ts @@ -37,7 +37,6 @@ import { ExternalSourceEntry } from '../shared/external-source-entry.model'; import { GenericConstructor } from '../shared/generic-constructor'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { Item } from '../shared/item.model'; -import { ITEM } from '../shared/item.resource-type'; import { MetadataMap } from '../shared/metadata.models'; import { NoContent } from '../shared/NoContent.model'; import { sendRequest } from '../shared/request.operators'; @@ -46,7 +45,6 @@ import { CreateData, CreateDataImpl, } from './base/create-data'; -import { dataService } from './base/data-service.decorator'; import { DeleteData, DeleteDataImpl, @@ -432,8 +430,7 @@ export abstract class BaseItemDataService extends IdentifiableDataService /** * A service for CRUD operations on Items */ -@Injectable() -@dataService(ITEM) +@Injectable({ providedIn: 'root' }) export class ItemDataService extends BaseItemDataService { constructor( protected requestService: RequestService, diff --git a/src/app/core/data/item-template-data.service.ts b/src/app/core/data/item-template-data.service.ts index 0ee362c71a..f89a297fad 100644 --- a/src/app/core/data/item-template-data.service.ts +++ b/src/app/core/data/item-template-data.service.ts @@ -64,7 +64,7 @@ class CollectionItemTemplateDataService extends IdentifiableDataService { /** * A service responsible for fetching/sending data from/to the REST API on a collection's itemtemplates endpoint */ -@Injectable() +@Injectable({ providedIn: 'root' }) export class ItemTemplateDataService extends BaseItemDataService { private byCollection: CollectionItemTemplateDataService; diff --git a/src/app/core/data/lookup-relation.service.ts b/src/app/core/data/lookup-relation.service.ts index 31b9ed845c..55b68afa65 100644 --- a/src/app/core/data/lookup-relation.service.ts +++ b/src/app/core/data/lookup-relation.service.ts @@ -34,7 +34,7 @@ import { RequestService } from './request.service'; /** * A service for retrieving local and external entries information during a relation lookup */ -@Injectable() +@Injectable({ providedIn: 'root' }) export class LookupRelationService { /** * The search config last used for retrieving local results diff --git a/src/app/core/data/metadata-field-data.service.ts b/src/app/core/data/metadata-field-data.service.ts index c6a08fa82b..aa79ef517e 100644 --- a/src/app/core/data/metadata-field-data.service.ts +++ b/src/app/core/data/metadata-field-data.service.ts @@ -9,7 +9,6 @@ import { RemoteDataBuildService } from '../cache/builders/remote-data-build.serv import { RequestParam } from '../cache/models/request-param.model'; import { ObjectCacheService } from '../cache/object-cache.service'; import { MetadataField } from '../metadata/metadata-field.model'; -import { METADATA_FIELD } from '../metadata/metadata-field.resource-type'; import { MetadataSchema } from '../metadata/metadata-schema.model'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { NoContent } from '../shared/NoContent.model'; @@ -17,7 +16,6 @@ import { CreateData, CreateDataImpl, } from './base/create-data'; -import { dataService } from './base/data-service.decorator'; import { DeleteData, DeleteDataImpl, @@ -39,8 +37,7 @@ import { RequestService } from './request.service'; /** * A service responsible for fetching/sending data from/to the REST API on the metadatafields endpoint */ -@Injectable() -@dataService(METADATA_FIELD) +@Injectable({ providedIn: 'root' }) export class MetadataFieldDataService extends IdentifiableDataService implements CreateData, PutData, DeleteData, SearchData { private createData: CreateData; private searchData: SearchData; diff --git a/src/app/core/data/metadata-schema-data.service.ts b/src/app/core/data/metadata-schema-data.service.ts index 8054dcf657..e893cb5404 100644 --- a/src/app/core/data/metadata-schema-data.service.ts +++ b/src/app/core/data/metadata-schema-data.service.ts @@ -8,14 +8,12 @@ import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { ObjectCacheService } from '../cache/object-cache.service'; import { MetadataSchema } from '../metadata/metadata-schema.model'; -import { METADATA_SCHEMA } from '../metadata/metadata-schema.resource-type'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { NoContent } from '../shared/NoContent.model'; import { CreateData, CreateDataImpl, } from './base/create-data'; -import { dataService } from './base/data-service.decorator'; import { DeleteData, DeleteDataImpl, @@ -37,8 +35,7 @@ import { RequestService } from './request.service'; /** * A service responsible for fetching/sending data from/to the REST API on the metadataschemas endpoint */ -@Injectable() -@dataService(METADATA_SCHEMA) +@Injectable({ providedIn: 'root' }) export class MetadataSchemaDataService extends IdentifiableDataService implements FindAllData, DeleteData { private createData: CreateData; private findAllData: FindAllData; diff --git a/src/app/core/data/mydspace-response-parsing.service.ts b/src/app/core/data/mydspace-response-parsing.service.ts index 8e7532a58e..f824be7f56 100644 --- a/src/app/core/data/mydspace-response-parsing.service.ts +++ b/src/app/core/data/mydspace-response-parsing.service.ts @@ -12,7 +12,7 @@ import { import { DspaceRestResponseParsingService } from './dspace-rest-response-parsing.service'; import { RestRequest } from './rest-request.model'; -@Injectable() +@Injectable({ providedIn: 'root' }) export class MyDSpaceResponseParsingService extends DspaceRestResponseParsingService { parse(request: RestRequest, data: RawRestResponse): ParsedResponse { // fallback for unexpected empty response diff --git a/src/app/core/data/notify-services-status-data.service.ts b/src/app/core/data/notify-services-status-data.service.ts index ad0c32fadd..deeaad967a 100644 --- a/src/app/core/data/notify-services-status-data.service.ts +++ b/src/app/core/data/notify-services-status-data.service.ts @@ -6,18 +6,15 @@ import { } from 'rxjs'; import { NotifyRequestsStatus } from '../../item-page/simple/notify-requests-status/notify-requests-status.model'; -import { NOTIFYREQUEST } from '../../item-page/simple/notify-requests-status/notify-requests-status.resource-type'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { ObjectCacheService } from '../cache/object-cache.service'; import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { dataService } from './base/data-service.decorator'; import { IdentifiableDataService } from './base/identifiable-data.service'; import { RemoteData } from './remote-data'; import { GetRequest } from './request.models'; import { RequestService } from './request.service'; -@Injectable() -@dataService(NOTIFYREQUEST) +@Injectable({ providedIn: 'root' }) export class NotifyRequestsStatusDataService extends IdentifiableDataService { constructor( diff --git a/src/app/core/data/object-updates/object-updates.reducer.ts b/src/app/core/data/object-updates/object-updates.reducer.ts index e014889850..cadae9ae83 100644 --- a/src/app/core/data/object-updates/object-updates.reducer.ts +++ b/src/app/core/data/object-updates/object-updates.reducer.ts @@ -61,6 +61,8 @@ export interface VirtualMetadataSource { export interface RelationshipIdentifiable extends Identifiable { nameVariant?: string; + originalItem: Item; + originalIsLeft: boolean relatedItem: Item; relationship: Relationship; type: RelationshipType; diff --git a/src/app/core/data/object-updates/object-updates.service.spec.ts b/src/app/core/data/object-updates/object-updates.service.spec.ts index 21486a581c..6602cda080 100644 --- a/src/app/core/data/object-updates/object-updates.service.spec.ts +++ b/src/app/core/data/object-updates/object-updates.service.spec.ts @@ -1,5 +1,6 @@ import { Injector } from '@angular/core'; import { Store } from '@ngrx/store'; +import { createMockStore } from '@ngrx/store/testing'; import { of as observableOf } from 'rxjs'; import { Notification } from '../../../shared/notifications/models/notification.model'; @@ -52,7 +53,7 @@ describe('ObjectUpdatesService', () => { const objectEntry = { fieldStates, fieldUpdates, lastModified: modDate, virtualMetadataSources: {}, patchOperationService, }; - store = new Store(undefined, undefined, undefined); + store = createMockStore({}); spyOn(store, 'dispatch'); injector = jasmine.createSpyObj('injector', { get: patchOperationService, diff --git a/src/app/core/data/object-updates/object-updates.service.stub.ts b/src/app/core/data/object-updates/object-updates.service.stub.ts new file mode 100644 index 0000000000..c41728a338 --- /dev/null +++ b/src/app/core/data/object-updates/object-updates.service.stub.ts @@ -0,0 +1,28 @@ +export class ObjectUpdatesServiceStub { + + initialize = jasmine.createSpy('initialize'); + saveFieldUpdate = jasmine.createSpy('saveFieldUpdate'); + getObjectEntry = jasmine.createSpy('getObjectEntry'); + getFieldState = jasmine.createSpy('getFieldState'); + getFieldUpdates = jasmine.createSpy('getFieldUpdates'); + getFieldUpdatesExclusive = jasmine.createSpy('getFieldUpdatesExclusive'); + isValid = jasmine.createSpy('isValid'); + isValidPage = jasmine.createSpy('isValidPage'); + saveAddFieldUpdate = jasmine.createSpy('saveAddFieldUpdate'); + saveRemoveFieldUpdate = jasmine.createSpy('saveRemoveFieldUpdate'); + saveChangeFieldUpdate = jasmine.createSpy('saveChangeFieldUpdate'); + isSelectedVirtualMetadata = jasmine.createSpy('isSelectedVirtualMetadata'); + setSelectedVirtualMetadata = jasmine.createSpy('setSelectedVirtualMetadata'); + setEditableFieldUpdate = jasmine.createSpy('setEditableFieldUpdate'); + setValidFieldUpdate = jasmine.createSpy('setValidFieldUpdate'); + discardFieldUpdates = jasmine.createSpy('discardFieldUpdates'); + discardAllFieldUpdates = jasmine.createSpy('discardAllFieldUpdates'); + reinstateFieldUpdates = jasmine.createSpy('reinstateFieldUpdates'); + removeSingleFieldUpdate = jasmine.createSpy('removeSingleFieldUpdate'); + getUpdateFields = jasmine.createSpy('getUpdateFields'); + hasUpdates = jasmine.createSpy('hasUpdates'); + isReinstatable = jasmine.createSpy('isReinstatable'); + getLastModified = jasmine.createSpy('getLastModified'); + createPatch = jasmine.createSpy('getPatch'); + +} diff --git a/src/app/core/data/object-updates/object-updates.service.ts b/src/app/core/data/object-updates/object-updates.service.ts index b526e7e980..75b554d87b 100644 --- a/src/app/core/data/object-updates/object-updates.service.ts +++ b/src/app/core/data/object-updates/object-updates.service.ts @@ -15,6 +15,7 @@ import { filter, map, switchMap, + take, } from 'rxjs/operators'; import { @@ -69,7 +70,7 @@ function virtualMetadataSourceSelector(url: string, source: string): MemoizedSel /** * Service that dispatches and reads from the ObjectUpdates' state in the store */ -@Injectable() +@Injectable({ providedIn: 'root' }) export class ObjectUpdatesService { constructor(private store: Store, private injector: Injector) { @@ -212,8 +213,14 @@ export class ObjectUpdatesService { * @param url The page's URL for which the changes are saved * @param field An updated field for the page's object */ - saveAddFieldUpdate(url: string, field: Identifiable) { + saveAddFieldUpdate(url: string, field: Identifiable): Observable { + const update$: Observable = this.getFieldUpdatesExclusive(url, [field]).pipe( + filter((fieldUpdates: FieldUpdates) => fieldUpdates[field.uuid].changeType === FieldChangeType.ADD), + take(1), + map(() => true), + ); this.saveFieldUpdate(url, field, FieldChangeType.ADD); + return update$; } /** @@ -221,8 +228,14 @@ export class ObjectUpdatesService { * @param url The page's URL for which the changes are saved * @param field An updated field for the page's object */ - saveRemoveFieldUpdate(url: string, field: Identifiable) { + saveRemoveFieldUpdate(url: string, field: Identifiable): Observable { + const update$: Observable = this.getFieldUpdatesExclusive(url, [field]).pipe( + filter((fieldUpdates: FieldUpdates) => fieldUpdates[field.uuid].changeType === FieldChangeType.REMOVE), + take(1), + map(() => true), + ); this.saveFieldUpdate(url, field, FieldChangeType.REMOVE); + return update$; } /** diff --git a/src/app/core/data/paginated-list.model.ts b/src/app/core/data/paginated-list.model.ts index e412af4986..ef2819afd8 100644 --- a/src/app/core/data/paginated-list.model.ts +++ b/src/app/core/data/paginated-list.model.ts @@ -73,13 +73,13 @@ export class PaginatedList extends CacheableObject { * The type of the list */ @excludeFromEquals - type = PAGINATED_LIST; + type = PAGINATED_LIST; /** * The type of objects in the list */ @autoserialize - objectType?: ResourceType; + objectType?: ResourceType; /** * The list of objects that represents the current page @@ -90,13 +90,13 @@ export class PaginatedList extends CacheableObject { * the {@link PageInfo} object */ @autoserialize - pageInfo?: PageInfo; + pageInfo?: PageInfo; /** * The {@link HALLink}s for this PaginatedList */ @deserialize - _links: { + _links: { self: HALLink; page: HALLink[]; first?: HALLink; diff --git a/src/app/core/data/processes/process-data.service.ts b/src/app/core/data/processes/process-data.service.ts index ae0dfc11fa..b39a500c80 100644 --- a/src/app/core/data/processes/process-data.service.ts +++ b/src/app/core/data/processes/process-data.service.ts @@ -17,7 +17,6 @@ import { import { ProcessStatus } from 'src/app/process-page/processes/process-status.model'; import { Process } from '../../../process-page/processes/process.model'; -import { PROCESS } from '../../../process-page/processes/process.resource-type'; import { hasValue } from '../../../shared/empty.util'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { FollowLinkConfig } from '../../../shared/utils/follow-link-config.model'; @@ -27,7 +26,6 @@ import { Bitstream } from '../../shared/bitstream.model'; import { HALEndpointService } from '../../shared/hal-endpoint.service'; import { NoContent } from '../../shared/NoContent.model'; import { getAllCompletedRemoteData } from '../../shared/operators'; -import { dataService } from '../base/data-service.decorator'; import { DeleteData, DeleteDataImpl, @@ -56,10 +54,8 @@ export const TIMER_FACTORY = new InjectionToken<(callback: (...args: any[]) => v factory: () => setTimeout, }); -@Injectable() -@dataService(PROCESS) +@Injectable({ providedIn: 'root' }) export class ProcessDataService extends IdentifiableDataService implements FindAllData, DeleteData, SearchData { - private findAllData: FindAllData; private deleteData: DeleteData; private searchData: SearchData; diff --git a/src/app/core/data/processes/script-data.service.ts b/src/app/core/data/processes/script-data.service.ts index 901a92e818..e61da4db63 100644 --- a/src/app/core/data/processes/script-data.service.ts +++ b/src/app/core/data/processes/script-data.service.ts @@ -8,7 +8,6 @@ import { import { Process } from '../../../process-page/processes/process.model'; import { ProcessParameter } from '../../../process-page/processes/process-parameter.model'; import { Script } from '../../../process-page/scripts/script.model'; -import { SCRIPT } from '../../../process-page/scripts/script.resource-type'; import { hasValue } from '../../../shared/empty.util'; import { FollowLinkConfig } from '../../../shared/utils/follow-link-config.model'; import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service'; @@ -16,7 +15,6 @@ import { ObjectCacheService } from '../../cache/object-cache.service'; import { HALEndpointService } from '../../shared/hal-endpoint.service'; import { getFirstCompletedRemoteData } from '../../shared/operators'; import { URLCombiner } from '../../url-combiner/url-combiner'; -import { dataService } from '../base/data-service.decorator'; import { FindAllData, FindAllDataImpl, @@ -34,8 +32,7 @@ export const METADATA_EXPORT_SCRIPT_NAME = 'metadata-export'; export const BATCH_IMPORT_SCRIPT_NAME = 'import'; export const BATCH_EXPORT_SCRIPT_NAME = 'export'; -@Injectable() -@dataService(SCRIPT) +@Injectable({ providedIn: 'root' }) export class ScriptDataService extends IdentifiableDataService'">`, + standalone: true, + imports: [ MarkdownDirective ], +}) +class TestComponent {} + +describe('MarkdownDirective', () => { + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + providers: [ + { provide: MathService, useClass: MockMathService }, + ], + }).compileComponents(); + spyOn(MarkdownDirective.prototype, 'render'); + fixture = TestBed.createComponent(TestComponent); + }); + + it('should call render method', () => { + fixture.detectChanges(); + expect(MarkdownDirective.prototype.render).toHaveBeenCalled(); + }); + +}); + +describe('MarkdownDirective sanitization with markdown disabled', () => { + let fixture: ComponentFixture; + let divEl: DebugElement; + // Disable markdown + environment.markdown.enabled = false; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + providers: [ + { provide: MathService, useClass: MockMathService }, + ], + }).compileComponents(); + fixture = TestBed.createComponent(TestComponent); + divEl = fixture.debugElement.query(By.css('div')); + + }); + + it('should sanitize the script element out of innerHTML (markdown disabled)',() => { + fixture.detectChanges(); + divEl = fixture.debugElement.query(By.css('div')); + expect(divEl.nativeElement.innerHTML).toEqual('test'); + }); + +}); + +describe('MarkdownDirective sanitization with markdown enabled', () => { + let fixture: ComponentFixture; + let divEl: DebugElement; + // Enable markdown + environment.markdown.enabled = true; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + providers: [ + { provide: MathService, useClass: MockMathService }, + ], + }).compileComponents(); + fixture = TestBed.createComponent(TestComponent); + divEl = fixture.debugElement.query(By.css('div')); + + }); + + it('should sanitize the script element out of innerHTML (markdown enabled)',() => { + fixture.detectChanges(); + divEl = fixture.debugElement.query(By.css('div')); + expect(divEl.nativeElement.innerHTML).toEqual('test'); + }); + +}); diff --git a/src/app/shared/utils/markdown.directive.ts b/src/app/shared/utils/markdown.directive.ts new file mode 100644 index 0000000000..de13acb303 --- /dev/null +++ b/src/app/shared/utils/markdown.directive.ts @@ -0,0 +1,97 @@ +import { + Directive, + ElementRef, + Inject, + InjectionToken, + Input, + OnDestroy, + OnInit, + SecurityContext, +} from '@angular/core'; +import { + DomSanitizer, + SafeHtml, +} from '@angular/platform-browser'; +import { Subject } from 'rxjs'; +import { + filter, + take, + takeUntil, +} from 'rxjs/operators'; + +import { environment } from '../../../environments/environment'; +import { MathService } from '../../core/shared/math.service'; +import { isEmpty } from '../empty.util'; + +const markdownItLoader = async () => (await import('markdown-it')).default; +type LazyMarkdownIt = ReturnType; +const MARKDOWN_IT = new InjectionToken( + 'Lazily loaded MarkdownIt', + { providedIn: 'root', factory: markdownItLoader }, +); + +@Directive({ + selector: '[dsMarkdown]', + standalone: true, +}) +export class MarkdownDirective implements OnInit, OnDestroy { + + @Input() dsMarkdown: string; + private alive$ = new Subject(); + + el: HTMLElement; + + constructor( + @Inject(MARKDOWN_IT) private markdownIt: LazyMarkdownIt, + protected sanitizer: DomSanitizer, + private mathService: MathService, + private elementRef: ElementRef) { + this.el = elementRef.nativeElement; + } + + ngOnInit() { + this.render(this.dsMarkdown); + } + + async render(value: string, forcePreview = false): Promise { + if (isEmpty(value) || (!environment.markdown.enabled && !forcePreview)) { + this.el.innerHTML = this.sanitizer.sanitize(SecurityContext.HTML, value); + return; + } else { + if (environment.markdown.mathjax) { + this.renderMathjaxThenMarkdown(value); + } else { + this.renderMarkdown(value); + } + } + } + + private renderMathjaxThenMarkdown(value: string) { + const sanitized = this.sanitizer.sanitize(SecurityContext.HTML, value); + this.el.innerHTML = sanitized; + this.mathService.ready().pipe( + filter((ready) => ready), + take(1), + takeUntil(this.alive$), + ).subscribe(() => { + this.mathService.render(this.el)?.then(_ => { + this.renderMarkdown(this.el.innerHTML, true); + }); + }); + } + + private async renderMarkdown(value: string, alreadySanitized = false) { + const MarkdownIt = await this.markdownIt; + const md = new MarkdownIt({ + html: true, + linkify: true, + }); + + const html = alreadySanitized ? md.render(value) : this.sanitizer.sanitize(SecurityContext.HTML, md.render(value)); + this.el.innerHTML = html; + } + + ngOnDestroy() { + this.alive$.next(false); + } +} diff --git a/src/app/shared/utils/markdown.pipe.spec.ts b/src/app/shared/utils/markdown.pipe.spec.ts deleted file mode 100644 index cf644767e2..0000000000 --- a/src/app/shared/utils/markdown.pipe.spec.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { TestBed } from '@angular/core/testing'; - -import { APP_CONFIG } from '../../../config/app-config.interface'; -import { environment } from '../../../environments/environment'; -import { MarkdownPipe } from './markdown.pipe'; - -describe('Markdown Pipe', () => { - - let markdownPipe: MarkdownPipe; - - beforeEach(() => { - TestBed.configureTestingModule({ - providers: [ - MarkdownPipe, - { - provide: APP_CONFIG, - useValue: Object.assign(environment, { - markdown: { - enabled: true, - mathjax: true, - }, - }), - }, - ], - }).compileComponents(); - - markdownPipe = TestBed.inject(MarkdownPipe); - }); - - it('should render markdown', async () => { - await testTransform( - '# Header', - '

Header

', - ); - }); - - it('should render mathjax', async () => { - await testTransform( - '$\\sqrt{2}^2$', - '.*', - ); - }); - - it('should render regular links', async () => { - await testTransform( - 'DSpace', - 'DSpace', - ); - }); - - it('should not render javascript links', async () => { - await testTransform( - 'exploit', - 'exploit', - ); - }); - - async function testTransform(input: string, output: string) { - expect( - await markdownPipe.transform(input), - ).toMatch( - new RegExp('.*' + output + '.*'), - ); - } -}); diff --git a/src/app/shared/utils/markdown.pipe.ts b/src/app/shared/utils/markdown.pipe.ts deleted file mode 100644 index d86a8d95ca..0000000000 --- a/src/app/shared/utils/markdown.pipe.ts +++ /dev/null @@ -1,124 +0,0 @@ -import { - Inject, - InjectionToken, - Pipe, - PipeTransform, - SecurityContext, -} from '@angular/core'; -import { - DomSanitizer, - SafeHtml, -} from '@angular/platform-browser'; - -import { environment } from '../../../environments/environment'; - -const markdownItLoader = async () => (await import('markdown-it')).default; -type LazyMarkdownIt = ReturnType; -const MARKDOWN_IT = new InjectionToken( - 'Lazily loaded MarkdownIt', - { providedIn: 'root', factory: markdownItLoader }, -); - -const mathjaxLoader = async () => (await import('markdown-it-mathjax3')).default; -type Mathjax = ReturnType; -const MATHJAX = new InjectionToken( - 'Lazily loaded mathjax', - { providedIn: 'root', factory: mathjaxLoader }, -); - -const sanitizeHtmlLoader = async () => (await import('sanitize-html') as any).default; -type SanitizeHtml = ReturnType; -const SANITIZE_HTML = new InjectionToken( - 'Lazily loaded sanitize-html', - { providedIn: 'root', factory: sanitizeHtmlLoader }, -); - -/** - * Pipe for rendering markdown and mathjax. - * - markdown will only be rendered if {@link MarkdownConfig#enabled} is true - * - mathjax will only be rendered if both {@link MarkdownConfig#enabled} and {@link MarkdownConfig#mathjax} are true - * - * This pipe should be used on the 'innerHTML' attribute of a component, in combination with an async pipe. - * Example usage: - * - * Result: - * - *

title

- *
- */ -@Pipe({ - name: 'dsMarkdown', -}) -export class MarkdownPipe implements PipeTransform { - - constructor( - protected sanitizer: DomSanitizer, - @Inject(MARKDOWN_IT) private markdownIt: LazyMarkdownIt, - @Inject(MATHJAX) private mathjax: Mathjax, - @Inject(SANITIZE_HTML) private sanitizeHtml: SanitizeHtml, - ) { - } - - async transform(value: string): Promise { - if (!environment.markdown.enabled) { - return value; - } - const MarkdownIt = await this.markdownIt; - const md = new MarkdownIt({ - html: true, - linkify: true, - }); - - let html: string; - if (environment.markdown.mathjax) { - md.use(await this.mathjax); - const sanitizeHtml = await this.sanitizeHtml; - html = sanitizeHtml(md.render(value), { - // sanitize-html doesn't let through SVG by default, so we extend its allowlists to cover MathJax SVG - allowedTags: [ - ...sanitizeHtml.defaults.allowedTags, - 'mjx-container', 'svg', 'g', 'path', 'rect', 'text', - // Also let the mjx-assistive-mml tag (and it's children) through (for screen readers) - 'mjx-assistive-mml', 'math', 'mrow', 'mi', - ], - allowedAttributes: { - ...sanitizeHtml.defaults.allowedAttributes, - 'mjx-container': [ - 'class', 'style', 'jax', - ], - svg: [ - 'xmlns', 'viewBox', 'style', 'width', 'height', 'role', 'focusable', 'alt', 'aria-label', - ], - g: [ - 'data-mml-node', 'style', 'stroke', 'fill', 'stroke-width', 'transform', - ], - path: [ - 'd', 'style', 'transform', - ], - rect: [ - 'width', 'height', 'x', 'y', 'transform', 'style', - ], - text: [ - 'transform', 'font-size', - ], - 'mjx-assistive-mml': [ - 'unselectable', 'display', 'style', - ], - math: [ - 'xmlns', - ], - mrow: [ - 'data-mjx-texclass', - ], - }, - parser: { - lowerCaseAttributeNames: false, - }, - }); - } else { - html = this.sanitizer.sanitize(SecurityContext.HTML, md.render(value)); - } - - return this.sanitizer.bypassSecurityTrustHtml(html); - } -} diff --git a/src/app/shared/utils/metadatafield-validator.directive.ts b/src/app/shared/utils/metadatafield-validator.directive.ts index badfad5b72..3112d4870b 100644 --- a/src/app/shared/utils/metadatafield-validator.directive.ts +++ b/src/app/shared/utils/metadatafield-validator.directive.ts @@ -34,6 +34,7 @@ import { getFirstSucceededRemoteData } from '../../core/shared/operators'; providers: [ { provide: NG_VALIDATORS, useExisting: MetadataFieldValidator, multi: true }, ], + standalone: true, }) @Injectable({ providedIn: 'root' }) export class MetadataFieldValidator implements AsyncValidator { diff --git a/src/app/shared/utils/object-keys-pipe.ts b/src/app/shared/utils/object-keys-pipe.ts index 095e076b8e..8ade3f236b 100644 --- a/src/app/shared/utils/object-keys-pipe.ts +++ b/src/app/shared/utils/object-keys-pipe.ts @@ -3,7 +3,10 @@ import { PipeTransform, } from '@angular/core'; -@Pipe({ name: 'dsObjectKeys' }) +@Pipe({ + name: 'dsObjectKeys', + standalone: true, +}) /** * Pipe for parsing all keys of an object to an array of key-value pairs */ diff --git a/src/app/shared/utils/object-ngfor.pipe.ts b/src/app/shared/utils/object-ngfor.pipe.ts index 0e2d65b43e..c52426fef4 100644 --- a/src/app/shared/utils/object-ngfor.pipe.ts +++ b/src/app/shared/utils/object-ngfor.pipe.ts @@ -13,6 +13,7 @@ import { */ @Pipe({ name: 'dsObjNgFor', + standalone: true, }) export class ObjNgFor implements PipeTransform { transform(value: any, args: any[] = null): any { diff --git a/src/app/shared/utils/object-values-pipe.ts b/src/app/shared/utils/object-values-pipe.ts index a72d443b7e..6b862afff9 100644 --- a/src/app/shared/utils/object-values-pipe.ts +++ b/src/app/shared/utils/object-values-pipe.ts @@ -8,6 +8,7 @@ import { isNotEmpty } from '../empty.util'; @Pipe({ name: 'dsObjectValues', pure: true, + standalone: true, }) /** * Pipe for parsing all values of an object to an array of values diff --git a/src/app/shared/utils/relation-query.utils.ts b/src/app/shared/utils/relation-query.utils.ts index e2f3a0eb6c..158744e78c 100644 --- a/src/app/shared/utils/relation-query.utils.ts +++ b/src/app/shared/utils/relation-query.utils.ts @@ -1,3 +1,4 @@ +import { Item } from '../../core/shared/item.model'; import { Relationship } from '../../core/shared/item-relationships/relationship.model'; import { followLink, @@ -24,19 +25,22 @@ export function getFilterByRelation(relationType: string, itemUUID: string): str } /** - * Creates links to follow for the leftItem and rightItem. Links will include - * @param showThumbnail thumbnail image configuration - * @returns followLink array + * Creates links to follow for the leftItem and rightItem. Optionally additional links for `thumbnail` & `accessStatus` + * can be embedded as well. + * + * @param showThumbnail Whether the `thumbnail` needs to be embedded on the {@link Item} + * @param showAccessStatus Whether the `accessStatus` needs to be embedded on the {@link Item} */ -export function itemLinksToFollow(showThumbnail: boolean): FollowLinkConfig[] { - let linksToFollow: FollowLinkConfig[]; +export function itemLinksToFollow(showThumbnail: boolean, showAccessStatus: boolean): FollowLinkConfig[] { + const conditionalLinksToFollow: FollowLinkConfig[] = []; if (showThumbnail) { - linksToFollow = [ - followLink('leftItem',{}, followLink('thumbnail')), - followLink('rightItem',{}, followLink('thumbnail')), - ]; - } else { - linksToFollow = [followLink('leftItem'), followLink('rightItem')]; + conditionalLinksToFollow.push(followLink('thumbnail')); } - return linksToFollow; + if (showAccessStatus) { + conditionalLinksToFollow.push(followLink('accessStatus')); + } + return [ + followLink('leftItem', undefined, ...conditionalLinksToFollow), + followLink('rightItem', undefined, ...conditionalLinksToFollow), + ]; } diff --git a/src/app/shared/utils/require-file.validator.ts b/src/app/shared/utils/require-file.validator.ts index c4ec59e160..6825f058dd 100644 --- a/src/app/shared/utils/require-file.validator.ts +++ b/src/app/shared/utils/require-file.validator.ts @@ -11,6 +11,7 @@ import { providers: [ { provide: NG_VALIDATORS, useExisting: FileValidator, multi: true }, ], + standalone: true, }) /** * Validator directive to validate if a file is selected diff --git a/src/app/shared/utils/safe-url-pipe.ts b/src/app/shared/utils/safe-url-pipe.ts index 1d04c7e08a..6cb9d9fb1b 100644 --- a/src/app/shared/utils/safe-url-pipe.ts +++ b/src/app/shared/utils/safe-url-pipe.ts @@ -9,7 +9,10 @@ import { DomSanitizer } from '@angular/platform-browser'; * only use this when you are sure the URL is indeed safe */ -@Pipe({ name: 'dsSafeUrl' }) +@Pipe({ + name: 'dsSafeUrl', + standalone: true, +}) export class SafeUrlPipe implements PipeTransform { constructor(private domSanitizer: DomSanitizer) { } transform(url) { diff --git a/src/app/shared/utils/short-number.pipe.ts b/src/app/shared/utils/short-number.pipe.ts index dc06e7f52a..f0b458755d 100644 --- a/src/app/shared/utils/short-number.pipe.ts +++ b/src/app/shared/utils/short-number.pipe.ts @@ -8,6 +8,7 @@ import { isEmpty } from '../empty.util'; @Pipe({ name: 'dsShortNumber', + standalone: true, }) export class ShortNumberPipe implements PipeTransform { diff --git a/src/app/shared/utils/split.pipe.ts b/src/app/shared/utils/split.pipe.ts index 2f6290322a..c84e7895f1 100644 --- a/src/app/shared/utils/split.pipe.ts +++ b/src/app/shared/utils/split.pipe.ts @@ -11,6 +11,7 @@ import { */ @Pipe({ name: 'dsSplit', + standalone: true, }) export class SplitPipe implements PipeTransform { transform(value: string, separator: string): string[] { diff --git a/src/app/shared/utils/truncate.pipe.ts b/src/app/shared/utils/truncate.pipe.ts index 35d94656ba..2955be8f5d 100644 --- a/src/app/shared/utils/truncate.pipe.ts +++ b/src/app/shared/utils/truncate.pipe.ts @@ -11,6 +11,7 @@ import { hasValue } from '../empty.util'; */ @Pipe({ name: 'dsTruncate', + standalone: true, }) export class TruncatePipe implements PipeTransform { diff --git a/src/app/shared/utils/var.directive.ts b/src/app/shared/utils/var.directive.ts index a5f6818214..eaa8a6fdc1 100644 --- a/src/app/shared/utils/var.directive.ts +++ b/src/app/shared/utils/var.directive.ts @@ -8,6 +8,7 @@ import { /* eslint-disable @angular-eslint/directive-selector */ @Directive({ selector: '[ngVar]', + standalone: true, }) export class VarDirective { @Input() diff --git a/src/app/shared/view-mode-switch/view-mode-switch.component.html b/src/app/shared/view-mode-switch/view-mode-switch.component.html index 5d29562a76..74ee22533d 100644 --- a/src/app/shared/view-mode-switch/view-mode-switch.component.html +++ b/src/app/shared/view-mode-switch/view-mode-switch.component.html @@ -6,7 +6,6 @@ [queryParams]="{view: 'list'}" queryParamsHandling="merge" (click)="switchViewTo(viewModeEnum.ListElement)" - routerLinkActive="active" [class.active]="currentMode === viewModeEnum.ListElement" class="btn btn-secondary" [attr.data-test]="'list-view' | dsBrowserOnly"> @@ -19,21 +18,21 @@ [queryParams]="{view: 'grid'}" queryParamsHandling="merge" (click)="switchViewTo(viewModeEnum.GridElement)" - routerLinkActive="active" [class.active]="currentMode === viewModeEnum.GridElement" class="btn btn-secondary" [attr.data-test]="'grid-view' | dsBrowserOnly"> diff --git a/src/app/shared/view-mode-switch/view-mode-switch.component.spec.ts b/src/app/shared/view-mode-switch/view-mode-switch.component.spec.ts index 90c5b64e72..e4d92bece4 100644 --- a/src/app/shared/view-mode-switch/view-mode-switch.component.spec.ts +++ b/src/app/shared/view-mode-switch/view-mode-switch.component.spec.ts @@ -19,11 +19,13 @@ import { import { SearchService } from '../../core/shared/search/search.service'; import { ViewMode } from '../../core/shared/view-mode.model'; import { TranslateLoaderMock } from '../mocks/translate-loader.mock'; -import { BrowserOnlyMockPipe } from '../testing/browser-only-mock.pipe'; import { SearchServiceStub } from '../testing/search-service.stub'; import { ViewModeSwitchComponent } from './view-mode-switch.component'; -@Component({ template: '' }) +@Component({ + template: '', + standalone: true, +}) class DummyComponent { } @@ -46,11 +48,8 @@ describe('ViewModeSwitchComponent', () => { RouterTestingModule.withRoutes([ { path: 'search', component: DummyComponent, pathMatch: 'full' }, ]), - ], - declarations: [ ViewModeSwitchComponent, DummyComponent, - BrowserOnlyMockPipe, ], providers: [ { provide: SearchService, useValue: searchService }, diff --git a/src/app/shared/view-mode-switch/view-mode-switch.component.ts b/src/app/shared/view-mode-switch/view-mode-switch.component.ts index b4b50462a1..baed3c9336 100644 --- a/src/app/shared/view-mode-switch/view-mode-switch.component.ts +++ b/src/app/shared/view-mode-switch/view-mode-switch.component.ts @@ -1,3 +1,4 @@ +import { NgIf } from '@angular/common'; import { Component, EventEmitter, @@ -6,7 +7,12 @@ import { OnInit, Output, } from '@angular/core'; -import { Router } from '@angular/router'; +import { + Router, + RouterLink, + RouterLinkActive, +} from '@angular/router'; +import { TranslateModule } from '@ngx-translate/core'; import { Subscription } from 'rxjs'; import { filter } from 'rxjs/operators'; @@ -16,6 +22,7 @@ import { isEmpty, isNotEmpty, } from '../empty.util'; +import { BrowserOnlyPipe } from '../utils/browser-only.pipe'; import { currentPath } from '../utils/route.utils'; /** @@ -25,6 +32,8 @@ import { currentPath } from '../utils/route.utils'; selector: 'ds-view-mode-switch', styleUrls: ['./view-mode-switch.component.scss'], templateUrl: './view-mode-switch.component.html', + standalone: true, + imports: [NgIf, RouterLink, RouterLinkActive, TranslateModule, BrowserOnlyPipe], }) export class ViewModeSwitchComponent implements OnInit, OnDestroy { diff --git a/src/app/statistics-page/collection-statistics-page/collection-statistics-page.component.spec.ts b/src/app/statistics-page/collection-statistics-page/collection-statistics-page.component.spec.ts index 241ab66405..c78fef0521 100644 --- a/src/app/statistics-page/collection-statistics-page/collection-statistics-page.component.spec.ts +++ b/src/app/statistics-page/collection-statistics-page/collection-statistics-page.component.spec.ts @@ -20,7 +20,6 @@ import { Collection } from '../../core/shared/collection.model'; import { UsageReport } from '../../core/statistics/models/usage-report.model'; import { UsageReportDataService } from '../../core/statistics/usage-report-data.service'; import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils'; -import { SharedModule } from '../../shared/shared.module'; import { StatisticsTableComponent } from '../statistics-table/statistics-table.component'; import { CollectionStatisticsPageComponent } from './collection-statistics-page.component'; @@ -73,9 +72,6 @@ describe('CollectionStatisticsPageComponent', () => { imports: [ TranslateModule.forRoot(), CommonModule, - SharedModule, - ], - declarations: [ CollectionStatisticsPageComponent, StatisticsTableComponent, ], diff --git a/src/app/statistics-page/collection-statistics-page/collection-statistics-page.component.ts b/src/app/statistics-page/collection-statistics-page/collection-statistics-page.component.ts index c8b51765e6..e07a506b7c 100644 --- a/src/app/statistics-page/collection-statistics-page/collection-statistics-page.component.ts +++ b/src/app/statistics-page/collection-statistics-page/collection-statistics-page.component.ts @@ -1,24 +1,24 @@ +import { CommonModule } from '@angular/common'; import { Component } from '@angular/core'; -import { - ActivatedRoute, - Router, -} from '@angular/router'; +import { TranslateModule } from '@ngx-translate/core'; -import { AuthService } from '../../core/auth/auth.service'; -import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; import { Collection } from '../../core/shared/collection.model'; -import { UsageReportDataService } from '../../core/statistics/usage-report-data.service'; -import { StatisticsPageComponent } from '../statistics-page/statistics-page.component'; +import { ThemedLoadingComponent } from '../../shared/loading/themed-loading.component'; +import { VarDirective } from '../../shared/utils/var.directive'; +import { StatisticsPageDirective } from '../statistics-page/statistics-page.directive'; +import { StatisticsTableComponent } from '../statistics-table/statistics-table.component'; /** * Component representing the statistics page for a collection. */ @Component({ - selector: 'ds-collection-statistics-page', + selector: 'ds-base-collection-statistics-page', templateUrl: '../statistics-page/statistics-page.component.html', styleUrls: ['./collection-statistics-page.component.scss'], + standalone: true, + imports: [CommonModule, VarDirective, ThemedLoadingComponent, StatisticsTableComponent, TranslateModule], }) -export class CollectionStatisticsPageComponent extends StatisticsPageComponent { +export class CollectionStatisticsPageComponent extends StatisticsPageDirective { /** * The report types to show on this statistics page. @@ -29,20 +29,4 @@ export class CollectionStatisticsPageComponent extends StatisticsPageComponent { protected getComponentName(): string { diff --git a/src/app/statistics-page/community-statistics-page/community-statistics-page.component.spec.ts b/src/app/statistics-page/community-statistics-page/community-statistics-page.component.spec.ts index a51e201043..e29e37880f 100644 --- a/src/app/statistics-page/community-statistics-page/community-statistics-page.component.spec.ts +++ b/src/app/statistics-page/community-statistics-page/community-statistics-page.component.spec.ts @@ -20,7 +20,6 @@ import { Community } from '../../core/shared/community.model'; import { UsageReport } from '../../core/statistics/models/usage-report.model'; import { UsageReportDataService } from '../../core/statistics/usage-report-data.service'; import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils'; -import { SharedModule } from '../../shared/shared.module'; import { StatisticsTableComponent } from '../statistics-table/statistics-table.component'; import { CommunityStatisticsPageComponent } from './community-statistics-page.component'; @@ -73,9 +72,6 @@ describe('CommunityStatisticsPageComponent', () => { imports: [ TranslateModule.forRoot(), CommonModule, - SharedModule, - ], - declarations: [ CommunityStatisticsPageComponent, StatisticsTableComponent, ], diff --git a/src/app/statistics-page/community-statistics-page/community-statistics-page.component.ts b/src/app/statistics-page/community-statistics-page/community-statistics-page.component.ts index f6f7e8c78c..72a2c46b9e 100644 --- a/src/app/statistics-page/community-statistics-page/community-statistics-page.component.ts +++ b/src/app/statistics-page/community-statistics-page/community-statistics-page.component.ts @@ -1,24 +1,24 @@ +import { CommonModule } from '@angular/common'; import { Component } from '@angular/core'; -import { - ActivatedRoute, - Router, -} from '@angular/router'; +import { TranslateModule } from '@ngx-translate/core'; -import { AuthService } from '../../core/auth/auth.service'; -import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; import { Community } from '../../core/shared/community.model'; -import { UsageReportDataService } from '../../core/statistics/usage-report-data.service'; -import { StatisticsPageComponent } from '../statistics-page/statistics-page.component'; +import { ThemedLoadingComponent } from '../../shared/loading/themed-loading.component'; +import { VarDirective } from '../../shared/utils/var.directive'; +import { StatisticsPageDirective } from '../statistics-page/statistics-page.directive'; +import { StatisticsTableComponent } from '../statistics-table/statistics-table.component'; /** * Component representing the statistics page for a community. */ @Component({ - selector: 'ds-community-statistics-page', + selector: 'ds-base-community-statistics-page', templateUrl: '../statistics-page/statistics-page.component.html', styleUrls: ['./community-statistics-page.component.scss'], + standalone: true, + imports: [CommonModule, VarDirective, ThemedLoadingComponent, StatisticsTableComponent, TranslateModule], }) -export class CommunityStatisticsPageComponent extends StatisticsPageComponent { +export class CommunityStatisticsPageComponent extends StatisticsPageDirective { /** * The report types to show on this statistics page. @@ -29,20 +29,4 @@ export class CommunityStatisticsPageComponent extends StatisticsPageComponent { protected getComponentName(): string { diff --git a/src/app/statistics-page/item-statistics-page/item-statistics-page.component.spec.ts b/src/app/statistics-page/item-statistics-page/item-statistics-page.component.spec.ts index e5ee114e39..f5f3361cce 100644 --- a/src/app/statistics-page/item-statistics-page/item-statistics-page.component.spec.ts +++ b/src/app/statistics-page/item-statistics-page/item-statistics-page.component.spec.ts @@ -20,7 +20,6 @@ import { Item } from '../../core/shared/item.model'; import { UsageReport } from '../../core/statistics/models/usage-report.model'; import { UsageReportDataService } from '../../core/statistics/usage-report-data.service'; import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils'; -import { SharedModule } from '../../shared/shared.module'; import { StatisticsTableComponent } from '../statistics-table/statistics-table.component'; import { ItemStatisticsPageComponent } from './item-statistics-page.component'; @@ -73,9 +72,6 @@ describe('ItemStatisticsPageComponent', () => { imports: [ TranslateModule.forRoot(), CommonModule, - SharedModule, - ], - declarations: [ ItemStatisticsPageComponent, StatisticsTableComponent, ], diff --git a/src/app/statistics-page/item-statistics-page/item-statistics-page.component.ts b/src/app/statistics-page/item-statistics-page/item-statistics-page.component.ts index f7ec8299fc..702e39806f 100644 --- a/src/app/statistics-page/item-statistics-page/item-statistics-page.component.ts +++ b/src/app/statistics-page/item-statistics-page/item-statistics-page.component.ts @@ -1,24 +1,24 @@ +import { CommonModule } from '@angular/common'; import { Component } from '@angular/core'; -import { - ActivatedRoute, - Router, -} from '@angular/router'; +import { TranslateModule } from '@ngx-translate/core'; -import { AuthService } from '../../core/auth/auth.service'; -import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; import { Item } from '../../core/shared/item.model'; -import { UsageReportDataService } from '../../core/statistics/usage-report-data.service'; -import { StatisticsPageComponent } from '../statistics-page/statistics-page.component'; +import { ThemedLoadingComponent } from '../../shared/loading/themed-loading.component'; +import { VarDirective } from '../../shared/utils/var.directive'; +import { StatisticsPageDirective } from '../statistics-page/statistics-page.directive'; +import { StatisticsTableComponent } from '../statistics-table/statistics-table.component'; /** * Component representing the statistics page for an item. */ @Component({ - selector: 'ds-item-statistics-page', + selector: 'ds-base-item-statistics-page', templateUrl: '../statistics-page/statistics-page.component.html', styleUrls: ['./item-statistics-page.component.scss'], + standalone: true, + imports: [CommonModule, VarDirective, ThemedLoadingComponent, StatisticsTableComponent, TranslateModule], }) -export class ItemStatisticsPageComponent extends StatisticsPageComponent { +export class ItemStatisticsPageComponent extends StatisticsPageDirective { /** * The report types to show on this statistics page. @@ -30,20 +30,4 @@ export class ItemStatisticsPageComponent extends StatisticsPageComponent { 'TopCountries', 'TopCities', ]; - - constructor( - protected route: ActivatedRoute, - protected router: Router, - protected usageReportService: UsageReportDataService, - protected nameService: DSONameService, - protected authService: AuthService, - ) { - super( - route, - router, - usageReportService, - nameService, - authService, - ); - } } diff --git a/src/app/statistics-page/item-statistics-page/themed-item-statistics-page.component.ts b/src/app/statistics-page/item-statistics-page/themed-item-statistics-page.component.ts index b9a46308a0..88d4c04d15 100644 --- a/src/app/statistics-page/item-statistics-page/themed-item-statistics-page.component.ts +++ b/src/app/statistics-page/item-statistics-page/themed-item-statistics-page.component.ts @@ -7,9 +7,11 @@ import { ItemStatisticsPageComponent } from './item-statistics-page.component'; * Themed wrapper for ItemStatisticsPageComponent */ @Component({ - selector: 'ds-themed-item-statistics-page', + selector: 'ds-item-statistics-page', styleUrls: [], templateUrl: '../../shared/theme-support/themed.component.html', + standalone: true, + imports: [ItemStatisticsPageComponent], }) export class ThemedItemStatisticsPageComponent extends ThemedComponent { protected getComponentName(): string { diff --git a/src/app/statistics-page/site-statistics-page/site-statistics-page.component.spec.ts b/src/app/statistics-page/site-statistics-page/site-statistics-page.component.spec.ts index 60148bc55b..9b23afdd74 100644 --- a/src/app/statistics-page/site-statistics-page/site-statistics-page.component.spec.ts +++ b/src/app/statistics-page/site-statistics-page/site-statistics-page.component.spec.ts @@ -20,7 +20,6 @@ import { SiteDataService } from '../../core/data/site-data.service'; import { Site } from '../../core/shared/site.model'; import { UsageReport } from '../../core/statistics/models/usage-report.model'; import { UsageReportDataService } from '../../core/statistics/usage-report-data.service'; -import { SharedModule } from '../../shared/shared.module'; import { StatisticsTableComponent } from '../statistics-table/statistics-table.component'; import { SiteStatisticsPageComponent } from './site-statistics-page.component'; @@ -73,9 +72,6 @@ describe('SiteStatisticsPageComponent', () => { imports: [ TranslateModule.forRoot(), CommonModule, - SharedModule, - ], - declarations: [ SiteStatisticsPageComponent, StatisticsTableComponent, ], diff --git a/src/app/statistics-page/site-statistics-page/site-statistics-page.component.ts b/src/app/statistics-page/site-statistics-page/site-statistics-page.component.ts index 4fa3e10d36..eaed9f5401 100644 --- a/src/app/statistics-page/site-statistics-page/site-statistics-page.component.ts +++ b/src/app/statistics-page/site-statistics-page/site-statistics-page.component.ts @@ -1,26 +1,26 @@ +import { CommonModule } from '@angular/common'; import { Component } from '@angular/core'; -import { - ActivatedRoute, - Router, -} from '@angular/router'; +import { TranslateModule } from '@ngx-translate/core'; import { switchMap } from 'rxjs/operators'; -import { AuthService } from '../../core/auth/auth.service'; -import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; import { SiteDataService } from '../../core/data/site-data.service'; import { Site } from '../../core/shared/site.model'; -import { UsageReportDataService } from '../../core/statistics/usage-report-data.service'; -import { StatisticsPageComponent } from '../statistics-page/statistics-page.component'; +import { ThemedLoadingComponent } from '../../shared/loading/themed-loading.component'; +import { VarDirective } from '../../shared/utils/var.directive'; +import { StatisticsPageDirective } from '../statistics-page/statistics-page.directive'; +import { StatisticsTableComponent } from '../statistics-table/statistics-table.component'; /** * Component representing the site-wide statistics page. */ @Component({ - selector: 'ds-site-statistics-page', + selector: 'ds-base-site-statistics-page', templateUrl: '../statistics-page/statistics-page.component.html', styleUrls: ['./site-statistics-page.component.scss'], + standalone: true, + imports: [CommonModule, VarDirective, ThemedLoadingComponent, StatisticsTableComponent, TranslateModule], }) -export class SiteStatisticsPageComponent extends StatisticsPageComponent { +export class SiteStatisticsPageComponent extends StatisticsPageDirective { /** * The report types to show on this statistics page. @@ -29,21 +29,8 @@ export class SiteStatisticsPageComponent extends StatisticsPageComponent { 'TotalVisits', ]; - constructor( - protected route: ActivatedRoute, - protected router: Router, - protected usageReportService: UsageReportDataService, - protected nameService: DSONameService, - protected siteService: SiteDataService, - protected authService: AuthService, - ) { - super( - route, - router, - usageReportService, - nameService, - authService, - ); + constructor(protected siteService: SiteDataService) { + super(); } protected getScope$() { diff --git a/src/app/statistics-page/site-statistics-page/themed-site-statistics-page.component.ts b/src/app/statistics-page/site-statistics-page/themed-site-statistics-page.component.ts index e38cfe8247..3792b33c59 100644 --- a/src/app/statistics-page/site-statistics-page/themed-site-statistics-page.component.ts +++ b/src/app/statistics-page/site-statistics-page/themed-site-statistics-page.component.ts @@ -7,9 +7,11 @@ import { SiteStatisticsPageComponent } from './site-statistics-page.component'; * Themed wrapper for SiteStatisticsPageComponent */ @Component({ - selector: 'ds-themed-site-statistics-page', + selector: 'ds-site-statistics-page', styleUrls: [], templateUrl: '../../shared/theme-support/themed.component.html', + standalone: true, + imports: [SiteStatisticsPageComponent], }) export class ThemedSiteStatisticsPageComponent extends ThemedComponent { protected getComponentName(): string { diff --git a/src/app/statistics-page/statistics-page-routes.ts b/src/app/statistics-page/statistics-page-routes.ts new file mode 100644 index 0000000000..69bcc6b41c --- /dev/null +++ b/src/app/statistics-page/statistics-page-routes.ts @@ -0,0 +1,70 @@ +import { Route } from '@angular/router'; + +import { collectionPageResolver } from '../collection-page/collection-page.resolver'; +import { communityPageResolver } from '../community-page/community-page.resolver'; +import { i18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver'; +import { statisticsAdministratorGuard } from '../core/data/feature-authorization/feature-authorization-guard/statistics-administrator.guard'; +import { itemResolver } from '../item-page/item.resolver'; +import { ThemedCollectionStatisticsPageComponent } from './collection-statistics-page/themed-collection-statistics-page.component'; +import { ThemedCommunityStatisticsPageComponent } from './community-statistics-page/themed-community-statistics-page.component'; +import { ThemedItemStatisticsPageComponent } from './item-statistics-page/themed-item-statistics-page.component'; +import { ThemedSiteStatisticsPageComponent } from './site-statistics-page/themed-site-statistics-page.component'; + +export const ROUTES: Route[] = [ + { + path: '', + resolve: { + breadcrumb: i18nBreadcrumbResolver, + }, + data: { + title: 'statistics.title', + breadcrumbKey: 'statistics', + }, + children: [ + { + path: '', + component: ThemedSiteStatisticsPageComponent, + }, + ], + canActivate: [statisticsAdministratorGuard], + }, + { + path: `items/:id`, + resolve: { + scope: itemResolver, + breadcrumb: i18nBreadcrumbResolver, + }, + data: { + title: 'statistics.title', + breadcrumbKey: 'statistics', + }, + component: ThemedItemStatisticsPageComponent, + canActivate: [statisticsAdministratorGuard], + }, + { + path: `collections/:id`, + resolve: { + scope: collectionPageResolver, + breadcrumb: i18nBreadcrumbResolver, + }, + data: { + title: 'statistics.title', + breadcrumbKey: 'statistics', + }, + component: ThemedCollectionStatisticsPageComponent, + canActivate: [statisticsAdministratorGuard], + }, + { + path: `communities/:id`, + resolve: { + scope: communityPageResolver, + breadcrumb: i18nBreadcrumbResolver, + }, + data: { + title: 'statistics.title', + breadcrumbKey: 'statistics', + }, + component: ThemedCommunityStatisticsPageComponent, + canActivate: [statisticsAdministratorGuard], + }, +]; diff --git a/src/app/statistics-page/statistics-page-routing.module.ts b/src/app/statistics-page/statistics-page-routing.module.ts deleted file mode 100644 index a435948db9..0000000000 --- a/src/app/statistics-page/statistics-page-routing.module.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { NgModule } from '@angular/core'; -import { RouterModule } from '@angular/router'; - -import { CollectionPageResolver } from '../collection-page/collection-page.resolver'; -import { CommunityPageResolver } from '../community-page/community-page.resolver'; -import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver'; -import { I18nBreadcrumbsService } from '../core/breadcrumbs/i18n-breadcrumbs.service'; -import { StatisticsAdministratorGuard } from '../core/data/feature-authorization/feature-authorization-guard/statistics-administrator.guard'; -import { ItemResolver } from '../item-page/item.resolver'; -import { ThemedCollectionStatisticsPageComponent } from './collection-statistics-page/themed-collection-statistics-page.component'; -import { ThemedCommunityStatisticsPageComponent } from './community-statistics-page/themed-community-statistics-page.component'; -import { ThemedItemStatisticsPageComponent } from './item-statistics-page/themed-item-statistics-page.component'; -import { ThemedSiteStatisticsPageComponent } from './site-statistics-page/themed-site-statistics-page.component'; -import { StatisticsPageModule } from './statistics-page.module'; - -@NgModule({ - imports: [ - StatisticsPageModule, - RouterModule.forChild([ - { - path: '', - resolve: { - breadcrumb: I18nBreadcrumbResolver, - }, - data: { - title: 'statistics.title', - breadcrumbKey: 'statistics', - }, - children: [ - { - path: '', - component: ThemedSiteStatisticsPageComponent, - }, - ], - canActivate: [StatisticsAdministratorGuard], - }, - { - path: `items/:id`, - resolve: { - scope: ItemResolver, - breadcrumb: I18nBreadcrumbResolver, - }, - data: { - title: 'statistics.title', - breadcrumbKey: 'statistics', - }, - component: ThemedItemStatisticsPageComponent, - canActivate: [StatisticsAdministratorGuard], - }, - { - path: `collections/:id`, - resolve: { - scope: CollectionPageResolver, - breadcrumb: I18nBreadcrumbResolver, - }, - data: { - title: 'statistics.title', - breadcrumbKey: 'statistics', - }, - component: ThemedCollectionStatisticsPageComponent, - canActivate: [StatisticsAdministratorGuard], - }, - { - path: `communities/:id`, - resolve: { - scope: CommunityPageResolver, - breadcrumb: I18nBreadcrumbResolver, - }, - data: { - title: 'statistics.title', - breadcrumbKey: 'statistics', - }, - component: ThemedCommunityStatisticsPageComponent, - canActivate: [StatisticsAdministratorGuard], - }, - ], - ), - ], - providers: [ - I18nBreadcrumbResolver, - I18nBreadcrumbsService, - CollectionPageResolver, - CommunityPageResolver, - ItemResolver, - ], -}) -export class StatisticsPageRoutingModule { -} diff --git a/src/app/statistics-page/statistics-page.module.ts b/src/app/statistics-page/statistics-page.module.ts deleted file mode 100644 index b0d54341a5..0000000000 --- a/src/app/statistics-page/statistics-page.module.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { CommonModule } from '@angular/common'; -import { NgModule } from '@angular/core'; - -import { CoreModule } from '../core/core.module'; -import { UsageReportDataService } from '../core/statistics/usage-report-data.service'; -import { SharedModule } from '../shared/shared.module'; -import { StatisticsModule } from '../statistics/statistics.module'; -import { CollectionStatisticsPageComponent } from './collection-statistics-page/collection-statistics-page.component'; -import { ThemedCollectionStatisticsPageComponent } from './collection-statistics-page/themed-collection-statistics-page.component'; -import { CommunityStatisticsPageComponent } from './community-statistics-page/community-statistics-page.component'; -import { ThemedCommunityStatisticsPageComponent } from './community-statistics-page/themed-community-statistics-page.component'; -import { ItemStatisticsPageComponent } from './item-statistics-page/item-statistics-page.component'; -import { ThemedItemStatisticsPageComponent } from './item-statistics-page/themed-item-statistics-page.component'; -import { SiteStatisticsPageComponent } from './site-statistics-page/site-statistics-page.component'; -import { ThemedSiteStatisticsPageComponent } from './site-statistics-page/themed-site-statistics-page.component'; -import { StatisticsTableComponent } from './statistics-table/statistics-table.component'; - -const components = [ - StatisticsTableComponent, - SiteStatisticsPageComponent, - ItemStatisticsPageComponent, - CollectionStatisticsPageComponent, - CommunityStatisticsPageComponent, - ThemedCollectionStatisticsPageComponent, - ThemedCommunityStatisticsPageComponent, - ThemedItemStatisticsPageComponent, - ThemedSiteStatisticsPageComponent, -]; - -@NgModule({ - imports: [ - CommonModule, - SharedModule, - CoreModule.forRoot(), - StatisticsModule.forRoot(), - ], - declarations: components, - providers: [ - UsageReportDataService, - ], - exports: components, -}) - -/** - * This module handles all components and pipes that are necessary for the search page - */ -export class StatisticsPageModule { -} diff --git a/src/app/statistics-page/statistics-page/statistics-page.component.html b/src/app/statistics-page/statistics-page/statistics-page.component.html index 5ee12b72d9..78e736bd3a 100644 --- a/src/app/statistics-page/statistics-page/statistics-page.component.html +++ b/src/app/statistics-page/statistics-page/statistics-page.component.html @@ -11,7 +11,7 @@ - + diff --git a/src/app/statistics-page/statistics-page/statistics-page.component.ts b/src/app/statistics-page/statistics-page/statistics-page.directive.ts similarity index 86% rename from src/app/statistics-page/statistics-page/statistics-page.component.ts rename to src/app/statistics-page/statistics-page/statistics-page.directive.ts index 8f2b13a034..e781b89487 100644 --- a/src/app/statistics-page/statistics-page/statistics-page.component.ts +++ b/src/app/statistics-page/statistics-page/statistics-page.directive.ts @@ -1,5 +1,6 @@ import { - Component, + Directive, + inject, OnInit, } from '@angular/core'; import { @@ -27,14 +28,11 @@ import { import { UsageReport } from '../../core/statistics/models/usage-report.model'; import { UsageReportDataService } from '../../core/statistics/usage-report-data.service'; +@Directive() /** * Class representing an abstract statistics page component. */ -@Component({ - selector: 'ds-statistics-page', - template: '', -}) -export abstract class StatisticsPageComponent implements OnInit { +export abstract class StatisticsPageDirective implements OnInit { /** * The scope dso for this statistics page, as an Observable. @@ -53,14 +51,11 @@ export abstract class StatisticsPageComponent implements hasData$: Observable; - constructor( - protected route: ActivatedRoute, - protected router: Router, - protected usageReportService: UsageReportDataService, - protected nameService: DSONameService, - protected authService: AuthService, - ) { - } + protected route = inject(ActivatedRoute); + protected router = inject(Router); + protected usageReportService = inject(UsageReportDataService); + protected nameService = inject(DSONameService); + protected authService = inject(AuthService); ngOnInit(): void { this.scope$ = this.getScope$(); diff --git a/src/app/statistics-page/statistics-table/statistics-table.component.spec.ts b/src/app/statistics-page/statistics-table/statistics-table.component.spec.ts index 3c1c69fc6a..105a7623d6 100644 --- a/src/app/statistics-page/statistics-table/statistics-table.component.spec.ts +++ b/src/app/statistics-page/statistics-table/statistics-table.component.spec.ts @@ -22,8 +22,6 @@ describe('StatisticsTableComponent', () => { TestBed.configureTestingModule({ imports: [ TranslateModule.forRoot(), - ], - declarations: [ StatisticsTableComponent, ], providers: [ diff --git a/src/app/statistics-page/statistics-table/statistics-table.component.ts b/src/app/statistics-page/statistics-table/statistics-table.component.ts index 8c972b3bea..cd71730568 100644 --- a/src/app/statistics-page/statistics-table/statistics-table.component.ts +++ b/src/app/statistics-page/statistics-table/statistics-table.component.ts @@ -1,9 +1,17 @@ +import { + AsyncPipe, + NgFor, + NgIf, +} from '@angular/common'; import { Component, Input, OnInit, } from '@angular/core'; -import { TranslateService } from '@ngx-translate/core'; +import { + TranslateModule, + TranslateService, +} from '@ngx-translate/core'; import { Observable, of, @@ -29,6 +37,8 @@ import { isEmpty } from '../../shared/empty.util'; selector: 'ds-statistics-table', templateUrl: './statistics-table.component.html', styleUrls: ['./statistics-table.component.scss'], + standalone: true, + imports: [NgIf, NgFor, AsyncPipe, TranslateModule], }) export class StatisticsTableComponent implements OnInit { @@ -36,7 +46,7 @@ export class StatisticsTableComponent implements OnInit { * The usage report to display a statistics table for */ @Input() - report: UsageReport; + report: UsageReport; /** * Boolean indicating whether the usage report has data diff --git a/src/app/statistics/angulartics/dspace/view-tracker.component.ts b/src/app/statistics/angulartics/dspace/view-tracker.component.ts index 1820b03be6..801f4fd2cb 100644 --- a/src/app/statistics/angulartics/dspace/view-tracker.component.ts +++ b/src/app/statistics/angulartics/dspace/view-tracker.component.ts @@ -19,6 +19,7 @@ import { hasValue } from '../../../shared/empty.util'; selector: 'ds-view-tracker', styleUrls: ['./view-tracker.component.scss'], templateUrl: './view-tracker.component.html', + standalone: true, }) export class ViewTrackerComponent implements OnInit, OnDestroy { /** diff --git a/src/app/statistics/google-analytics.service.spec.ts b/src/app/statistics/google-analytics.service.spec.ts index eb35750c4c..1b8783f3bf 100644 --- a/src/app/statistics/google-analytics.service.spec.ts +++ b/src/app/statistics/google-analytics.service.spec.ts @@ -6,8 +6,8 @@ import { of } from 'rxjs'; import { ConfigurationDataService } from '../core/data/configuration-data.service'; import { ConfigurationProperty } from '../core/shared/configuration-property.model'; -import { KlaroService } from '../shared/cookies/klaro.service'; -import { GOOGLE_ANALYTICS_KLARO_KEY } from '../shared/cookies/klaro-configuration'; +import { OrejimeService } from '../shared/cookies/orejime.service'; +import { GOOGLE_ANALYTICS_OREJIME_KEY } from '../shared/cookies/orejime-configuration'; import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject$, @@ -24,7 +24,7 @@ describe('GoogleAnalyticsService', () => { let googleAnalyticsSpy: Angulartics2GoogleAnalytics; let googleTagManagerSpy: Angulartics2GoogleGlobalSiteTag; let configSpy: ConfigurationDataService; - let klaroServiceSpy: jasmine.SpyObj; + let orejimeServiceSpy: jasmine.SpyObj; let scriptElementMock: any; let srcSpy: any; let innerHTMLSpy: any; @@ -47,7 +47,7 @@ describe('GoogleAnalyticsService', () => { 'startTracking', ]); - klaroServiceSpy = jasmine.createSpyObj('KlaroService', { + orejimeServiceSpy = jasmine.createSpyObj('OrejimeService', { 'getSavedPreferences': jasmine.createSpy('getSavedPreferences'), }); @@ -73,11 +73,11 @@ describe('GoogleAnalyticsService', () => { body: bodyElementSpy, }); - klaroServiceSpy.getSavedPreferences.and.returnValue(of({ - GOOGLE_ANALYTICS_KLARO_KEY: true, + orejimeServiceSpy.getSavedPreferences.and.returnValue(of({ + GOOGLE_ANALYTICS_OREJIME_KEY: true, })); - service = new GoogleAnalyticsService(googleAnalyticsSpy, googleTagManagerSpy, klaroServiceSpy, configSpy, documentSpy ); + service = new GoogleAnalyticsService(googleAnalyticsSpy, googleTagManagerSpy, orejimeServiceSpy, configSpy, documentSpy ); }); it('should be created', () => { @@ -97,11 +97,11 @@ describe('GoogleAnalyticsService', () => { findByPropertyName: createFailedRemoteDataObject$(), }); - klaroServiceSpy.getSavedPreferences.and.returnValue(of({ - GOOGLE_ANALYTICS_KLARO_KEY: true, + orejimeServiceSpy.getSavedPreferences.and.returnValue(of({ + GOOGLE_ANALYTICS_OREJIME_KEY: true, })); - service = new GoogleAnalyticsService(googleAnalyticsSpy, googleTagManagerSpy, klaroServiceSpy, configSpy, documentSpy); + service = new GoogleAnalyticsService(googleAnalyticsSpy, googleTagManagerSpy, orejimeServiceSpy, configSpy, documentSpy); }); it('should NOT add a script to the body', () => { @@ -120,10 +120,10 @@ describe('GoogleAnalyticsService', () => { describe('when the tracking id is empty', () => { beforeEach(() => { configSpy = createConfigSuccessSpy(); - klaroServiceSpy.getSavedPreferences.and.returnValue(of({ - [GOOGLE_ANALYTICS_KLARO_KEY]: true, + orejimeServiceSpy.getSavedPreferences.and.returnValue(of({ + [GOOGLE_ANALYTICS_OREJIME_KEY]: true, })); - service = new GoogleAnalyticsService(googleAnalyticsSpy, googleTagManagerSpy, klaroServiceSpy, configSpy, documentSpy); + service = new GoogleAnalyticsService(googleAnalyticsSpy, googleTagManagerSpy, orejimeServiceSpy, configSpy, documentSpy); }); it('should NOT add a script to the body', () => { @@ -141,8 +141,8 @@ describe('GoogleAnalyticsService', () => { describe('when google-analytics cookie preferences are not existing', () => { beforeEach(() => { configSpy = createConfigSuccessSpy(trackingIdV4TestValue); - klaroServiceSpy.getSavedPreferences.and.returnValue(of({})); - service = new GoogleAnalyticsService(googleAnalyticsSpy, googleTagManagerSpy, klaroServiceSpy, configSpy, documentSpy); + orejimeServiceSpy.getSavedPreferences.and.returnValue(of({})); + service = new GoogleAnalyticsService(googleAnalyticsSpy, googleTagManagerSpy, orejimeServiceSpy, configSpy, documentSpy); }); it('should NOT add a script to the body', () => { @@ -161,10 +161,10 @@ describe('GoogleAnalyticsService', () => { describe('when google-analytics cookie preferences are set to false', () => { beforeEach(() => { configSpy = createConfigSuccessSpy(trackingIdV4TestValue); - klaroServiceSpy.getSavedPreferences.and.returnValue(of({ - [GOOGLE_ANALYTICS_KLARO_KEY]: false, + orejimeServiceSpy.getSavedPreferences.and.returnValue(of({ + [GOOGLE_ANALYTICS_OREJIME_KEY]: false, })); - service = new GoogleAnalyticsService(googleAnalyticsSpy, googleTagManagerSpy, klaroServiceSpy, configSpy, documentSpy); + service = new GoogleAnalyticsService(googleAnalyticsSpy, googleTagManagerSpy, orejimeServiceSpy, configSpy, documentSpy); }); it('should NOT add a script to the body', () => { @@ -183,10 +183,10 @@ describe('GoogleAnalyticsService', () => { beforeEach(() => { configSpy = createConfigSuccessSpy(trackingIdV4TestValue); - klaroServiceSpy.getSavedPreferences.and.returnValue(of({ - [GOOGLE_ANALYTICS_KLARO_KEY]: true, + orejimeServiceSpy.getSavedPreferences.and.returnValue(of({ + [GOOGLE_ANALYTICS_OREJIME_KEY]: true, })); - service = new GoogleAnalyticsService(googleAnalyticsSpy, googleTagManagerSpy, klaroServiceSpy, configSpy, documentSpy); + service = new GoogleAnalyticsService(googleAnalyticsSpy, googleTagManagerSpy, orejimeServiceSpy, configSpy, documentSpy); }); it('should create a script tag whose innerHTML contains the tracking id', () => { @@ -220,10 +220,10 @@ describe('GoogleAnalyticsService', () => { beforeEach(() => { configSpy = createConfigSuccessSpy(trackingIdV3TestValue); - klaroServiceSpy.getSavedPreferences.and.returnValue(of({ - [GOOGLE_ANALYTICS_KLARO_KEY]: true, + orejimeServiceSpy.getSavedPreferences.and.returnValue(of({ + [GOOGLE_ANALYTICS_OREJIME_KEY]: true, })); - service = new GoogleAnalyticsService(googleAnalyticsSpy, googleTagManagerSpy, klaroServiceSpy, configSpy, documentSpy); + service = new GoogleAnalyticsService(googleAnalyticsSpy, googleTagManagerSpy, orejimeServiceSpy, configSpy, documentSpy); }); it('should create a script tag whose innerHTML contains the tracking id', () => { diff --git a/src/app/statistics/google-analytics.service.ts b/src/app/statistics/google-analytics.service.ts index 508297c3b9..d638b0ce39 100644 --- a/src/app/statistics/google-analytics.service.ts +++ b/src/app/statistics/google-analytics.service.ts @@ -11,8 +11,8 @@ import { combineLatest } from 'rxjs'; import { ConfigurationDataService } from '../core/data/configuration-data.service'; import { getFirstCompletedRemoteData } from '../core/shared/operators'; -import { KlaroService } from '../shared/cookies/klaro.service'; -import { GOOGLE_ANALYTICS_KLARO_KEY } from '../shared/cookies/klaro-configuration'; +import { OrejimeService } from '../shared/cookies/orejime.service'; +import { GOOGLE_ANALYTICS_OREJIME_KEY } from '../shared/cookies/orejime-configuration'; import { isEmpty } from '../shared/empty.util'; /** @@ -25,7 +25,7 @@ export class GoogleAnalyticsService { constructor( private googleAnalytics: Angulartics2GoogleAnalytics, private googleGlobalSiteTag: Angulartics2GoogleGlobalSiteTag, - private klaroService: KlaroService, + private orejimeService: OrejimeService, private configService: ConfigurationDataService, @Inject(DOCUMENT) private document: any, ) { @@ -41,12 +41,12 @@ export class GoogleAnalyticsService { const googleKey$ = this.configService.findByPropertyName('google.analytics.key').pipe( getFirstCompletedRemoteData(), ); - const preferences$ = this.klaroService.getSavedPreferences(); + const preferences$ = this.orejimeService.getSavedPreferences(); combineLatest([preferences$, googleKey$]) .subscribe(([preferences, remoteData]) => { // make sure user has accepted Google Analytics consents - if (isEmpty(preferences) || isEmpty(preferences[GOOGLE_ANALYTICS_KLARO_KEY]) || !preferences[GOOGLE_ANALYTICS_KLARO_KEY]) { + if (isEmpty(preferences) || isEmpty(preferences[GOOGLE_ANALYTICS_OREJIME_KEY]) || !preferences[GOOGLE_ANALYTICS_OREJIME_KEY]) { return; } diff --git a/src/app/statistics/statistics-endpoint.model.ts b/src/app/statistics/statistics-endpoint.model.ts index e1152e4a50..8bee88c7da 100644 --- a/src/app/statistics/statistics-endpoint.model.ts +++ b/src/app/statistics/statistics-endpoint.model.ts @@ -22,13 +22,13 @@ export class StatisticsEndpoint implements CacheableObject { */ @excludeFromEquals @autoserialize - type: ResourceType; + type: ResourceType; /** * The {@link HALLink}s for the statistics endpoint */ @deserialize - _links: { + _links: { self: HALLink; searchevents: HALLink; viewevents: HALLink; diff --git a/src/app/statistics/statistics.module.ts b/src/app/statistics/statistics.module.ts deleted file mode 100644 index 9e1c248da8..0000000000 --- a/src/app/statistics/statistics.module.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { CommonModule } from '@angular/common'; -import { - ModuleWithProviders, - NgModule, -} from '@angular/core'; - -import { CoreModule } from '../core/core.module'; -import { SharedModule } from '../shared/shared.module'; -import { ViewTrackerComponent } from './angulartics/dspace/view-tracker.component'; -import { StatisticsEndpoint } from './statistics-endpoint.model'; - -/** - * Declaration needed to make sure all decorator functions are called in time - */ -export const models = [ - StatisticsEndpoint, -]; - -@NgModule({ - imports: [ - CommonModule, - CoreModule.forRoot(), - SharedModule, - ], - declarations: [ - ViewTrackerComponent, - ], - exports: [ - ViewTrackerComponent, - ], -}) -/** - * This module handles the statistics - */ -export class StatisticsModule { - static forRoot(): ModuleWithProviders { - return { - ngModule: StatisticsModule, - }; - } -} diff --git a/src/app/submission/edit/submission-edit.component.spec.ts b/src/app/submission/edit/submission-edit.component.spec.ts index ac623e6c6a..59f9883f19 100644 --- a/src/app/submission/edit/submission-edit.component.spec.ts +++ b/src/app/submission/edit/submission-edit.component.spec.ts @@ -10,23 +10,33 @@ import { Router, } from '@angular/router'; import { RouterTestingModule } from '@angular/router/testing'; +import { provideMockStore } from '@ngrx/store/testing'; +import { TranslateModule } from '@ngx-translate/core'; import { - TranslateModule, - TranslateService, -} from '@ngx-translate/core'; -import { of as observableOf } from 'rxjs'; + of as observableOf, + of, +} from 'rxjs'; +import { APP_DATA_SERVICES_MAP } from '../../../config/app-config.interface'; +import { AuthService } from '../../core/auth/auth.service'; import { ItemDataService } from '../../core/data/item-data.service'; +import { HALEndpointService } from '../../core/shared/hal-endpoint.service'; import { SubmissionJsonPatchOperationsService } from '../../core/submission/submission-json-patch-operations.service'; +import { XSRFService } from '../../core/xsrf/xsrf.service'; import { mockSubmissionObject } from '../../shared/mocks/submission.mock'; -import { getMockTranslateService } from '../../shared/mocks/translate.service.mock'; +import { getMockThemeService } from '../../shared/mocks/theme-service.mock'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; import { ActivatedRouteStub } from '../../shared/testing/active-router.stub'; +import { AuthServiceStub } from '../../shared/testing/auth-service.stub'; import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub'; import { RouterStub } from '../../shared/testing/router.stub'; +import { SectionsServiceStub } from '../../shared/testing/sections-service.stub'; import { SubmissionJsonPatchOperationsServiceStub } from '../../shared/testing/submission-json-patch-operations-service.stub'; import { SubmissionServiceStub } from '../../shared/testing/submission-service.stub'; +import { ThemeService } from '../../shared/theme-support/theme.service'; +import { SubmissionFormComponent } from '../form/submission-form.component'; +import { SectionsService } from '../sections/sections.service'; import { SubmissionService } from '../submission.service'; import { SubmissionEditComponent } from './submission-edit.component'; @@ -38,6 +48,9 @@ describe('SubmissionEditComponent Component', () => { let itemDataService: ItemDataService; let submissionJsonPatchOperationsServiceStub: SubmissionJsonPatchOperationsServiceStub; let router: RouterStub; + let halService: jasmine.SpyObj; + + let themeService = getMockThemeService(); const submissionId = '826'; const route: ActivatedRouteStub = new ActivatedRouteStub(); @@ -47,25 +60,39 @@ describe('SubmissionEditComponent Component', () => { itemDataService = jasmine.createSpyObj('itemDataService', { findByHref: createSuccessfulRemoteDataObject$(submissionObject.item), }); + + halService = jasmine.createSpyObj('halService', { + getEndpoint: of('fake-url'), + }); + TestBed.configureTestingModule({ imports: [ TranslateModule.forRoot(), RouterTestingModule.withRoutes([ { path: ':id/edit', component: SubmissionEditComponent, pathMatch: 'full' }, ]), + SubmissionEditComponent, ], - declarations: [SubmissionEditComponent], providers: [ { provide: NotificationsService, useClass: NotificationsServiceStub }, { provide: SubmissionService, useClass: SubmissionServiceStub }, { provide: SubmissionJsonPatchOperationsService, useClass: SubmissionJsonPatchOperationsServiceStub }, { provide: ItemDataService, useValue: itemDataService }, - { provide: TranslateService, useValue: getMockTranslateService() }, { provide: Router, useValue: new RouterStub() }, { provide: ActivatedRoute, useValue: route }, - + { provide: AuthService, useValue: new AuthServiceStub() }, + { provide: HALEndpointService, useValue: halService }, + { provide: SectionsService, useValue: new SectionsServiceStub() }, + { provide: ThemeService, useValue: themeService }, + { provide: XSRFService, useValue: {} }, + { provide: APP_DATA_SERVICES_MAP, useValue: {} }, + provideMockStore(), ], schemas: [NO_ERRORS_SCHEMA], + }).overrideComponent(SubmissionEditComponent, { + remove: { + imports: [ SubmissionFormComponent ], + }, }).compileComponents(); })); @@ -89,6 +116,9 @@ describe('SubmissionEditComponent Component', () => { submissionServiceStub.retrieveSubmission.and.returnValue( createSuccessfulRemoteDataObject$(submissionObject), ); + submissionServiceStub.getSubmissionObject.and.returnValue(observableOf(submissionObject)); + submissionServiceStub.getSubmissionStatus.and.returnValue(observableOf(true)); + fixture.detectChanges(); diff --git a/src/app/submission/edit/submission-edit.component.ts b/src/app/submission/edit/submission-edit.component.ts index 153ed42588..822818ee24 100644 --- a/src/app/submission/edit/submission-edit.component.ts +++ b/src/app/submission/edit/submission-edit.component.ts @@ -36,6 +36,7 @@ import { isNotNull, } from '../../shared/empty.util'; import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { SubmissionFormComponent } from '../form/submission-form.component'; import { SubmissionError } from '../objects/submission-error.model'; import { SubmissionService } from '../submission.service'; import parseSectionErrors from '../utils/parseSectionErrors'; @@ -44,9 +45,13 @@ import parseSectionErrors from '../utils/parseSectionErrors'; * This component allows to edit an existing workspaceitem/workflowitem. */ @Component({ - selector: 'ds-submission-edit', + selector: 'ds-base-submission-edit', styleUrls: ['./submission-edit.component.scss'], templateUrl: './submission-edit.component.html', + standalone: true, + imports: [ + SubmissionFormComponent, + ], }) export class SubmissionEditComponent implements OnDestroy, OnInit { diff --git a/src/app/submission/edit/themed-submission-edit.component.ts b/src/app/submission/edit/themed-submission-edit.component.ts index 62e4c1a4da..e1abb0634e 100644 --- a/src/app/submission/edit/themed-submission-edit.component.ts +++ b/src/app/submission/edit/themed-submission-edit.component.ts @@ -7,9 +7,11 @@ import { ThemedComponent } from '../../shared/theme-support/themed.component'; import { SubmissionEditComponent } from './submission-edit.component'; @Component({ - selector: 'ds-themed-submission-edit', + selector: 'ds-submission-edit', styleUrls: [], templateUrl: './../../shared/theme-support/themed.component.html', + standalone: true, + imports: [SubmissionEditComponent], }) export class ThemedSubmissionEditComponent extends ThemedComponent { protected getComponentName(): string { diff --git a/src/app/submission/form/collection/submission-form-collection.component.html b/src/app/submission/form/collection/submission-form-collection.component.html index 4e4200d16b..16370a5873 100644 --- a/src/app/submission/form/collection/submission-form-collection.component.html +++ b/src/app/submission/form/collection/submission-form-collection.component.html @@ -35,9 +35,9 @@ class="dropdown-menu p-0" id="collectionControlsDropdownMenu" aria-labelledby="collectionControlsMenuButton"> - - + diff --git a/src/app/submission/form/collection/submission-form-collection.component.spec.ts b/src/app/submission/form/collection/submission-form-collection.component.spec.ts index e2a90641e3..540c2ba561 100644 --- a/src/app/submission/form/collection/submission-form-collection.component.spec.ts +++ b/src/app/submission/form/collection/submission-form-collection.component.spec.ts @@ -30,6 +30,7 @@ import { JsonPatchOperationPathCombiner } from '../../../core/json-patch/builder import { JsonPatchOperationsBuilder } from '../../../core/json-patch/builder/json-patch-operations-builder'; import { Collection } from '../../../core/shared/collection.model'; import { SubmissionJsonPatchOperationsService } from '../../../core/submission/submission-json-patch-operations.service'; +import { ThemedCollectionDropdownComponent } from '../../../shared/collection-dropdown/themed-collection-dropdown.component'; import { DSONameServiceMock } from '../../../shared/mocks/dso-name.service.mock'; import { mockSubmissionId, @@ -149,8 +150,6 @@ describe('SubmissionFormCollectionComponent Component', () => { ReactiveFormsModule, NgbModule, TranslateModule.forRoot(), - ], - declarations: [ SubmissionFormCollectionComponent, TestComponent, ], @@ -167,7 +166,11 @@ describe('SubmissionFormCollectionComponent Component', () => { SubmissionFormCollectionComponent, ], schemas: [CUSTOM_ELEMENTS_SCHEMA], - }).compileComponents(); + }) + .overrideComponent(SubmissionFormCollectionComponent, { + remove: { imports: [ThemedCollectionDropdownComponent] }, + }) + .compileComponents(); })); describe('', () => { @@ -310,6 +313,10 @@ describe('SubmissionFormCollectionComponent Component', () => { @Component({ selector: 'ds-test-cmp', template: ``, + standalone: true, + imports: [FormsModule, + ReactiveFormsModule, + NgbModule], }) class TestComponent { diff --git a/src/app/submission/form/collection/submission-form-collection.component.ts b/src/app/submission/form/collection/submission-form-collection.component.ts index 89d9112284..2aec1c9e30 100644 --- a/src/app/submission/form/collection/submission-form-collection.component.ts +++ b/src/app/submission/form/collection/submission-form-collection.component.ts @@ -1,14 +1,18 @@ +import { CommonModule } from '@angular/common'; import { ChangeDetectorRef, Component, EventEmitter, Input, OnChanges, + OnDestroy, OnInit, Output, SimpleChanges, ViewChild, } from '@angular/core'; +import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'; +import { TranslateModule } from '@ngx-translate/core'; import { BehaviorSubject, Observable, @@ -31,6 +35,7 @@ import { getFirstSucceededRemoteDataPayload } from '../../../core/shared/operato import { SubmissionObject } from '../../../core/submission/models/submission-object.model'; import { SubmissionJsonPatchOperationsService } from '../../../core/submission/submission-json-patch-operations.service'; import { CollectionDropdownComponent } from '../../../shared/collection-dropdown/collection-dropdown.component'; +import { ThemedCollectionDropdownComponent } from '../../../shared/collection-dropdown/themed-collection-dropdown.component'; import { hasValue, isNotEmpty, @@ -46,8 +51,15 @@ import { SubmissionService } from '../../submission.service'; selector: 'ds-submission-form-collection', styleUrls: ['./submission-form-collection.component.scss'], templateUrl: './submission-form-collection.component.html', + standalone: true, + imports: [ + CommonModule, + TranslateModule, + NgbDropdownModule, + ThemedCollectionDropdownComponent, + ], }) -export class SubmissionFormCollectionComponent implements OnChanges, OnInit { +export class SubmissionFormCollectionComponent implements OnDestroy, OnChanges, OnInit { /** * The current collection id this submission belonging to @@ -155,7 +167,7 @@ export class SubmissionFormCollectionComponent implements OnChanges, OnInit { */ ngOnInit() { this.pathCombiner = new JsonPatchOperationPathCombiner('sections', 'collection'); - this.available$ = this.sectionsService.isSectionTypeAvailable(this.submissionId, SectionsType.collection); + this.available$ = this.sectionsService.isSectionTypeAvailable(this.submissionId, SectionsType.Collection); } /** diff --git a/src/app/submission/form/footer/submission-form-footer.component.spec.ts b/src/app/submission/form/footer/submission-form-footer.component.spec.ts index 14270f2dea..4c6d04c970 100644 --- a/src/app/submission/form/footer/submission-form-footer.component.spec.ts +++ b/src/app/submission/form/footer/submission-form-footer.component.spec.ts @@ -26,7 +26,6 @@ import { TestScheduler } from 'rxjs/testing'; import { SubmissionRestService } from '../../../core/submission/submission-rest.service'; import { mockSubmissionId } from '../../../shared/mocks/submission.mock'; -import { BrowserOnlyMockPipe } from '../../../shared/testing/browser-only-mock.pipe'; import { SubmissionRestServiceStub } from '../../../shared/testing/submission-rest-service.stub'; import { SubmissionServiceStub } from '../../../shared/testing/submission-service.stub'; import { createTestComponent } from '../../../shared/testing/utils.test'; @@ -50,11 +49,8 @@ describe('SubmissionFormFooterComponent', () => { imports: [ NgbModule, TranslateModule.forRoot(), - ], - declarations: [ SubmissionFormFooterComponent, TestComponent, - BrowserOnlyMockPipe, ], providers: [ { provide: SubmissionService, useValue: submissionServiceStub }, @@ -266,6 +262,8 @@ describe('SubmissionFormFooterComponent', () => { @Component({ selector: 'ds-test-cmp', template: ``, + standalone: true, + imports: [NgbModule], }) class TestComponent { diff --git a/src/app/submission/form/footer/submission-form-footer.component.ts b/src/app/submission/form/footer/submission-form-footer.component.ts index 1db4b606d7..712a944a84 100644 --- a/src/app/submission/form/footer/submission-form-footer.component.ts +++ b/src/app/submission/form/footer/submission-form-footer.component.ts @@ -1,3 +1,4 @@ +import { CommonModule } from '@angular/common'; import { Component, Input, @@ -5,6 +6,7 @@ import { SimpleChanges, } from '@angular/core'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import { TranslateModule } from '@ngx-translate/core'; import { Observable, of as observableOf, @@ -14,6 +16,7 @@ import { map } from 'rxjs/operators'; import { SubmissionRestService } from '../../../core/submission/submission-rest.service'; import { SubmissionScopeType } from '../../../core/submission/submission-scope-type'; import { isNotEmpty } from '../../../shared/empty.util'; +import { BrowserOnlyPipe } from '../../../shared/utils/browser-only.pipe'; import { SubmissionService } from '../../submission.service'; /** @@ -23,6 +26,8 @@ import { SubmissionService } from '../../submission.service'; selector: 'ds-submission-form-footer', styleUrls: ['./submission-form-footer.component.scss'], templateUrl: './submission-form-footer.component.html', + standalone: true, + imports: [CommonModule, BrowserOnlyPipe, TranslateModule], }) export class SubmissionFormFooterComponent implements OnChanges { diff --git a/src/app/submission/form/section-add/submission-form-section-add.component.spec.ts b/src/app/submission/form/section-add/submission-form-section-add.component.spec.ts index fe59c06357..5aaf9203c0 100644 --- a/src/app/submission/form/section-add/submission-form-section-add.component.spec.ts +++ b/src/app/submission/form/section-add/submission-form-section-add.component.spec.ts @@ -74,8 +74,6 @@ describe('SubmissionFormSectionAddComponent Component', () => { imports: [ NgbModule, TranslateModule.forRoot(), - ], - declarations: [ SubmissionFormSectionAddComponent, TestComponent, ], @@ -223,6 +221,8 @@ describe('SubmissionFormSectionAddComponent Component', () => { @Component({ selector: 'ds-test-cmp', template: ``, + standalone: true, + imports: [NgbModule], }) class TestComponent { diff --git a/src/app/submission/form/section-add/submission-form-section-add.component.ts b/src/app/submission/form/section-add/submission-form-section-add.component.ts index 60452aa51b..4cf10e55d8 100644 --- a/src/app/submission/form/section-add/submission-form-section-add.component.ts +++ b/src/app/submission/form/section-add/submission-form-section-add.component.ts @@ -1,8 +1,11 @@ +import { CommonModule } from '@angular/common'; import { Component, Input, OnInit, } from '@angular/core'; +import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'; +import { TranslateModule } from '@ngx-translate/core'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; @@ -16,8 +19,10 @@ import { SubmissionService } from '../../submission.service'; */ @Component({ selector: 'ds-submission-form-section-add', - styleUrls: [ './submission-form-section-add.component.scss' ], + styleUrls: ['./submission-form-section-add.component.scss'], templateUrl: './submission-form-section-add.component.html', + standalone: true, + imports: [CommonModule, TranslateModule, NgbDropdownModule], }) export class SubmissionFormSectionAddComponent implements OnInit { diff --git a/src/app/submission/form/submission-form.component.html b/src/app/submission/form/submission-form.component.html index 84767c8c6f..17e863d435 100644 --- a/src/app/submission/form/submission-form.component.html +++ b/src/app/submission/form/submission-form.component.html @@ -27,11 +27,11 @@
- - + + + [sectionData]="$any(object)">
diff --git a/src/app/submission/form/submission-form.component.spec.ts b/src/app/submission/form/submission-form.component.spec.ts index 94001f14fa..f097a1e7ea 100644 --- a/src/app/submission/form/submission-form.component.spec.ts +++ b/src/app/submission/form/submission-form.component.spec.ts @@ -20,6 +20,7 @@ import { TestScheduler } from 'rxjs/testing'; import { AuthService } from '../../core/auth/auth.service'; import { HALEndpointService } from '../../core/shared/hal-endpoint.service'; import { Item } from '../../core/shared/item.model'; +import { ThemedLoadingComponent } from '../../shared/loading/themed-loading.component'; import { mockSectionsData, mockSectionsList, @@ -30,14 +31,21 @@ import { mockSubmissionSelfUrl, mockSubmissionState, } from '../../shared/mocks/submission.mock'; +import { getMockThemeService } from '../../shared/mocks/theme-service.mock'; import { AuthServiceStub } from '../../shared/testing/auth-service.stub'; import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub'; import { SubmissionServiceStub } from '../../shared/testing/submission-service.stub'; import { createTestComponent } from '../../shared/testing/utils.test'; +import { ThemeService } from '../../shared/theme-support/theme.service'; +import { SubmissionSectionContainerComponent } from '../sections/container/section-container.component'; import { SectionsService } from '../sections/sections.service'; import { VisibilityType } from '../sections/visibility-type'; import { SubmissionService } from '../submission.service'; +import { SubmissionFormCollectionComponent } from './collection/submission-form-collection.component'; +import { SubmissionFormFooterComponent } from './footer/submission-form-footer.component'; +import { SubmissionFormSectionAddComponent } from './section-add/submission-form-section-add.component'; import { SubmissionFormComponent } from './submission-form.component'; +import { ThemedSubmissionUploadFilesComponent } from './submission-upload-files/themed-submission-upload-files.component'; describe('SubmissionFormComponent Component', () => { @@ -59,21 +67,31 @@ describe('SubmissionFormComponent Component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [], - declarations: [ - SubmissionFormComponent, - TestComponent, + imports: [SubmissionFormComponent, TestComponent, ], providers: [ { provide: AuthService, useClass: AuthServiceStub }, { provide: HALEndpointService, useValue: new HALEndpointServiceStub('workspaceitems') }, { provide: SubmissionService, useValue: submissionServiceStub }, { provide: SectionsService, useValue: { isSectionTypeAvailable: () => observableOf(true) } }, + { provide: ThemeService, useValue: getMockThemeService() }, ChangeDetectorRef, SubmissionFormComponent, ], schemas: [NO_ERRORS_SCHEMA], - }).compileComponents(); + }) + .overrideComponent(SubmissionFormComponent, { + remove: { + imports: [ + ThemedLoadingComponent, + SubmissionSectionContainerComponent, + SubmissionFormFooterComponent, + ThemedSubmissionUploadFilesComponent, + SubmissionFormCollectionComponent, + SubmissionFormSectionAddComponent, + ] }, + }) + .compileComponents(); })); describe('', () => { @@ -259,6 +277,7 @@ describe('SubmissionFormComponent Component', () => { @Component({ selector: 'ds-test-cmp', template: ``, + standalone: true, }) class TestComponent { diff --git a/src/app/submission/form/submission-form.component.ts b/src/app/submission/form/submission-form.component.ts index d697b64f9d..74c262befc 100644 --- a/src/app/submission/form/submission-form.component.ts +++ b/src/app/submission/form/submission-form.component.ts @@ -1,3 +1,4 @@ +import { CommonModule } from '@angular/common'; import { ChangeDetectorRef, Component, @@ -21,6 +22,7 @@ import { import { AuthService } from '../../core/auth/auth.service'; import { SubmissionDefinitionsModel } from '../../core/config/models/config-submission-definitions.model'; +import { SubmissionSectionModel } from '../../core/config/models/config-submission-section.model'; import { Collection } from '../../core/shared/collection.model'; import { HALEndpointService } from '../../core/shared/hal-endpoint.service'; import { Item } from '../../core/shared/item.model'; @@ -31,18 +33,21 @@ import { isNotEmpty, isNotUndefined, } from '../../shared/empty.util'; +import { ThemedLoadingComponent } from '../../shared/loading/themed-loading.component'; import { UploaderOptions } from '../../shared/upload/uploader/uploader-options.model'; +import { SectionVisibility } from '../objects/section-visibility.model'; import { SubmissionError } from '../objects/submission-error.model'; import { SubmissionObjectEntry } from '../objects/submission-objects.reducer'; +import { SubmissionSectionContainerComponent } from '../sections/container/section-container.component'; import { SectionDataObject } from '../sections/models/section-data.model'; import { SectionsService } from '../sections/sections.service'; import { SectionsType } from '../sections/sections-type'; import { VisibilityType } from '../sections/visibility-type'; import { SubmissionService } from '../submission.service'; -import { - SubmissionSectionModel, - SubmissionSectionVisibility, -} from './../../core/config/models/config-submission-section.model'; +import { SubmissionFormCollectionComponent } from './collection/submission-form-collection.component'; +import { SubmissionFormFooterComponent } from './footer/submission-form-footer.component'; +import { SubmissionFormSectionAddComponent } from './section-add/submission-form-section-add.component'; +import { ThemedSubmissionUploadFilesComponent } from './submission-upload-files/themed-submission-upload-files.component'; /** * This component represents the submission form. @@ -51,6 +56,16 @@ import { selector: 'ds-submission-form', styleUrls: ['./submission-form.component.scss'], templateUrl: './submission-form.component.html', + imports: [ + CommonModule, + ThemedLoadingComponent, + SubmissionSectionContainerComponent, + SubmissionFormFooterComponent, + ThemedSubmissionUploadFilesComponent, + SubmissionFormCollectionComponent, + SubmissionFormSectionAddComponent, + ], + standalone: true, }) export class SubmissionFormComponent implements OnChanges, OnDestroy { @@ -64,7 +79,7 @@ export class SubmissionFormComponent implements OnChanges, OnDestroy { /** * Checks if the collection can be modifiable by the user - * @type {booelan} + * @type {boolean} */ @Input() collectionModifiable: boolean | null = null; @@ -216,7 +231,7 @@ export class SubmissionFormComponent implements OnChanges, OnDestroy { /** * Returns the visibility object of the collection section */ - private getCollectionVisibility(): SubmissionSectionVisibility { + private getCollectionVisibility(): SectionVisibility { const submissionSectionModel: SubmissionSectionModel = this.submissionDefinition.sections.page.find( (section) => isEqual(section.sectionType, SectionsType.Collection), diff --git a/src/app/submission/form/submission-upload-files/submission-upload-files.component.spec.ts b/src/app/submission/form/submission-upload-files/submission-upload-files.component.spec.ts index 28f6803c48..72ae72b846 100644 --- a/src/app/submission/form/submission-upload-files/submission-upload-files.component.spec.ts +++ b/src/app/submission/form/submission-upload-files/submission-upload-files.component.spec.ts @@ -32,7 +32,6 @@ import { } from '../../../shared/mocks/submission.mock'; import { getMockTranslateService } from '../../../shared/mocks/translate.service.mock'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; -import { SharedModule } from '../../../shared/shared.module'; import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub'; import { SectionsServiceStub } from '../../../shared/testing/sections-service.stub'; import { SubmissionJsonPatchOperationsServiceStub } from '../../../shared/testing/submission-json-patch-operations-service.stub'; @@ -66,10 +65,7 @@ describe('SubmissionUploadFilesComponent Component', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [ - SharedModule, TranslateModule.forRoot(), - ], - declarations: [ SubmissionUploadFilesComponent, TestComponent, ], @@ -218,6 +214,7 @@ describe('SubmissionUploadFilesComponent Component', () => { @Component({ selector: 'ds-test-cmp', template: ``, + standalone: true, }) class TestComponent { diff --git a/src/app/submission/form/submission-upload-files/submission-upload-files.component.ts b/src/app/submission/form/submission-upload-files/submission-upload-files.component.ts index 630356b1f8..3632ec6760 100644 --- a/src/app/submission/form/submission-upload-files/submission-upload-files.component.ts +++ b/src/app/submission/form/submission-upload-files/submission-upload-files.component.ts @@ -1,7 +1,9 @@ +import { NgIf } from '@angular/common'; import { Component, Input, OnChanges, + OnDestroy, } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; import { @@ -23,6 +25,7 @@ import { isNotEmpty, } from '../../../shared/empty.util'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; +import { UploaderComponent } from '../../../shared/upload/uploader/uploader.component'; import { UploaderOptions } from '../../../shared/upload/uploader/uploader-options.model'; import { SectionsService } from '../../sections/sections.service'; import { SectionsType } from '../../sections/sections-type'; @@ -33,10 +36,15 @@ import parseSectionErrors from '../../utils/parseSectionErrors'; * This component represents the drop zone that provides to add files to the submission. */ @Component({ - selector: 'ds-submission-upload-files', + selector: 'ds-base-submission-upload-files', templateUrl: './submission-upload-files.component.html', + imports: [ + UploaderComponent, + NgIf, + ], + standalone: true, }) -export class SubmissionUploadFilesComponent implements OnChanges { +export class SubmissionUploadFilesComponent implements OnChanges, OnDestroy { /** * The collection id this submission belonging to diff --git a/src/app/submission/form/submission-upload-files/themed-submission-upload-files.component.ts b/src/app/submission/form/submission-upload-files/themed-submission-upload-files.component.ts new file mode 100644 index 0000000000..cfa913297c --- /dev/null +++ b/src/app/submission/form/submission-upload-files/themed-submission-upload-files.component.ts @@ -0,0 +1,44 @@ +import { + Component, + Input, +} from '@angular/core'; + +import { ThemedComponent } from '../../../shared/theme-support/themed.component'; +import { UploaderOptions } from '../../../shared/upload/uploader/uploader-options.model'; +import { SubmissionUploadFilesComponent } from './submission-upload-files.component'; + +/** + * Themed wrapper for {@link SubmissionUploadFilesComponent} + */ +@Component({ + selector: 'ds-submission-upload-files', + templateUrl: '../../../shared/theme-support/themed.component.html', + standalone: true, + imports: [SubmissionUploadFilesComponent], +}) +export class ThemedSubmissionUploadFilesComponent extends ThemedComponent { + + @Input() collectionId: string; + + @Input() submissionId: string; + + @Input() uploadFilesOptions: UploaderOptions; + + protected inAndOutputNames: (keyof SubmissionUploadFilesComponent & keyof this)[] = [ + 'collectionId', + 'submissionId', + 'uploadFilesOptions', + ]; + + protected getComponentName(): string { + return 'SubmissionUploadFilesComponent'; + } + + protected importThemedComponent(themeName: string): Promise { + return import(`../../../../themes/${themeName}/app/submission/form/submission-upload-files/submission-upload-files.component.ts`); + } + + protected importUnthemedComponent(): Promise { + return import('./submission-upload-files.component'); + } +} diff --git a/src/app/submission/import-external/import-external-collection/submission-import-external-collection.component.html b/src/app/submission/import-external/import-external-collection/submission-import-external-collection.component.html index 58697b09f3..e2b769295a 100644 --- a/src/app/submission/import-external/import-external-collection/submission-import-external-collection.component.html +++ b/src/app/submission/import-external/import-external-collection/submission-import-external-collection.component.html @@ -5,11 +5,11 @@ diff --git a/src/app/submission/import-external/import-external-collection/submission-import-external-collection.component.spec.ts b/src/app/submission/import-external/import-external-collection/submission-import-external-collection.component.spec.ts index 0ad28959ed..989726e7e4 100644 --- a/src/app/submission/import-external/import-external-collection/submission-import-external-collection.component.spec.ts +++ b/src/app/submission/import-external/import-external-collection/submission-import-external-collection.component.spec.ts @@ -4,7 +4,6 @@ import { } from '@angular/core'; import { ComponentFixture, - fakeAsync, inject, TestBed, waitForAsync, @@ -14,29 +13,42 @@ import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; import { TranslateModule } from '@ngx-translate/core'; import { CollectionListEntry } from '../../../shared/collection-dropdown/collection-dropdown.component'; +import { ThemedCollectionDropdownComponent } from '../../../shared/collection-dropdown/themed-collection-dropdown.component'; +import { ThemedLoadingComponent } from '../../../shared/loading/themed-loading.component'; +import { getMockThemeService } from '../../../shared/mocks/theme-service.mock'; import { createTestComponent } from '../../../shared/testing/utils.test'; +import { ThemeService } from '../../../shared/theme-support/theme.service'; import { SubmissionImportExternalCollectionComponent } from './submission-import-external-collection.component'; describe('SubmissionImportExternalCollectionComponent test suite', () => { let comp: SubmissionImportExternalCollectionComponent; let compAsAny: any; let fixture: ComponentFixture; + let themeService = getMockThemeService(); - beforeEach(waitForAsync (() => { + beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [ TranslateModule.forRoot(), - ], - declarations: [ SubmissionImportExternalCollectionComponent, TestComponent, ], providers: [ NgbActiveModal, SubmissionImportExternalCollectionComponent, + { provide: ThemeService, useValue: themeService }, ], schemas: [NO_ERRORS_SCHEMA], - }).compileComponents().then(); + }) + .overrideComponent(SubmissionImportExternalCollectionComponent, { + remove: { + imports: [ + ThemedLoadingComponent, + ThemedCollectionDropdownComponent, + ], + }, + }) + .compileComponents().then(); })); // First test to check the correct component creation @@ -127,13 +139,13 @@ describe('SubmissionImportExternalCollectionComponent test suite', () => { expect(comp.selectedEvent.emit).toHaveBeenCalledWith(selected); }); - it('dropdown should be invisible when the component is loading', fakeAsync(() => { + it('dropdown should be invisible when the component is loading', waitForAsync(() => { spyOn(comp, 'isLoading').and.returnValue(true); fixture.detectChanges(); fixture.whenStable().then(() => { - const dropdownMenu = fixture.debugElement.query(By.css('ds-themed-collection-dropdown')).nativeElement; + const dropdownMenu = fixture.debugElement.query(By.css('ds-collection-dropdown')).nativeElement; expect(dropdownMenu.classList).toContain('d-none'); }); })); @@ -145,6 +157,7 @@ describe('SubmissionImportExternalCollectionComponent test suite', () => { @Component({ selector: 'ds-test-cmp', template: ``, + standalone: true, }) class TestComponent { diff --git a/src/app/submission/import-external/import-external-collection/submission-import-external-collection.component.ts b/src/app/submission/import-external/import-external-collection/submission-import-external-collection.component.ts index 0642d4156b..03bd10cfc2 100644 --- a/src/app/submission/import-external/import-external-collection/submission-import-external-collection.component.ts +++ b/src/app/submission/import-external/import-external-collection/submission-import-external-collection.component.ts @@ -1,11 +1,18 @@ +import { + NgClass, + NgIf, +} from '@angular/common'; import { Component, EventEmitter, Output, } from '@angular/core'; import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import { TranslateModule } from '@ngx-translate/core'; import { CollectionListEntry } from '../../../shared/collection-dropdown/collection-dropdown.component'; +import { ThemedCollectionDropdownComponent } from '../../../shared/collection-dropdown/themed-collection-dropdown.component'; +import { ThemedLoadingComponent } from '../../../shared/loading/themed-loading.component'; /** * Wrap component for 'ds-collection-dropdown'. @@ -14,6 +21,14 @@ import { CollectionListEntry } from '../../../shared/collection-dropdown/collect selector: 'ds-submission-import-external-collection', styleUrls: ['./submission-import-external-collection.component.scss'], templateUrl: './submission-import-external-collection.component.html', + imports: [ + ThemedLoadingComponent, + ThemedCollectionDropdownComponent, + TranslateModule, + NgClass, + NgIf, + ], + standalone: true, }) export class SubmissionImportExternalCollectionComponent { /** diff --git a/src/app/submission/import-external/import-external-preview/submission-import-external-preview.component.html b/src/app/submission/import-external/import-external-preview/submission-import-external-preview.component.html index bbb0dbcc94..beecd68d70 100644 --- a/src/app/submission/import-external/import-external-preview/submission-import-external-preview.component.html +++ b/src/app/submission/import-external/import-external-preview/submission-import-external-preview.component.html @@ -18,10 +18,12 @@
-
- {{'item.preview.' + metadata.key | translate}} -

{{metadata.value.value}}

-
+

+ {{'item.preview.' + metadata.key | translate}}
+ + {{metadatum.value}}
+
+

diff --git a/src/app/submission/import-external/import-external-preview/submission-import-external-preview.component.spec.ts b/src/app/submission/import-external/import-external-preview/submission-import-external-preview.component.spec.ts index 210ae9cf27..06a048709a 100644 --- a/src/app/submission/import-external/import-external-preview/submission-import-external-preview.component.spec.ts +++ b/src/app/submission/import-external/import-external-preview/submission-import-external-preview.component.spec.ts @@ -59,8 +59,6 @@ describe('SubmissionImportExternalPreviewComponent test suite', () => { TestBed.configureTestingModule({ imports: [ TranslateModule.forRoot(), - ], - declarations: [ SubmissionImportExternalPreviewComponent, TestComponent, ], @@ -115,7 +113,7 @@ describe('SubmissionImportExternalPreviewComponent test suite', () => { it('Should init component properly', () => { comp.externalSourceEntry = externalEntry; const expected = [ - { key: 'dc.identifier.uri', value: Metadata.first(comp.externalSourceEntry.metadata, 'dc.identifier.uri') }, + { key: 'dc.identifier.uri', values: Metadata.all(comp.externalSourceEntry.metadata, 'dc.identifier.uri') }, ]; fixture.detectChanges(); @@ -173,6 +171,7 @@ describe('SubmissionImportExternalPreviewComponent test suite', () => { @Component({ selector: 'ds-test-cmp', template: ``, + standalone: true, }) class TestComponent { diff --git a/src/app/submission/import-external/import-external-preview/submission-import-external-preview.component.ts b/src/app/submission/import-external/import-external-preview/submission-import-external-preview.component.ts index 279e087093..c7f434b23c 100644 --- a/src/app/submission/import-external/import-external-preview/submission-import-external-preview.component.ts +++ b/src/app/submission/import-external/import-external-preview/submission-import-external-preview.component.ts @@ -1,3 +1,4 @@ +import { NgFor } from '@angular/common'; import { Component, Input, @@ -9,6 +10,7 @@ import { NgbModal, NgbModalRef, } from '@ng-bootstrap/ng-bootstrap'; +import { TranslateModule } from '@ngx-translate/core'; import { mergeMap } from 'rxjs/operators'; import { ExternalSourceEntry } from '../../../core/shared/external-source-entry.model'; @@ -27,6 +29,11 @@ import { SubmissionImportExternalCollectionComponent } from '../import-external- selector: 'ds-submission-import-external-preview', styleUrls: ['./submission-import-external-preview.component.scss'], templateUrl: './submission-import-external-preview.component.html', + imports: [ + NgFor, + TranslateModule, + ], + standalone: true, }) export class SubmissionImportExternalPreviewComponent implements OnInit { /** @@ -36,7 +43,7 @@ export class SubmissionImportExternalPreviewComponent implements OnInit { /** * The entry metadata list */ - public metadataList: { key: string, value: MetadataValue }[]; + public metadataList: { key: string, values: MetadataValue[] }[]; /** * The label prefix to use to generate the translation label */ @@ -71,7 +78,7 @@ export class SubmissionImportExternalPreviewComponent implements OnInit { metadataKeys.forEach((key) => { this.metadataList.push({ key: key, - value: Metadata.first(this.externalSourceEntry.metadata, key), + values: Metadata.all(this.externalSourceEntry.metadata, key), }); }); } diff --git a/src/app/submission/import-external/import-external-searchbar/submission-import-external-searchbar.component.spec.ts b/src/app/submission/import-external/import-external-searchbar/submission-import-external-searchbar.component.spec.ts index 7715835c64..9005dc1589 100644 --- a/src/app/submission/import-external/import-external-searchbar/submission-import-external-searchbar.component.spec.ts +++ b/src/app/submission/import-external/import-external-searchbar/submission-import-external-searchbar.component.spec.ts @@ -55,8 +55,6 @@ describe('SubmissionImportExternalSearchbarComponent test suite', () => { TestBed.configureTestingModule({ imports: [ TranslateModule.forRoot(), - ], - declarations: [ SubmissionImportExternalSearchbarComponent, TestComponent, ], @@ -184,6 +182,7 @@ describe('SubmissionImportExternalSearchbarComponent test suite', () => { @Component({ selector: 'ds-test-cmp', template: ``, + standalone: true, }) class TestComponent { initExternalSourceData = { entity: 'Publication', query: 'dummy', sourceId: 'ciencia' }; diff --git a/src/app/submission/import-external/import-external-searchbar/submission-import-external-searchbar.component.ts b/src/app/submission/import-external/import-external-searchbar/submission-import-external-searchbar.component.ts index a3fd396dc2..aace4de79a 100644 --- a/src/app/submission/import-external/import-external-searchbar/submission-import-external-searchbar.component.ts +++ b/src/app/submission/import-external/import-external-searchbar/submission-import-external-searchbar.component.ts @@ -1,3 +1,4 @@ +import { CommonModule } from '@angular/common'; import { ChangeDetectorRef, Component, @@ -7,6 +8,10 @@ import { OnInit, Output, } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'; +import { TranslateModule } from '@ngx-translate/core'; +import { InfiniteScrollModule } from 'ngx-infinite-scroll'; import { Observable, of as observableOf, @@ -59,6 +64,14 @@ export interface ExternalSourceData { selector: 'ds-submission-import-external-searchbar', styleUrls: ['./submission-import-external-searchbar.component.scss'], templateUrl: './submission-import-external-searchbar.component.html', + imports: [ + CommonModule, + TranslateModule, + InfiniteScrollModule, + NgbDropdownModule, + FormsModule, + ], + standalone: true, }) export class SubmissionImportExternalSearchbarComponent implements OnInit, OnDestroy { /** diff --git a/src/app/submission/import-external/submission-import-external.component.html b/src/app/submission/import-external/submission-import-external.component.html index 55acb591d4..21783f9a2e 100644 --- a/src/app/submission/import-external/submission-import-external.component.html +++ b/src/app/submission/import-external/submission-import-external.component.html @@ -22,8 +22,8 @@ [importConfig]="importConfig" (importObject)="import($event)"> - +
{{ 'search.results.empty' | translate }}
diff --git a/src/app/submission/import-external/submission-import-external.component.spec.ts b/src/app/submission/import-external/submission-import-external.component.spec.ts index a0955c9ea5..0364040c51 100644 --- a/src/app/submission/import-external/submission-import-external.component.spec.ts +++ b/src/app/submission/import-external/submission-import-external.component.spec.ts @@ -10,7 +10,10 @@ import { } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { Router } from '@angular/router'; +import { + ActivatedRoute, + Router, +} from '@angular/router'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { TranslateModule } from '@ngx-translate/core'; import { getTestScheduler } from 'jasmine-marbles'; @@ -21,7 +24,12 @@ import { ExternalSourceDataService } from '../../core/data/external-source-data. import { RouteService } from '../../core/services/route.service'; import { ExternalSourceEntry } from '../../core/shared/external-source-entry.model'; import { SearchConfigurationService } from '../../core/shared/search/search-configuration.service'; +import { AlertComponent } from '../../shared/alert/alert.component'; +import { HostWindowService } from '../../shared/host-window.service'; +import { ThemedLoadingComponent } from '../../shared/loading/themed-loading.component'; import { getMockExternalSourceService } from '../../shared/mocks/external-source.service.mock'; +import { getMockThemeService } from '../../shared/mocks/theme-service.mock'; +import { ObjectCollectionComponent } from '../../shared/object-collection/object-collection.component'; import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; import { createFailedRemoteDataObject$, @@ -29,14 +37,18 @@ import { createSuccessfulRemoteDataObject$, } from '../../shared/remote-data.utils'; import { PaginatedSearchOptions } from '../../shared/search/models/paginated-search-options.model'; +import { ActivatedRouteStub } from '../../shared/testing/active-router.stub'; +import { HostWindowServiceStub } from '../../shared/testing/host-window-service.stub'; import { routeServiceStub } from '../../shared/testing/route-service.stub'; import { RouterStub } from '../../shared/testing/router.stub'; import { createPaginatedList, createTestComponent, } from '../../shared/testing/utils.test'; +import { ThemeService } from '../../shared/theme-support/theme.service'; import { VarDirective } from '../../shared/utils/var.directive'; import { SubmissionImportExternalPreviewComponent } from './import-external-preview/submission-import-external-preview.component'; +import { SubmissionImportExternalSearchbarComponent } from './import-external-searchbar/submission-import-external-searchbar.component'; import { SubmissionImportExternalComponent } from './submission-import-external.component'; describe('SubmissionImportExternalComponent test suite', () => { @@ -62,8 +74,6 @@ describe('SubmissionImportExternalComponent test suite', () => { imports: [ TranslateModule.forRoot(), BrowserAnimationsModule, - ], - declarations: [ SubmissionImportExternalComponent, TestComponent, VarDirective, @@ -73,11 +83,25 @@ describe('SubmissionImportExternalComponent test suite', () => { { provide: SearchConfigurationService, useValue: searchConfigServiceStub }, { provide: RouteService, useValue: routeServiceStub }, { provide: Router, useValue: new RouterStub() }, + { provide: ActivatedRoute, useValue: new ActivatedRouteStub() }, { provide: NgbModal, useValue: ngbModal }, + { provide: HostWindowService, useValue: new HostWindowServiceStub(800) }, + { provide: ThemeService, useValue: getMockThemeService() }, SubmissionImportExternalComponent, ], schemas: [NO_ERRORS_SCHEMA], - }).compileComponents().then(); + }) + .overrideComponent(SubmissionImportExternalComponent, { + remove: { + imports: [ + ObjectCollectionComponent, + ThemedLoadingComponent, + AlertComponent, + SubmissionImportExternalSearchbarComponent, + ], + }, + }) + .compileComponents().then(); })); // First test to check the correct component creation @@ -520,6 +544,7 @@ describe('SubmissionImportExternalComponent test suite', () => { @Component({ selector: 'ds-test-cmp', template: ``, + standalone: true, }) class TestComponent { diff --git a/src/app/submission/import-external/submission-import-external.component.ts b/src/app/submission/import-external/submission-import-external.component.ts index 73fd472e29..47b5c09d66 100644 --- a/src/app/submission/import-external/submission-import-external.component.ts +++ b/src/app/submission/import-external/submission-import-external.component.ts @@ -1,13 +1,21 @@ +import { + AsyncPipe, + NgIf, +} from '@angular/common'; import { Component, OnDestroy, OnInit, } from '@angular/core'; -import { Router } from '@angular/router'; +import { + Router, + RouterLink, +} from '@angular/router'; import { NgbModal, NgbModalRef, } from '@ng-bootstrap/ng-bootstrap'; +import { TranslateModule } from '@ngx-translate/core'; import { BehaviorSubject, combineLatest, @@ -35,24 +43,43 @@ import { NONE_ENTITY_TYPE } from '../../core/shared/item-relationships/item-type import { getFinishedRemoteData } from '../../core/shared/operators'; import { PageInfo } from '../../core/shared/page-info.model'; import { SearchConfigurationService } from '../../core/shared/search/search-configuration.service'; +import { AlertComponent } from '../../shared/alert/alert.component'; import { fadeIn } from '../../shared/animations/fade'; import { hasValue, isNotEmpty, } from '../../shared/empty.util'; +import { ThemedLoadingComponent } from '../../shared/loading/themed-loading.component'; +import { ObjectCollectionComponent } from '../../shared/object-collection/object-collection.component'; import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils'; +import { VarDirective } from '../../shared/utils/var.directive'; import { SubmissionImportExternalPreviewComponent } from './import-external-preview/submission-import-external-preview.component'; -import { ExternalSourceData } from './import-external-searchbar/submission-import-external-searchbar.component'; +import { + ExternalSourceData, + SubmissionImportExternalSearchbarComponent, +} from './import-external-searchbar/submission-import-external-searchbar.component'; /** * This component allows to submit a new workspaceitem importing the data from an external source. */ @Component({ - selector: 'ds-submission-import-external', + selector: 'ds-base-submission-import-external', styleUrls: ['./submission-import-external.component.scss'], templateUrl: './submission-import-external.component.html', animations: [fadeIn], + imports: [ + ObjectCollectionComponent, + ThemedLoadingComponent, + AlertComponent, + NgIf, + AsyncPipe, + SubmissionImportExternalSearchbarComponent, + TranslateModule, + VarDirective, + RouterLink, + ], + standalone: true, }) export class SubmissionImportExternalComponent implements OnInit, OnDestroy { diff --git a/src/app/submission/import-external/themed-submission-import-external.component.ts b/src/app/submission/import-external/themed-submission-import-external.component.ts index a452ad09a9..bd7242293d 100644 --- a/src/app/submission/import-external/themed-submission-import-external.component.ts +++ b/src/app/submission/import-external/themed-submission-import-external.component.ts @@ -7,9 +7,11 @@ import { SubmissionImportExternalComponent } from './submission-import-external. * Themed wrapper for SubmissionImportExternalComponent */ @Component({ - selector: 'ds-themed-submission-import-external', + selector: 'ds-submission-import-external', styleUrls: [], templateUrl: './../../shared/theme-support/themed.component.html', + standalone: true, + imports: [SubmissionImportExternalComponent], }) export class ThemedSubmissionImportExternalComponent extends ThemedComponent { protected getComponentName(): string { diff --git a/src/app/submission/objects/section-visibility.model.ts b/src/app/submission/objects/section-visibility.model.ts index c41735178c..16cf16b2ab 100644 --- a/src/app/submission/objects/section-visibility.model.ts +++ b/src/app/submission/objects/section-visibility.model.ts @@ -5,3 +5,9 @@ export interface SectionVisibility { main: any; other: any; } + + +export enum SectionScope { + Submission = 'SUBMISSION', + Workflow = 'WORKFLOW', +} diff --git a/src/app/submission/objects/submission-objects.actions.ts b/src/app/submission/objects/submission-objects.actions.ts index e075ce5e5b..956c8f9533 100644 --- a/src/app/submission/objects/submission-objects.actions.ts +++ b/src/app/submission/objects/submission-objects.actions.ts @@ -11,7 +11,10 @@ import { } from '../../core/submission/models/workspaceitem-sections.model'; import { type } from '../../shared/ngrx/type'; import { SectionsType } from '../sections/sections-type'; -import { SectionVisibility } from './section-visibility.model'; +import { + SectionScope, + SectionVisibility, +} from './section-visibility.model'; import { SubmissionError } from './submission-error.model'; import { SubmissionSectionError } from './submission-section-error.model'; @@ -120,6 +123,7 @@ export class InitSectionAction implements Action { header: string; config: string; mandatory: boolean; + scope: SectionScope; sectionType: SectionsType; visibility: SectionVisibility; enabled: boolean; @@ -140,6 +144,8 @@ export class InitSectionAction implements Action { * the section's config * @param mandatory * the section's mandatory + * @param scope + * the section's scope * @param sectionType * the section's type * @param visibility @@ -156,12 +162,13 @@ export class InitSectionAction implements Action { header: string, config: string, mandatory: boolean, + scope: SectionScope, sectionType: SectionsType, visibility: SectionVisibility, enabled: boolean, data: WorkspaceitemSectionDataType, errors: SubmissionSectionError[]) { - this.payload = { submissionId, sectionId, header, config, mandatory, sectionType, visibility, enabled, data, errors }; + this.payload = { submissionId, sectionId, header, config, mandatory, scope, sectionType, visibility, enabled, data, errors }; } } diff --git a/src/app/submission/objects/submission-objects.effects.spec.ts b/src/app/submission/objects/submission-objects.effects.spec.ts index 66c6e8316e..a16dc365f2 100644 --- a/src/app/submission/objects/submission-objects.effects.spec.ts +++ b/src/app/submission/objects/submission-objects.effects.spec.ts @@ -82,6 +82,8 @@ describe('SubmissionObjectEffects test suite', () => { let submissionServiceStub; let submissionJsonPatchOperationsServiceStub; let submissionObjectDataServiceStub; + let workspaceItemDataService; + const collectionId: string = mockSubmissionCollectionId; const submissionId: string = mockSubmissionId; const submissionDefinitionResponse: any = mockSubmissionDefinitionResponse; @@ -98,6 +100,10 @@ describe('SubmissionObjectEffects test suite', () => { submissionServiceStub.hasUnsavedModification.and.returnValue(observableOf(true)); + workspaceItemDataService = jasmine.createSpyObj('WorkspaceItemDataService', { + invalidateById: observableOf(true), + }); + TestBed.configureTestingModule({ imports: [ StoreModule.forRoot({}, storeModuleConfig), @@ -122,6 +128,7 @@ describe('SubmissionObjectEffects test suite', () => { { provide: WorkflowItemDataService, useValue: {} }, { provide: HALEndpointService, useValue: {} }, { provide: SubmissionObjectDataService, useValue: submissionObjectDataServiceStub }, + { provide: WorkspaceitemDataService, useValue: workspaceItemDataService }, ], }); @@ -160,6 +167,7 @@ describe('SubmissionObjectEffects test suite', () => { sectionDefinition.header, config, sectionDefinition.mandatory, + sectionDefinition.scope, sectionDefinition.sectionType, sectionDefinition.visibility, enabled, diff --git a/src/app/submission/objects/submission-objects.effects.ts b/src/app/submission/objects/submission-objects.effects.ts index 53dcd9a19e..ef1610794c 100644 --- a/src/app/submission/objects/submission-objects.effects.ts +++ b/src/app/submission/objects/submission-objects.effects.ts @@ -37,6 +37,7 @@ import { WorkspaceitemSectionUploadObject } from '../../core/submission/models/w import { WorkspaceitemSectionsObject } from '../../core/submission/models/workspaceitem-sections.model'; import { SubmissionJsonPatchOperationsService } from '../../core/submission/submission-json-patch-operations.service'; import { SubmissionObjectDataService } from '../../core/submission/submission-object-data.service'; +import { WorkspaceitemDataService } from '../../core/submission/workspaceitem-data.service'; import { isEmpty, isNotEmpty, @@ -115,6 +116,7 @@ export class SubmissionObjectEffects { sectionDefinition.header, config, sectionDefinition.mandatory, + sectionDefinition.scope, sectionDefinition.sectionType, sectionDefinition.visibility, enabled, @@ -287,6 +289,7 @@ export class SubmissionObjectEffects { depositSubmissionSuccess$ = createEffect(() => this.actions$.pipe( ofType(SubmissionObjectActionTypes.DEPOSIT_SUBMISSION_SUCCESS), tap(() => this.notificationsService.success(null, this.translate.get('submission.sections.general.deposit_success_notice'))), + tap((action: DepositSubmissionSuccessAction) => this.workspaceItemDataService.invalidateById(action.payload.submissionId)), tap(() => this.submissionService.redirectToMyDSpace())), { dispatch: false }); /** @@ -355,14 +358,17 @@ export class SubmissionObjectEffects { ofType(SubmissionObjectActionTypes.DISCARD_SUBMISSION_ERROR), tap(() => this.notificationsService.error(null, this.translate.get('submission.sections.general.discard_error_notice')))), { dispatch: false }); - constructor(private actions$: Actions, + constructor( + private actions$: Actions, private notificationsService: NotificationsService, private operationsService: SubmissionJsonPatchOperationsService, private sectionService: SectionsService, private store$: Store, private submissionService: SubmissionService, private submissionObjectService: SubmissionObjectDataService, - private translate: TranslateService) { + private translate: TranslateService, + private workspaceItemDataService: WorkspaceitemDataService, + ) { } /** @@ -501,7 +507,7 @@ function getForm(forms, currentState, sectionId) { * Whether notifications are enabled */ function filterErrors(sectionForm: FormState, sectionErrors: SubmissionSectionError[], sectionType: string, notify: boolean): SubmissionSectionError[] { - if (notify || sectionType !== SectionsType.SubmissionForm) { + if (notify || sectionType !== SectionsType.SubmissionForm.valueOf()) { return sectionErrors; } if (!sectionForm || !sectionForm.touched) { diff --git a/src/app/submission/objects/submission-objects.reducer.spec.ts b/src/app/submission/objects/submission-objects.reducer.spec.ts index 2389a5600c..2bbb875c3c 100644 --- a/src/app/submission/objects/submission-objects.reducer.spec.ts +++ b/src/app/submission/objects/submission-objects.reducer.spec.ts @@ -237,6 +237,7 @@ describe('submissionReducer test suite', () => { header: 'submit.progressbar.describe.stepone', config: 'https://rest.api/dspace-spring-rest/api/config/submissionforms/traditionalpageone', mandatory: true, + scope: null, sectionType: 'submission-form', visibility: undefined, collapsed: false, @@ -257,6 +258,7 @@ describe('submissionReducer test suite', () => { 'submit.progressbar.describe.stepone', 'https://rest.api/dspace-spring-rest/api/config/submissionforms/traditionalpageone', true, + null, SectionsType.SubmissionForm, undefined, true, diff --git a/src/app/submission/objects/submission-objects.reducer.ts b/src/app/submission/objects/submission-objects.reducer.ts index 82e33062cd..a34cf236b9 100644 --- a/src/app/submission/objects/submission-objects.reducer.ts +++ b/src/app/submission/objects/submission-objects.reducer.ts @@ -565,6 +565,7 @@ function initSection(state: SubmissionObjectState, action: InitSectionAction): S header: action.payload.header, config: action.payload.config, mandatory: action.payload.mandatory, + scope: action.payload.scope, sectionType: action.payload.sectionType, visibility: action.payload.visibility, collapsed: false, diff --git a/src/app/submission/objects/submission-section-object.model.ts b/src/app/submission/objects/submission-section-object.model.ts index 7338539607..5d8a14c745 100644 --- a/src/app/submission/objects/submission-section-object.model.ts +++ b/src/app/submission/objects/submission-section-object.model.ts @@ -1,6 +1,9 @@ import { WorkspaceitemSectionDataType } from '../../core/submission/models/workspaceitem-sections.model'; import { SectionsType } from '../sections/sections-type'; -import { SectionVisibility } from './section-visibility.model'; +import { + SectionScope, + SectionVisibility, +} from './section-visibility.model'; import { SubmissionSectionError } from './submission-section-error.model'; /** @@ -22,6 +25,11 @@ export interface SubmissionSectionObject { */ mandatory: boolean; + /** + * The submission scope for this section + */ + scope: SectionScope; + /** * The section type */ diff --git a/src/app/submission/provide-submission-state.ts b/src/app/submission/provide-submission-state.ts new file mode 100644 index 0000000000..3656df6da3 --- /dev/null +++ b/src/app/submission/provide-submission-state.ts @@ -0,0 +1,28 @@ +import { + EnvironmentProviders, + importProvidersFrom, + makeEnvironmentProviders, +} from '@angular/core'; +import { EffectsModule } from '@ngrx/effects'; +import { + Action, + StoreConfig, + StoreModule, +} from '@ngrx/store'; + +import { storeModuleConfig } from '../app.reducer'; +import { submissionEffects } from './submission.effects'; +import { + submissionReducers, + SubmissionState, +} from './submission.reducers'; + +export const provideSubmissionState = (): EnvironmentProviders => { + return makeEnvironmentProviders([ + importProvidersFrom( + StoreModule.forFeature('submission', submissionReducers, storeModuleConfig as StoreConfig), + EffectsModule.forFeature(submissionEffects), + ), + ]); +}; + diff --git a/src/app/submission/sections/accesses/section-accesses.component.spec.ts b/src/app/submission/sections/accesses/section-accesses.component.spec.ts index 92fc7eefd7..c49bd74e0d 100644 --- a/src/app/submission/sections/accesses/section-accesses.component.spec.ts +++ b/src/app/submission/sections/accesses/section-accesses.component.spec.ts @@ -1,26 +1,32 @@ +import { CommonModule } from '@angular/common'; import { ComponentFixture, - inject, TestBed, } from '@angular/core/testing'; -import { BrowserModule } from '@angular/platform-browser'; import { + DYNAMIC_FORM_CONTROL_MAP_FN, DynamicCheckboxModel, DynamicDatePickerModel, DynamicFormArrayModel, DynamicSelectModel, } from '@ng-dynamic-forms/core'; import { Store } from '@ngrx/store'; -import { - TranslateModule, - TranslateService, -} from '@ngx-translate/core'; +import { provideMockStore } from '@ngrx/store/testing'; +import { TranslateModule } from '@ngx-translate/core'; import { of as observableOf } from 'rxjs'; +import { + APP_CONFIG, + APP_DATA_SERVICES_MAP, +} from 'src/config/app-config.interface'; +import { environment } from 'src/environments/environment.test'; -import { AppState } from '../../../app.reducer'; import { SubmissionAccessesConfigDataService } from '../../../core/config/submission-accesses-config-data.service'; import { JsonPatchOperationsBuilder } from '../../../core/json-patch/builder/json-patch-operations-builder'; import { SubmissionJsonPatchOperationsService } from '../../../core/submission/submission-json-patch-operations.service'; +import { SubmissionObjectDataService } from '../../../core/submission/submission-object-data.service'; +import { XSRFService } from '../../../core/xsrf/xsrf.service'; +import { dsDynamicFormControlMapFn } from '../../../shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-map-fn'; +import { DsDynamicTypeBindRelationService } from '../../../shared/form/builder/ds-dynamic-form-ui/ds-dynamic-type-bind-relation.service'; import { FormBuilderService } from '../../../shared/form/builder/form-builder.service'; import { FormComponent } from '../../../shared/form/form.component'; import { FormService } from '../../../shared/form/form.service'; @@ -33,18 +39,27 @@ import { getSubmissionAccessesConfigService, } from '../../../shared/mocks/section-accesses-config.service.mock'; import { mockAccessesFormData } from '../../../shared/mocks/submission.mock'; -import { getMockTranslateService } from '../../../shared/mocks/translate.service.mock'; import { accessConditionChangeEvent, checkboxChangeEvent, } from '../../../shared/testing/form-event.stub'; import { SectionsServiceStub } from '../../../shared/testing/sections-service.stub'; import { SubmissionJsonPatchOperationsServiceStub } from '../../../shared/testing/submission-json-patch-operations-service.stub'; +import { SubmissionService } from '../../submission.service'; import { SectionFormOperationsService } from '../form/section-form-operations.service'; import { SectionsService } from '../sections.service'; import { SubmissionSectionAccessesComponent } from './section-accesses.component'; import { SectionAccessesService } from './section-accesses.service'; + +function getMockDsDynamicTypeBindRelationService(): DsDynamicTypeBindRelationService { + return jasmine.createSpyObj('DsDynamicTypeBindRelationService', { + getRelatedFormModel: jasmine.createSpy('getRelatedFormModel'), + matchesCondition: jasmine.createSpy('matchesCondition'), + subscribeRelations: jasmine.createSpy('subscribeRelations'), + }); +} + describe('SubmissionSectionAccessesComponent', () => { let component: SubmissionSectionAccessesComponent; let fixture: ComponentFixture; @@ -87,29 +102,37 @@ describe('SubmissionSectionAccessesComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ imports: [ - BrowserModule, + CommonModule, TranslateModule.forRoot(), + SubmissionSectionAccessesComponent, + FormComponent, ], - declarations: [SubmissionSectionAccessesComponent, FormComponent], providers: [ { provide: SectionsService, useValue: sectionsServiceStub }, { provide: SubmissionAccessesConfigDataService, useValue: submissionAccessesConfigService }, { provide: SectionAccessesService, useValue: sectionAccessesService }, { provide: SectionFormOperationsService, useValue: sectionFormOperationsService }, { provide: JsonPatchOperationsBuilder, useValue: operationsBuilder }, - { provide: TranslateService, useValue: getMockTranslateService() }, { provide: FormService, useValue: getMockFormService() }, { provide: Store, useValue: storeStub }, { provide: SubmissionJsonPatchOperationsService, useValue: SubmissionJsonPatchOperationsServiceStub }, { provide: 'sectionDataProvider', useValue: sectionData }, { provide: 'submissionIdProvider', useValue: '1508' }, + { provide: DsDynamicTypeBindRelationService, useValue: getMockDsDynamicTypeBindRelationService() }, + { provide: SubmissionObjectDataService, useValue: {} }, + { provide: SubmissionService, useValue: {} }, + { provide: XSRFService, useValue: {} }, + { provide: APP_CONFIG, useValue: environment }, + { provide: APP_DATA_SERVICES_MAP, useValue: {} }, + { provide: DYNAMIC_FORM_CONTROL_MAP_FN, useValue: dsDynamicFormControlMapFn }, FormBuilderService, + provideMockStore({}), ], }) .compileComponents(); }); - beforeEach(inject([Store], (store: Store) => { + beforeEach(() => { fixture = TestBed.createComponent(SubmissionSectionAccessesComponent); component = fixture.componentInstance; formService = TestBed.inject(FormService); @@ -118,8 +141,7 @@ describe('SubmissionSectionAccessesComponent', () => { formService.isValid.and.returnValue(observableOf(true)); formService.getFormData.and.returnValue(observableOf(mockAccessesFormData)); fixture.detectChanges(); - })); - + }); it('should create', () => { expect(component).toBeTruthy(); @@ -173,42 +195,49 @@ describe('SubmissionSectionAccessesComponent', () => { describe('when canDescoverable is false', () => { - - beforeEach(async () => { + formService = getMockFormService(); await TestBed.configureTestingModule({ imports: [ - BrowserModule, + CommonModule, TranslateModule.forRoot(), + SubmissionSectionAccessesComponent, + FormComponent, ], - declarations: [SubmissionSectionAccessesComponent, FormComponent], providers: [ { provide: SectionsService, useValue: sectionsServiceStub }, - { provide: FormBuilderService, useValue: builderService }, { provide: SubmissionAccessesConfigDataService, useValue: getSubmissionAccessesConfigNotChangeDiscoverableService() }, { provide: SectionAccessesService, useValue: sectionAccessesService }, { provide: SectionFormOperationsService, useValue: sectionFormOperationsService }, { provide: JsonPatchOperationsBuilder, useValue: operationsBuilder }, - { provide: TranslateService, useValue: getMockTranslateService() }, - { provide: FormService, useValue: getMockFormService() }, + { provide: FormService, useValue: formService }, { provide: Store, useValue: storeStub }, { provide: SubmissionJsonPatchOperationsService, useValue: SubmissionJsonPatchOperationsServiceStub }, { provide: 'sectionDataProvider', useValue: sectionData }, { provide: 'submissionIdProvider', useValue: '1508' }, + { provide: DsDynamicTypeBindRelationService, useValue: getMockDsDynamicTypeBindRelationService() }, + { provide: SubmissionObjectDataService, useValue: {} }, + { provide: SubmissionService, useValue: {} }, + { provide: XSRFService, useValue: {} }, + { provide: APP_CONFIG, useValue: environment }, + { provide: APP_DATA_SERVICES_MAP, useValue: {} }, + { provide: DYNAMIC_FORM_CONTROL_MAP_FN, useValue: dsDynamicFormControlMapFn }, + FormBuilderService, + provideMockStore({}), + ], }) .compileComponents(); }); - beforeEach(inject([Store], (store: Store) => { + beforeEach(() => { fixture = TestBed.createComponent(SubmissionSectionAccessesComponent); component = fixture.componentInstance; - formService = TestBed.inject(FormService); formService.validateAllFormFields.and.callFake(() => null); formService.isValid.and.returnValue(observableOf(true)); formService.getFormData.and.returnValue(observableOf(mockAccessesFormData)); fixture.detectChanges(); - })); + }); it('should have formModel length should be 1', () => { diff --git a/src/app/submission/sections/accesses/section-accesses.component.ts b/src/app/submission/sections/accesses/section-accesses.component.ts index 5c8d7afb14..b47f1cf8ff 100644 --- a/src/app/submission/sections/accesses/section-accesses.component.ts +++ b/src/app/submission/sections/accesses/section-accesses.component.ts @@ -1,3 +1,4 @@ +import { NgIf } from '@angular/common'; import { Component, Inject, @@ -53,8 +54,6 @@ import { SectionFormOperationsService } from '../form/section-form-operations.se import { SectionModelComponent } from '../models/section.model'; import { SectionDataObject } from '../models/section-data.model'; import { SectionsService } from '../sections.service'; -import { renderSectionFor } from '../sections-decorator'; -import { SectionsType } from '../sections-type'; import { ACCESS_CONDITION_GROUP_CONFIG, ACCESS_CONDITION_GROUP_LAYOUT, @@ -78,8 +77,12 @@ import { SectionAccessesService } from './section-accesses.service'; selector: 'ds-section-accesses', templateUrl: './section-accesses.component.html', styleUrls: ['./section-accesses.component.scss'], + imports: [ + FormComponent, + NgIf, + ], + standalone: true, }) -@renderSectionFor(SectionsType.AccessesCondition) export class SubmissionSectionAccessesComponent extends SectionModelComponent { /** diff --git a/src/app/submission/sections/accesses/section-accesses.service.ts b/src/app/submission/sections/accesses/section-accesses.service.ts index cb9b1e5ef3..4fa3a632d6 100644 --- a/src/app/submission/sections/accesses/section-accesses.service.ts +++ b/src/app/submission/sections/accesses/section-accesses.service.ts @@ -14,7 +14,7 @@ import { SubmissionState } from '../../submission.reducers'; /** * A service that provides methods to handle submission item's accesses condition state. */ -@Injectable() +@Injectable({ providedIn: 'root' }) export class SectionAccessesService { /** diff --git a/src/app/submission/sections/cc-license/submission-section-cc-licenses.component.html b/src/app/submission/sections/cc-license/submission-section-cc-licenses.component.html index 7c6145a65e..a8d6ef3262 100644 --- a/src/app/submission/sections/cc-license/submission-section-cc-licenses.component.html +++ b/src/app/submission/sections/cc-license/submission-section-cc-licenses.component.html @@ -1,38 +1,39 @@ -
- +@if (submissionCcLicenses) { +
+
+ + + @if(submissionCcLicenses?.length === 0) { + + } @else { + @for(license of submissionCcLicenses; track license.id) { + + } + } +
+
+
+} @@ -121,7 +122,7 @@
- +
diff --git a/src/app/submission/sections/cc-license/submission-section-cc-licenses.component.scss b/src/app/submission/sections/cc-license/submission-section-cc-licenses.component.scss index 62a902b79a..142cd82822 100644 --- a/src/app/submission/sections/cc-license/submission-section-cc-licenses.component.scss +++ b/src/app/submission/sections/cc-license/submission-section-cc-licenses.component.scss @@ -1,3 +1,13 @@ .options-select-menu { max-height: 25vh; } + +.ccLicense-select { + width: fit-content; +} + +.scrollable-menu { + height: auto; + max-height: var(--ds-dropdown-menu-max-height); + overflow-x: hidden; +} diff --git a/src/app/submission/sections/cc-license/submission-section-cc-licenses.component.spec.ts b/src/app/submission/sections/cc-license/submission-section-cc-licenses.component.spec.ts index 9048a3d25e..2e82948c14 100644 --- a/src/app/submission/sections/cc-license/submission-section-cc-licenses.component.spec.ts +++ b/src/app/submission/sections/cc-license/submission-section-cc-licenses.component.spec.ts @@ -8,6 +8,7 @@ import { By } from '@angular/platform-browser'; import { TranslateModule } from '@ngx-translate/core'; import { cold } from 'jasmine-marbles'; import { of as observableOf } from 'rxjs'; +import { FormBuilderService } from 'src/app/shared/form/builder/form-builder.service'; import { ConfigurationDataService } from '../../../core/data/configuration-data.service'; import { JsonPatchOperationsBuilder } from '../../../core/json-patch/builder/json-patch-operations-builder'; @@ -16,8 +17,8 @@ import { SUBMISSION_CC_LICENSE } from '../../../core/submission/models/submissio import { SubmissionCcLicence } from '../../../core/submission/models/submission-cc-license.model'; import { SubmissionCcLicenseDataService } from '../../../core/submission/submission-cc-license-data.service'; import { SubmissionCcLicenseUrlDataService } from '../../../core/submission/submission-cc-license-url-data.service'; +import { ThemedLoadingComponent } from '../../../shared/loading/themed-loading.component'; import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; -import { SharedModule } from '../../../shared/shared.module'; import { createPaginatedList } from '../../../shared/testing/utils.test'; import { SectionDataObject } from '../models/section-data.model'; import { SectionsService } from '../sections.service'; @@ -174,10 +175,7 @@ describe('SubmissionSectionCcLicensesComponent', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [ - SharedModule, TranslateModule.forRoot(), - ], - declarations: [ SubmissionSectionCcLicensesComponent, ], providers: [ @@ -189,8 +187,16 @@ describe('SubmissionSectionCcLicensesComponent', () => { { provide: 'collectionIdProvider', useValue: 'test collection id' }, { provide: 'sectionDataProvider', useValue: Object.assign({}, sectionObject) }, { provide: 'submissionIdProvider', useValue: 'test submission id' }, + { provide: FormBuilderService, useValue: {} }, ], }) + .overrideComponent(SubmissionSectionCcLicensesComponent, { + remove: { + imports:[ + ThemedLoadingComponent, + ], + }, + }) .compileComponents(); })); @@ -203,10 +209,10 @@ describe('SubmissionSectionCcLicensesComponent', () => { it('should display a dropdown with the different cc licenses', () => { expect( - de.query(By.css('.ccLicense-select ds-select .dropdown-menu button:nth-child(1)')).nativeElement.innerText, + de.query(By.css('.ccLicense-select .scrollable-menu button:nth-child(1)')).nativeElement.innerText, ).toContain('test license name 1'); expect( - de.query(By.css('.ccLicense-select ds-select .dropdown-menu button:nth-child(2)')).nativeElement.innerText, + de.query(By.css('.ccLicense-select .scrollable-menu button:nth-child(2)')).nativeElement.innerText, ).toContain('test license name 2'); }); @@ -220,9 +226,7 @@ describe('SubmissionSectionCcLicensesComponent', () => { }); it('should display the selected cc license', () => { - expect( - de.query(By.css('.ccLicense-select ds-select button.selection')).nativeElement.innerText, - ).toContain('test license name 2'); + expect(component.selectedCcLicense.name).toContain('test license name 2'); }); it('should display all field labels of the selected cc license only', () => { diff --git a/src/app/submission/sections/cc-license/submission-section-cc-licenses.component.ts b/src/app/submission/sections/cc-license/submission-section-cc-licenses.component.ts index 78feb16f11..cc1925a4a6 100644 --- a/src/app/submission/sections/cc-license/submission-section-cc-licenses.component.ts +++ b/src/app/submission/sections/cc-license/submission-section-cc-licenses.component.ts @@ -1,11 +1,21 @@ import { + AsyncPipe, + NgForOf, + NgIf, +} from '@angular/common'; +import { + ChangeDetectorRef, Component, Inject, } from '@angular/core'; +import { FormsModule } from '@angular/forms'; import { + NgbDropdownModule, NgbModal, NgbModalRef, } from '@ng-bootstrap/ng-bootstrap'; +import { TranslateModule } from '@ngx-translate/core'; +import { InfiniteScrollModule } from 'ngx-infinite-scroll'; import { Observable, of as observableOf, @@ -16,14 +26,16 @@ import { filter, map, take, + tap, } from 'rxjs/operators'; import { ConfigurationDataService } from '../../../core/data/configuration-data.service'; +import { FindListOptions } from '../../../core/data/find-list-options.model'; import { JsonPatchOperationPathCombiner } from '../../../core/json-patch/builder/json-patch-operation-path-combiner'; import { JsonPatchOperationsBuilder } from '../../../core/json-patch/builder/json-patch-operations-builder'; import { getFirstCompletedRemoteData, - getFirstSucceededRemoteData, + getFirstSucceededRemoteDataPayload, getRemoteDataPayload, } from '../../../core/shared/operators'; import { @@ -34,11 +46,13 @@ import { import { WorkspaceitemSectionCcLicenseObject } from '../../../core/submission/models/workspaceitem-section-cc-license.model'; import { SubmissionCcLicenseDataService } from '../../../core/submission/submission-cc-license-data.service'; import { SubmissionCcLicenseUrlDataService } from '../../../core/submission/submission-cc-license-url-data.service'; +import { DsSelectComponent } from '../../../shared/ds-select/ds-select.component'; import { isNotEmpty } from '../../../shared/empty.util'; +import { ThemedLoadingComponent } from '../../../shared/loading/themed-loading.component'; +import { VarDirective } from '../../../shared/utils/var.directive'; import { SectionModelComponent } from '../models/section.model'; import { SectionDataObject } from '../models/section-data.model'; import { SectionsService } from '../sections.service'; -import { renderSectionFor } from '../sections-decorator'; import { SectionsType } from '../sections-type'; /** @@ -48,8 +62,20 @@ import { SectionsType } from '../sections-type'; selector: 'ds-submission-section-cc-licenses', templateUrl: './submission-section-cc-licenses.component.html', styleUrls: ['./submission-section-cc-licenses.component.scss'], + imports: [ + TranslateModule, + NgIf, + ThemedLoadingComponent, + AsyncPipe, + VarDirective, + NgForOf, + DsSelectComponent, + NgbDropdownModule, + FormsModule, + InfiniteScrollModule, + ], + standalone: true, }) -@renderSectionFor(SectionsType.CcLicense) export class SubmissionSectionCcLicensesComponent extends SectionModelComponent { /** @@ -78,7 +104,7 @@ export class SubmissionSectionCcLicensesComponent extends SectionModelComponent /** * Cache of the available Creative Commons licenses. */ - submissionCcLicenses: SubmissionCcLicence[]; + submissionCcLicenses: SubmissionCcLicence[] = []; /** * Reference to NgbModal @@ -90,6 +116,25 @@ export class SubmissionSectionCcLicensesComponent extends SectionModelComponent */ defaultJurisdiction: string; + /** + * The currently selected cc licence + */ + selectedCcLicense: SubmissionCcLicence = new SubmissionCcLicence(); + + /** + * Options for paginated data loading + */ + ccLicenceOptions: FindListOptions = { + elementsPerPage: 20, + currentPage: 1, + }; + /** + * Check to stop paginated search + * + * @private + */ + private _isLastPage: boolean; + /** * The Creative Commons link saved in the workspace item. */ @@ -114,6 +159,7 @@ export class SubmissionSectionCcLicensesComponent extends SectionModelComponent protected submissionCcLicenseUrlDataService: SubmissionCcLicenseUrlDataService, protected operationsBuilder: JsonPatchOperationsBuilder, protected configService: ConfigurationDataService, + protected ref: ChangeDetectorRef, @Inject('collectionIdProvider') public injectedCollectionId: string, @Inject('sectionDataProvider') public injectedSectionData: SectionDataObject, @Inject('submissionIdProvider') public injectedSubmissionId: string, @@ -137,9 +183,10 @@ export class SubmissionSectionCcLicensesComponent extends SectionModelComponent * @param ccLicense the Creative Commons license to select. */ selectCcLicense(ccLicense: SubmissionCcLicence) { - if (!!this.getSelectedCcLicense() && this.getSelectedCcLicense().id === ccLicense.id) { + if (this.selectedCcLicense.id === ccLicense.id) { return; } + this.selectedCcLicense = ccLicense; this.setAccepted(false); this.updateSectionData({ ccLicense: { @@ -283,13 +330,6 @@ export class SubmissionSectionCcLicensesComponent extends SectionModelComponent } this.sectionData.data = data; }), - this.submissionCcLicensesDataService.findAll({ elementsPerPage: 9999 }).pipe( - getFirstSucceededRemoteData(), - getRemoteDataPayload(), - map((list) => list.page), - ).subscribe( - (licenses) => this.submissionCcLicenses = licenses, - ), this.configService.findByPropertyName('cc.license.jurisdiction').pipe( getFirstCompletedRemoteData(), getRemoteDataPayload(), @@ -302,6 +342,7 @@ export class SubmissionSectionCcLicensesComponent extends SectionModelComponent } }), ); + this.loadCcLicences(); } /** @@ -321,4 +362,31 @@ export class SubmissionSectionCcLicensesComponent extends SectionModelComponent updateSectionData(data: WorkspaceitemSectionCcLicenseObject) { this.sectionService.updateSectionData(this.submissionId, this.sectionData.id, Object.assign({}, this.data, data)); } + + onScroll(event) { + if (event.target.scrollTop + event.target.clientHeight >= event.target.scrollHeight) { + if (!this.isLoading && !this._isLastPage) { + this.ccLicenceOptions.currentPage++; + this.loadCcLicences(); + } + } + } + + loadCcLicences() { + this.isLoading = true; + + this.subscriptions.push( + this.submissionCcLicensesDataService.findAll(this.ccLicenceOptions).pipe( + getFirstSucceededRemoteDataPayload(), + tap((response) => this._isLastPage = response.pageInfo.currentPage === response.pageInfo.totalPages), + map((list) => list.page), + ).subscribe( + (licenses) => { + this.submissionCcLicenses = [...this.submissionCcLicenses, ...licenses]; + this.isLoading = false; + this.ref.detectChanges(); + }, + ), + ); + } } diff --git a/src/app/submission/sections/container/section-container.component.spec.ts b/src/app/submission/sections/container/section-container.component.spec.ts index 2bec3c0ac5..4dfa65a13b 100644 --- a/src/app/submission/sections/container/section-container.component.spec.ts +++ b/src/app/submission/sections/container/section-container.component.spec.ts @@ -79,12 +79,10 @@ describe('SubmissionSectionContainerComponent test suite', () => { imports: [ NgbModule, TranslateModule.forRoot(), - ], - declarations: [ SubmissionSectionContainerComponent, SectionsDirective, TestComponent, - ], // declare the test component + ], providers: [ { provide: SectionsService, useValue: sectionsServiceStub }, { provide: SubmissionService, useValue: submissionServiceStub }, @@ -237,6 +235,8 @@ describe('SubmissionSectionContainerComponent test suite', () => { // eslint-disable-next-line @angular-eslint/component-selector selector: '', template: ``, + standalone: true, + imports: [NgbModule], }) class TestComponent { diff --git a/src/app/submission/sections/container/section-container.component.ts b/src/app/submission/sections/container/section-container.component.ts index ed035ee6ff..6f4126a173 100644 --- a/src/app/submission/sections/container/section-container.component.ts +++ b/src/app/submission/sections/container/section-container.component.ts @@ -1,3 +1,10 @@ +import { + AsyncPipe, + NgClass, + NgComponentOutlet, + NgForOf, + NgIf, +} from '@angular/common'; import { Component, Injector, @@ -5,7 +12,10 @@ import { OnInit, ViewChild, } from '@angular/core'; +import { NgbAccordionModule } from '@ng-bootstrap/ng-bootstrap'; +import { TranslateModule } from '@ngx-translate/core'; +import { AlertComponent } from '../../../shared/alert/alert.component'; import { AlertType } from '../../../shared/alert/alert-type'; import { SectionDataObject } from '../models/section-data.model'; import { SectionsDirective } from '../sections.directive'; @@ -18,6 +28,18 @@ import { rendersSectionType } from '../sections-decorator'; selector: 'ds-submission-section-container', templateUrl: './section-container.component.html', styleUrls: ['./section-container.component.scss'], + imports: [ + AlertComponent, + NgForOf, + NgbAccordionModule, + NgComponentOutlet, + TranslateModule, + NgClass, + NgIf, + AsyncPipe, + SectionsDirective, + ], + standalone: true, }) export class SubmissionSectionContainerComponent implements OnInit { @@ -93,7 +115,7 @@ export class SubmissionSectionContainerComponent implements OnInit { /** * Find the correct component based on the section's type */ - getSectionContent(): string { + getSectionContent() { return rendersSectionType(this.sectionData.sectionType); } } diff --git a/src/app/submission/sections/duplicates/section-duplicates.component.spec.ts b/src/app/submission/sections/duplicates/section-duplicates.component.spec.ts index 3128a775fd..501a60e3b8 100644 --- a/src/app/submission/sections/duplicates/section-duplicates.component.spec.ts +++ b/src/app/submission/sections/duplicates/section-duplicates.component.spec.ts @@ -31,6 +31,7 @@ import { MetadataValue } from '../../../core/shared/metadata.models'; import { SubmissionScopeType } from '../../../core/submission/submission-scope-type'; import { FormBuilderService } from '../../../shared/form/builder/form-builder.service'; import { FormService } from '../../../shared/form/form.service'; +import { getMockFormBuilderService } from '../../../shared/mocks/form-builder-service.mock'; import { getMockFormOperationsService } from '../../../shared/mocks/form-operations-service.mock'; import { getMockFormService } from '../../../shared/mocks/form-service.mock'; import { @@ -149,8 +150,6 @@ describe('SubmissionSectionDuplicatesComponent test suite', () => { NgxPaginationModule, NoopAnimationsModule, TranslateModule.forRoot(), - ], - declarations: [ SubmissionSectionDuplicatesComponent, TestComponent, ObjNgFor, @@ -170,7 +169,7 @@ describe('SubmissionSectionDuplicatesComponent test suite', () => { { provide: 'submissionIdProvider', useValue: submissionId }, { provide: PaginationService, useValue: paginationService }, ChangeDetectorRef, - FormBuilderService, + { provide: FormBuilderService, useValue: getMockFormBuilderService() }, ], schemas: [NO_ERRORS_SCHEMA], }).compileComponents().then(); @@ -255,6 +254,12 @@ describe('SubmissionSectionDuplicatesComponent test suite', () => { @Component({ selector: 'ds-test-cmp', template: ``, + standalone: true, + imports: [BrowserModule, + CommonModule, + FormsModule, + ReactiveFormsModule, + NgxPaginationModule], }) class TestComponent { diff --git a/src/app/submission/sections/duplicates/section-duplicates.component.ts b/src/app/submission/sections/duplicates/section-duplicates.component.ts index d4065153ae..885511ca52 100644 --- a/src/app/submission/sections/duplicates/section-duplicates.component.ts +++ b/src/app/submission/sections/duplicates/section-duplicates.component.ts @@ -1,9 +1,18 @@ +import { + AsyncPipe, + NgForOf, + NgIf, +} from '@angular/common'; import { ChangeDetectionStrategy, Component, Inject, + OnInit, } from '@angular/core'; -import { TranslateService } from '@ngx-translate/core'; +import { + TranslateModule, + TranslateService, +} from '@ngx-translate/core'; import { Observable, of as observableOf, @@ -15,12 +24,11 @@ import { WorkspaceitemSectionDuplicatesObject } from '../../../core/submission/m import { URLCombiner } from '../../../core/url-combiner/url-combiner'; import { getItemModuleRoute } from '../../../item-page/item-page-routing-paths'; import { AlertType } from '../../../shared/alert/alert-type'; +import { VarDirective } from '../../../shared/utils/var.directive'; import { SubmissionService } from '../../submission.service'; import { SectionModelComponent } from '../models/section.model'; import { SectionDataObject } from '../models/section-data.model'; import { SectionsService } from '../sections.service'; -import { renderSectionFor } from '../sections-decorator'; -import { SectionsType } from '../sections-type'; /** * Detect duplicates step @@ -31,10 +39,17 @@ import { SectionsType } from '../sections-type'; selector: 'ds-submission-section-duplicates', templateUrl: './section-duplicates.component.html', changeDetection: ChangeDetectionStrategy.Default, + imports: [ + VarDirective, + NgIf, + AsyncPipe, + TranslateModule, + NgForOf, + ], + standalone: true, }) -@renderSectionFor(SectionsType.Duplicates) -export class SubmissionSectionDuplicatesComponent extends SectionModelComponent { +export class SubmissionSectionDuplicatesComponent extends SectionModelComponent implements OnInit { protected readonly Metadata = Metadata; /** * The Alert categories. diff --git a/src/app/submission/sections/form/section-form-operations.service.spec.ts b/src/app/submission/sections/form/section-form-operations.service.spec.ts index 2e0c5cccb5..cd169a76ac 100644 --- a/src/app/submission/sections/form/section-form-operations.service.spec.ts +++ b/src/app/submission/sections/form/section-form-operations.service.spec.ts @@ -13,6 +13,7 @@ import { TranslateModule, } from '@ngx-translate/core'; +import { APP_DATA_SERVICES_MAP } from '../../../../config/app-config.interface'; import { JsonPatchOperationPathCombiner } from '../../../core/json-patch/builder/json-patch-operation-path-combiner'; import { JsonPatchOperationsBuilder } from '../../../core/json-patch/builder/json-patch-operations-builder'; import { VocabularyEntry } from '../../../core/submission/vocabularies/models/vocabulary-entry.model'; @@ -79,6 +80,7 @@ describe('SectionFormOperationsService test suite', () => { providers: [ { provide: FormBuilderService, useValue: getMockFormBuilderService() }, { provide: JsonPatchOperationsBuilder, useValue: jsonPatchOpBuilder }, + { provide: APP_DATA_SERVICES_MAP, useValue: {} }, SectionFormOperationsService, ], }).compileComponents().then(); diff --git a/src/app/submission/sections/form/section-form-operations.service.ts b/src/app/submission/sections/form/section-form-operations.service.ts index f8c14cf96c..25c2ae303d 100644 --- a/src/app/submission/sections/form/section-form-operations.service.ts +++ b/src/app/submission/sections/form/section-form-operations.service.ts @@ -39,7 +39,7 @@ import { FormFieldPreviousValueObject } from '../../../shared/form/builder/model /** * The service handling all form section operations */ -@Injectable() +@Injectable({ providedIn: 'root' }) export class SectionFormOperationsService { /** @@ -92,7 +92,7 @@ export class SectionFormOperationsService { * @return number * the array index is part of array, zero otherwise */ - public getArrayIndexFromEvent(event: DynamicFormControlEvent | any): number { + public getArrayIndexFromEvent(event: any): number { let fieldIndex: number; if (isNotEmpty(event)) { @@ -110,7 +110,7 @@ export class SectionFormOperationsService { } else { // This is the case of a custom event which contains indexes information - fieldIndex = event.index as any; + fieldIndex = event?.index as any; } } diff --git a/src/app/submission/sections/form/section-form.component.html b/src/app/submission/sections/form/section-form.component.html index 675b550b57..cd7b45bb00 100644 --- a/src/app/submission/sections/form/section-form.component.html +++ b/src/app/submission/sections/form/section-form.component.html @@ -1,4 +1,4 @@ - + { let submissionServiceStub: SubmissionServiceStub; let notificationsServiceStub: NotificationsServiceStub; let formService: any = getMockFormService(); + let themeService = getMockThemeService(); let formOperationsService: any; let formBuilderService: any; @@ -177,13 +179,10 @@ describe('SubmissionSectionFormComponent test suite', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [ - BrowserModule, CommonModule, FormsModule, ReactiveFormsModule, TranslateModule.forRoot(), - ], - declarations: [ FormComponent, SubmissionSectionFormComponent, TestComponent, @@ -195,10 +194,13 @@ describe('SubmissionSectionFormComponent test suite', () => { { provide: SubmissionFormsConfigDataService, useValue: formConfigService }, { provide: NotificationsService, useClass: NotificationsServiceStub }, { provide: SectionsService, useValue: sectionsServiceStub }, + { provide: ThemeService, useValue: themeService }, { provide: SubmissionService, useClass: SubmissionServiceStub }, { provide: TranslateService, useValue: getMockTranslateService() }, - { provide: ObjectCacheService, useValue: { remove: () => {/*do nothing*/}, hasBySelfLinkObservable: () => observableOf(false), hasByHref$: () => observableOf(false) } }, - { provide: RequestService, useValue: { removeByHrefSubstring: () => {/*do nothing*/}, hasByHref$: () => observableOf(false) } }, + // eslint-disable-next-line @typescript-eslint/no-empty-function + { provide: ObjectCacheService, useValue: { remove: () => { }, hasBySelfLinkObservable: () => observableOf(false), hasByHref$: () => observableOf(false) } }, + // eslint-disable-next-line @typescript-eslint/no-empty-function + { provide: RequestService, useValue: { removeByHrefSubstring: () => { }, hasByHref$: () => observableOf(false) } }, { provide: 'collectionIdProvider', useValue: collectionId }, { provide: 'sectionDataProvider', useValue: Object.assign({}, sectionObject) }, { provide: 'submissionIdProvider', useValue: submissionId }, @@ -280,7 +282,7 @@ describe('SubmissionSectionFormComponent test suite', () => { expect(comp.sectionData.errorsToShow).toEqual([]); expect(comp.sectionData.data).toEqual(sectionData); expect(comp.isLoading).toBeFalsy(); - expect(comp.initForm).toHaveBeenCalledWith(sectionData); + expect(comp.initForm).toHaveBeenCalledWith(sectionData, [], []); expect(comp.subscriptions).toHaveBeenCalled(); }); @@ -288,7 +290,7 @@ describe('SubmissionSectionFormComponent test suite', () => { formBuilderService.modelFromConfiguration.and.returnValue(testFormModel); const sectionData = {}; - comp.initForm(sectionData); + comp.initForm(sectionData, [], []); expect(comp.formModel).toEqual(testFormModel); @@ -303,7 +305,7 @@ describe('SubmissionSectionFormComponent test suite', () => { path: '/sections/' + sectionObject.id, }; - comp.initForm(sectionData); + comp.initForm(sectionData, [], []); expect(comp.formModel).toBeUndefined(); expect(sectionsServiceStub.setSectionError).toHaveBeenCalledWith(submissionId, sectionObject.id, sectionError); @@ -462,7 +464,7 @@ describe('SubmissionSectionFormComponent test suite', () => { compAsAny.formData = {}; compAsAny.sectionMetadata = ['dc.title']; - comp.updateForm(sectionData, sectionError); + comp.updateForm({ data: sectionData, errorsToShow: sectionError } as any); expect(comp.isUpdating).toBeFalsy(); expect(comp.initForm).toHaveBeenCalled(); @@ -474,15 +476,19 @@ describe('SubmissionSectionFormComponent test suite', () => { it('should update form error properly', () => { spyOn(comp, 'initForm'); spyOn(comp, 'checksForErrors'); - const sectionData: any = { + const sectionData = { 'dc.title': [new FormFieldMetadataValueObject('test')], }; + const sectionState = { + data: sectionData, + errorsToShow: [{ path: '/test', message: 'test' }], + } as any; comp.sectionData.data = {}; comp.sectionData.errorsToShow = []; compAsAny.formData = sectionData; compAsAny.sectionMetadata = ['dc.title']; - comp.updateForm(sectionData, parsedSectionErrors); + comp.updateForm(sectionState); expect(comp.initForm).not.toHaveBeenCalled(); expect(comp.checksForErrors).toHaveBeenCalled(); @@ -493,8 +499,9 @@ describe('SubmissionSectionFormComponent test suite', () => { spyOn(comp, 'initForm'); spyOn(comp, 'checksForErrors'); const sectionData: any = {}; + const sectionErrors: any = [{ path: '/test', message: 'test' }]; - comp.updateForm(sectionData, parsedSectionErrors); + comp.updateForm({ data: sectionData, errorsToShow: sectionErrors } as any); expect(comp.initForm).not.toHaveBeenCalled(); expect(comp.checksForErrors).toHaveBeenCalled(); @@ -560,7 +567,7 @@ describe('SubmissionSectionFormComponent test suite', () => { const sectionState = { data: sectionData, errorsToShow: parsedSectionErrors, - }; + } as any; formService.getFormData.and.returnValue(observableOf(formData)); sectionsServiceStub.getSectionState.and.returnValue(observableOf(sectionState)); @@ -569,7 +576,7 @@ describe('SubmissionSectionFormComponent test suite', () => { expect(compAsAny.subs.length).toBe(2); expect(compAsAny.formData).toEqual(formData); - expect(comp.updateForm).toHaveBeenCalledWith(sectionState.data, sectionState.errorsToShow); + expect(comp.updateForm).toHaveBeenCalledWith(sectionState); }); @@ -651,7 +658,7 @@ describe('SubmissionSectionFormComponent test suite', () => { @Component({ selector: 'ds-test-cmp', template: ``, + standalone: true, + imports: [CommonModule, FormsModule, ReactiveFormsModule], }) -class TestComponent { - -} +class TestComponent {} diff --git a/src/app/submission/sections/form/section-form.component.ts b/src/app/submission/sections/form/section-form.component.ts index defc78d611..26285320b0 100644 --- a/src/app/submission/sections/form/section-form.component.ts +++ b/src/app/submission/sections/form/section-form.component.ts @@ -1,3 +1,4 @@ +import { NgIf } from '@angular/common'; import { ChangeDetectorRef, Component, @@ -55,6 +56,7 @@ import { FormBuilderService } from '../../../shared/form/builder/form-builder.se import { FormFieldPreviousValueObject } from '../../../shared/form/builder/models/form-field-previous-value-object'; import { FormComponent } from '../../../shared/form/form.component'; import { FormService } from '../../../shared/form/form.service'; +import { ThemedLoadingComponent } from '../../../shared/loading/themed-loading.component'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { difference } from '../../../shared/object.util'; import { followLink } from '../../../shared/utils/follow-link-config.model'; @@ -64,8 +66,6 @@ import { SubmissionService } from '../../submission.service'; import { SectionModelComponent } from '../models/section.model'; import { SectionDataObject } from '../models/section-data.model'; import { SectionsService } from '../sections.service'; -import { renderSectionFor } from '../sections-decorator'; -import { SectionsType } from '../sections-type'; import { SectionFormOperationsService } from './section-form-operations.service'; /** @@ -75,8 +75,13 @@ import { SectionFormOperationsService } from './section-form-operations.service' selector: 'ds-submission-section-form', styleUrls: ['./section-form.component.scss'], templateUrl: './section-form.component.html', + imports: [ + FormComponent, + ThemedLoadingComponent, + NgIf, + ], + standalone: true, }) -@renderSectionFor(SectionsType.SubmissionForm) export class SubmissionSectionFormComponent extends SectionModelComponent { /** @@ -219,7 +224,7 @@ export class SubmissionSectionFormComponent extends SectionModelComponent { this.submissionObject = submissionObject; this.isSectionReadonly = isSectionReadOnly; // Is the first loading so init form - this.initForm(sectionData); + this.initForm(sectionData, this.sectionData.errorsToShow, this.sectionData.serverValidationErrors); this.sectionData.data = sectionData; this.subscriptions(); this.isLoading = false; @@ -305,10 +310,10 @@ export class SubmissionSectionFormComponent extends SectionModelComponent { })?.fields?.[0]?.scope; switch (scope) { - case SubmissionScopeType.WorkspaceItem: { + case SubmissionScopeType.WorkspaceItem.valueOf(): { return (this.submissionObject as any).type === WorkspaceItem.type.value; } - case SubmissionScopeType.WorkflowItem: { + case SubmissionScopeType.WorkflowItem.valueOf(): { return (this.submissionObject as any).type === WorkflowItem.type.value; } default: { @@ -323,7 +328,7 @@ export class SubmissionSectionFormComponent extends SectionModelComponent { * @param sectionData * the section data retrieved from the server */ - initForm(sectionData: WorkspaceitemSectionFormObject): void { + initForm(sectionData: WorkspaceitemSectionFormObject, errorsToShow: SubmissionSectionError[], serverValidationErrors: SubmissionSectionError[]): void { try { this.formModel = this.formBuilderService.modelFromConfiguration( this.submissionId, @@ -334,9 +339,9 @@ export class SubmissionSectionFormComponent extends SectionModelComponent { this.isSectionReadonly, ); const sectionMetadata = this.sectionService.computeSectionConfiguredMetadata(this.formConfig); - this.sectionService.updateSectionData(this.submissionId, this.sectionData.id, sectionData, this.sectionData.errorsToShow, this.sectionData.serverValidationErrors, sectionMetadata); - } catch (e) { - const msg: string = this.translate.instant('error.submission.sections.init-form-error') + e.toString(); + this.sectionService.updateSectionData(this.submissionId, this.sectionData.id, sectionData, errorsToShow, serverValidationErrors, sectionMetadata); + } catch (e: unknown) { + const msg: string = this.translate.instant('error.submission.sections.init-form-error') + (e as Error).toString(); const sectionError: SubmissionSectionError = { message: msg, path: '/sections/' + this.sectionData.id, @@ -351,12 +356,13 @@ export class SubmissionSectionFormComponent extends SectionModelComponent { /** * Update form model * - * @param sectionData - * the section data retrieved from the server - * @param errors - * the section errors retrieved from the server + * @param sectionState + * the section state retrieved from the server */ - updateForm(sectionData: WorkspaceitemSectionFormObject, errors: SubmissionSectionError[]): void { + updateForm(sectionState: SubmissionSectionObject): void { + + const sectionData = sectionState.data as WorkspaceitemSectionFormObject; + const errors = sectionState.errorsToShow; if (isNotEmpty(sectionData) && !isEqual(sectionData, this.sectionData.data)) { this.sectionData.data = sectionData; @@ -364,7 +370,7 @@ export class SubmissionSectionFormComponent extends SectionModelComponent { this.isUpdating = true; this.formModel = null; this.cdr.detectChanges(); - this.initForm(sectionData); + this.initForm(sectionData, errors, sectionState.serverValidationErrors); this.checksForErrors(errors); this.isUpdating = false; this.cdr.detectChanges(); @@ -418,7 +424,7 @@ export class SubmissionSectionFormComponent extends SectionModelComponent { .subscribe((sectionState: SubmissionSectionObject) => { this.fieldsOnTheirWayToBeRemoved = new Map(); this.sectionMetadata = sectionState.metadata; - this.updateForm(sectionState.data as WorkspaceitemSectionFormObject, sectionState.errorsToShow); + this.updateForm(sectionState); }), ); } diff --git a/src/app/submission/sections/identifiers/section-identifiers.component.spec.ts b/src/app/submission/sections/identifiers/section-identifiers.component.spec.ts index 3184010278..8aa760bb3e 100644 --- a/src/app/submission/sections/identifiers/section-identifiers.component.spec.ts +++ b/src/app/submission/sections/identifiers/section-identifiers.component.spec.ts @@ -14,7 +14,6 @@ import { FormsModule, ReactiveFormsModule, } from '@angular/forms'; -import { BrowserModule } from '@angular/platform-browser'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { TranslateModule } from '@ngx-translate/core'; import { cold } from 'jasmine-marbles'; @@ -23,10 +22,12 @@ import { of as observableOf } from 'rxjs'; import { SubmissionFormsConfigDataService } from '../../../core/config/submission-forms-config-data.service'; import { CollectionDataService } from '../../../core/data/collection-data.service'; +import { ConfigurationDataService } from '../../../core/data/configuration-data.service'; import { JsonPatchOperationPathCombiner } from '../../../core/json-patch/builder/json-patch-operation-path-combiner'; import { JsonPatchOperationsBuilder } from '../../../core/json-patch/builder/json-patch-operations-builder'; import { PaginationService } from '../../../core/pagination/pagination.service'; import { Collection } from '../../../core/shared/collection.model'; +import { ConfigurationProperty } from '../../../core/shared/configuration-property.model'; import { Item } from '../../../core/shared/item.model'; import { License } from '../../../core/shared/license.model'; import { WorkspaceitemSectionIdentifiersObject } from '../../../core/submission/models/workspaceitem-section-identifiers.model'; @@ -135,6 +136,15 @@ describe('SubmissionSectionIdentifiersComponent test suite', () => { remove: jasmine.createSpy('remove'), }); + const configurationDataService = jasmine.createSpyObj('configurationDataService', { + findByPropertyName: createSuccessfulRemoteDataObject$(Object.assign(new ConfigurationProperty(), { + name: 'test', + values: [ + 'org.dspace.ctask.general.ProfileFormats = test', + ], + })), + }); + const licenseText = 'License text'; const mockCollection = Object.assign(new Collection(), { name: 'Community 1-Collection 1', @@ -152,15 +162,12 @@ describe('SubmissionSectionIdentifiersComponent test suite', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [ - BrowserModule, CommonModule, FormsModule, ReactiveFormsModule, NgxPaginationModule, NoopAnimationsModule, TranslateModule.forRoot(), - ], - declarations: [ SubmissionSectionIdentifiersComponent, TestComponent, ObjNgFor, @@ -179,6 +186,7 @@ describe('SubmissionSectionIdentifiersComponent test suite', () => { { provide: 'sectionDataProvider', useValue: sectionObject }, { provide: 'submissionIdProvider', useValue: submissionId }, { provide: PaginationService, useValue: paginationService }, + { provide: ConfigurationDataService, useValue: configurationDataService }, ChangeDetectorRef, FormBuilderService, SubmissionSectionIdentifiersComponent, @@ -266,6 +274,12 @@ describe('SubmissionSectionIdentifiersComponent test suite', () => { @Component({ selector: 'ds-test-cmp', template: ``, + standalone: true, + imports: [ + CommonModule, + FormsModule, + ReactiveFormsModule, + NgxPaginationModule], }) class TestComponent { diff --git a/src/app/submission/sections/identifiers/section-identifiers.component.ts b/src/app/submission/sections/identifiers/section-identifiers.component.ts index b32458c757..e297c795df 100644 --- a/src/app/submission/sections/identifiers/section-identifiers.component.ts +++ b/src/app/submission/sections/identifiers/section-identifiers.component.ts @@ -1,9 +1,18 @@ +import { + AsyncPipe, + NgForOf, + NgIf, +} from '@angular/common'; import { ChangeDetectionStrategy, Component, Inject, + OnInit, } from '@angular/core'; -import { TranslateService } from '@ngx-translate/core'; +import { + TranslateModule, + TranslateService, +} from '@ngx-translate/core'; import { Observable, of as observableOf, @@ -12,12 +21,11 @@ import { import { WorkspaceitemSectionIdentifiersObject } from '../../../core/submission/models/workspaceitem-section-identifiers.model'; import { AlertType } from '../../../shared/alert/alert-type'; +import { VarDirective } from '../../../shared/utils/var.directive'; import { SubmissionService } from '../../submission.service'; import { SectionModelComponent } from '../models/section.model'; import { SectionDataObject } from '../models/section-data.model'; import { SectionsService } from '../sections.service'; -import { renderSectionFor } from '../sections-decorator'; -import { SectionsType } from '../sections-type'; /** * This simple component displays DOI, handle and other identifiers that are already minted for the item in @@ -30,10 +38,17 @@ import { SectionsType } from '../sections-type'; selector: 'ds-submission-section-identifiers', templateUrl: './section-identifiers.component.html', changeDetection: ChangeDetectionStrategy.Default, + imports: [ + TranslateModule, + NgForOf, + NgIf, + AsyncPipe, + VarDirective, + ], + standalone: true, }) -@renderSectionFor(SectionsType.Identifiers) -export class SubmissionSectionIdentifiersComponent extends SectionModelComponent { +export class SubmissionSectionIdentifiersComponent extends SectionModelComponent implements OnInit { /** * The Alert categories. * @type {AlertType} @@ -62,7 +77,6 @@ export class SubmissionSectionIdentifiersComponent extends SectionModelComponent /** * Initialize instance variables. * - * @param {PaginationService} paginationService * @param {TranslateService} translate * @param {SectionsService} sectionService * @param {SubmissionService} submissionService @@ -79,7 +93,7 @@ export class SubmissionSectionIdentifiersComponent extends SectionModelComponent super(injectedCollectionId, injectedSectionData, injectedSubmissionId); } - ngOnInit() { + ngOnInit(): void { super.ngOnInit(); } diff --git a/src/app/submission/sections/license/section-license.component.spec.ts b/src/app/submission/sections/license/section-license.component.spec.ts index b0e51985fe..95b2e7f50a 100644 --- a/src/app/submission/sections/license/section-license.component.spec.ts +++ b/src/app/submission/sections/license/section-license.component.spec.ts @@ -14,15 +14,22 @@ import { FormsModule, ReactiveFormsModule, } from '@angular/forms'; -import { BrowserModule } from '@angular/platform-browser'; import { + DYNAMIC_FORM_CONTROL_MAP_FN, DynamicCheckboxModel, DynamicFormControlEvent, DynamicFormControlEventType, } from '@ng-dynamic-forms/core'; +import { provideMockStore } from '@ngrx/store/testing'; import { TranslateModule } from '@ngx-translate/core'; import { cold } from 'jasmine-marbles'; import { of as observableOf } from 'rxjs'; +import { DsDynamicTypeBindRelationService } from 'src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-type-bind-relation.service'; +import { + APP_CONFIG, + APP_DATA_SERVICES_MAP, +} from 'src/config/app-config.interface'; +import { environment } from 'src/environments/environment.test'; import { SubmissionFormsConfigDataService } from '../../../core/config/submission-forms-config-data.service'; import { CollectionDataService } from '../../../core/data/collection-data.service'; @@ -30,6 +37,9 @@ import { JsonPatchOperationPathCombiner } from '../../../core/json-patch/builder import { JsonPatchOperationsBuilder } from '../../../core/json-patch/builder/json-patch-operations-builder'; import { Collection } from '../../../core/shared/collection.model'; import { License } from '../../../core/shared/license.model'; +import { SubmissionObjectDataService } from '../../../core/submission/submission-object-data.service'; +import { XSRFService } from '../../../core/xsrf/xsrf.service'; +import { dsDynamicFormControlMapFn } from '../../../shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-map-fn'; import { FormBuilderService } from '../../../shared/form/builder/form-builder.service'; import { FormFieldMetadataValueObject } from '../../../shared/form/builder/models/form-field-metadata-value.model'; import { FormComponent } from '../../../shared/form/form.component'; @@ -40,9 +50,13 @@ import { mockLicenseParsedErrors, mockSubmissionCollectionId, mockSubmissionId, + mockSubmissionObject, } from '../../../shared/mocks/submission.mock'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; -import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; +import { + createSuccessfulRemoteDataObject, + createSuccessfulRemoteDataObject$, +} from '../../../shared/remote-data.utils'; import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub'; import { SectionsServiceStub } from '../../../shared/testing/sections-service.stub'; import { SubmissionServiceStub } from '../../../shared/testing/submission-service.stub'; @@ -54,6 +68,14 @@ import { SectionsService } from '../sections.service'; import { SectionsType } from '../sections-type'; import { SubmissionSectionLicenseComponent } from './section-license.component'; +function getMockDsDynamicTypeBindRelationService(): DsDynamicTypeBindRelationService { + return jasmine.createSpyObj('DsDynamicTypeBindRelationService', { + getRelatedFormModel: jasmine.createSpy('getRelatedFormModel'), + matchesCondition: jasmine.createSpy('matchesCondition'), + subscribeRelations: jasmine.createSpy('subscribeRelations'), + }); +} + const collectionId = mockSubmissionCollectionId; const licenseText = 'License text'; const mockCollection = Object.assign(new Collection(), { @@ -125,17 +147,22 @@ describe('SubmissionSectionLicenseComponent test suite', () => { findById: jasmine.createSpy('findById'), findByHref: jasmine.createSpy('findByHref'), }); - + const initialState: any = { + core: { + 'cache/object': {}, + 'cache/syncbuffer': {}, + 'cache/object-updates': {}, + 'data/request': {}, + 'index': {}, + }, + }; beforeEach(waitForAsync(() => { - TestBed.configureTestingModule({ + void TestBed.configureTestingModule({ imports: [ - BrowserModule, CommonModule, FormsModule, ReactiveFormsModule, TranslateModule.forRoot(), - ], - declarations: [ FormComponent, SubmissionSectionLicenseComponent, TestComponent, @@ -153,7 +180,19 @@ describe('SubmissionSectionLicenseComponent test suite', () => { { provide: 'sectionDataProvider', useValue: Object.assign({}, sectionObject) }, { provide: 'submissionIdProvider', useValue: submissionId }, ChangeDetectorRef, + provideMockStore({ initialState }), FormBuilderService, + { provide: DsDynamicTypeBindRelationService, useValue: getMockDsDynamicTypeBindRelationService() }, + { provide: APP_CONFIG, useValue: environment }, + { provide: APP_DATA_SERVICES_MAP, useValue: {} }, + { provide: DYNAMIC_FORM_CONTROL_MAP_FN, useValue: dsDynamicFormControlMapFn }, + { + provide: SubmissionObjectDataService, + useValue: { + findById: () => observableOf(createSuccessfulRemoteDataObject(mockSubmissionObject)), + }, + }, + { provide: XSRFService, useValue: {} }, SubmissionSectionLicenseComponent, ], schemas: [NO_ERRORS_SCHEMA], @@ -213,14 +252,13 @@ describe('SubmissionSectionLicenseComponent test suite', () => { mockCollectionDataService.findById.and.returnValue(createSuccessfulRemoteDataObject$(mockCollection)); sectionsServiceStub.getSectionErrors.and.returnValue(observableOf([])); sectionsServiceStub.isSectionReadOnly.and.returnValue(observableOf(false)); + spyOn(formBuilderService, 'findById').and.returnValue(new DynamicCheckboxModel({ id: 'granted' })); }); it('should init section properly', () => { - + fixture.detectChanges(); spyOn(compAsAny, 'getSectionStatus'); - comp.onSectionInit(); - const model = formBuilderService.findById('granted', comp.formModel); expect(compAsAny.subs.length).toBe(2); @@ -238,13 +276,8 @@ describe('SubmissionSectionLicenseComponent test suite', () => { acceptanceDate: Date.now(), granted: true, } as any; - - spyOn(compAsAny, 'getSectionStatus'); - - comp.onSectionInit(); - + fixture.detectChanges(); const model = formBuilderService.findById('granted', comp.formModel); - expect(compAsAny.subs.length).toBe(2); expect(comp.formModel).toBeDefined(); expect(model.value).toBeTruthy(); @@ -254,21 +287,21 @@ describe('SubmissionSectionLicenseComponent test suite', () => { })); }); - it('should have status true when checkbox is selected', () => { + it('should have status true when checkbox is selected', (done) => { fixture.detectChanges(); - const model = formBuilderService.findById('granted', comp.formModel); + const model = formBuilderService.findById('granted', comp.formModel); (model as DynamicCheckboxModel).value = true; compAsAny.getSectionStatus().subscribe((status) => { expect(status).toBeTruthy(); + done(); }); }); it('should have status false when checkbox is not selected', () => { fixture.detectChanges(); const model = formBuilderService.findById('granted', comp.formModel); - compAsAny.getSectionStatus().subscribe((status) => { expect(status).toBeFalsy(); }); @@ -286,7 +319,7 @@ describe('SubmissionSectionLicenseComponent test suite', () => { }); it('should set section errors properly', () => { - comp.onSectionInit(); + fixture.detectChanges(); const expectedErrors = mockLicenseParsedErrors.license; expect(sectionsServiceStub.checkSectionErrors).toHaveBeenCalled(); @@ -301,7 +334,7 @@ describe('SubmissionSectionLicenseComponent test suite', () => { granted: true, } as any; - comp.onSectionInit(); + fixture.detectChanges(); expect(sectionsServiceStub.dispatchRemoveSectionErrors).toHaveBeenCalled(); @@ -341,6 +374,14 @@ describe('SubmissionSectionLicenseComponent test suite', () => { @Component({ selector: 'ds-test-cmp', template: ``, + standalone: true, + imports: [ + SubmissionSectionLicenseComponent, + CommonModule, + FormsModule, + FormComponent, + ReactiveFormsModule, + ], }) class TestComponent { diff --git a/src/app/submission/sections/license/section-license.component.ts b/src/app/submission/sections/license/section-license.component.ts index 701bf75cb6..86a0455c30 100644 --- a/src/app/submission/sections/license/section-license.component.ts +++ b/src/app/submission/sections/license/section-license.component.ts @@ -1,4 +1,9 @@ import { + AsyncPipe, + NgIf, +} from '@angular/common'; +import { + AfterViewChecked, ChangeDetectorRef, Component, Inject, @@ -47,8 +52,6 @@ import { SectionFormOperationsService } from '../form/section-form-operations.se import { SectionModelComponent } from '../models/section.model'; import { SectionDataObject } from '../models/section-data.model'; import { SectionsService } from '../sections.service'; -import { renderSectionFor } from '../sections-decorator'; -import { SectionsType } from '../sections-type'; import { SECTION_LICENSE_FORM_LAYOUT, SECTION_LICENSE_FORM_MODEL, @@ -61,9 +64,15 @@ import { selector: 'ds-submission-section-license', styleUrls: ['./section-license.component.scss'], templateUrl: './section-license.component.html', + providers: [], + imports: [ + FormComponent, + NgIf, + AsyncPipe, + ], + standalone: true, }) -@renderSectionFor(SectionsType.License) -export class SubmissionSectionLicenseComponent extends SectionModelComponent { +export class SubmissionSectionLicenseComponent extends SectionModelComponent implements AfterViewChecked { /** * The form id @@ -153,7 +162,9 @@ export class SubmissionSectionLicenseComponent extends SectionModelComponent { const model = this.formBuilderService.findById('granted', this.formModel); // Translate checkbox label - model.label = this.translateService.instant(model.label); + if (model.label) { + model.label = this.translateService.instant(model.label); + } // Retrieve license accepted status (model as DynamicCheckboxModel).value = (this.sectionData.data as WorkspaceitemSectionLicenseObject).granted; @@ -204,11 +215,14 @@ export class SubmissionSectionLicenseComponent extends SectionModelComponent { // Remove any section's errors this.sectionService.dispatchRemoveSectionErrors(this.submissionId, this.sectionData.id); } - this.changeDetectorRef.detectChanges(); }), ); } + ngAfterViewChecked(): void { + this.changeDetectorRef.detectChanges(); + } + /** * Get section status * diff --git a/src/app/submission/sections/section-coar-notify/coar-notify-config-data.service.ts b/src/app/submission/sections/section-coar-notify/coar-notify-config-data.service.ts index cb5155dd32..74b2f0b97e 100644 --- a/src/app/submission/sections/section-coar-notify/coar-notify-config-data.service.ts +++ b/src/app/submission/sections/section-coar-notify/coar-notify-config-data.service.ts @@ -13,7 +13,6 @@ import { CreateData, CreateDataImpl, } from '../../../core/data/base/create-data'; -import { dataService } from '../../../core/data/base/data-service.decorator'; import { DeleteData, DeleteDataImpl, @@ -40,15 +39,13 @@ import { NoContent } from '../../../core/shared/NoContent.model'; import { URLCombiner } from '../../../core/url-combiner/url-combiner'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { FollowLinkConfig } from '../../../shared/utils/follow-link-config.model'; -import { SUBMISSION_COAR_NOTIFY_CONFIG } from './section-coar-notify-service.resource-type'; import { SubmissionCoarNotifyConfig } from './submission-coar-notify.config'; /** * A service responsible for fetching/sending data from/to the REST API on the CoarNotifyConfig endpoint */ -@Injectable() -@dataService(SUBMISSION_COAR_NOTIFY_CONFIG) +@Injectable({ providedIn: 'root' }) export class CoarNotifyConfigDataService extends IdentifiableDataService implements FindAllData, DeleteData, PatchData, CreateData { createData: CreateDataImpl; private findAllData: FindAllDataImpl; diff --git a/src/app/submission/sections/section-coar-notify/section-coar-notify.component.spec.ts b/src/app/submission/sections/section-coar-notify/section-coar-notify.component.spec.ts index f92ceff902..635b0a9f44 100644 --- a/src/app/submission/sections/section-coar-notify/section-coar-notify.component.spec.ts +++ b/src/app/submission/sections/section-coar-notify/section-coar-notify.component.spec.ts @@ -66,8 +66,7 @@ describe('SubmissionSectionCoarNotifyComponent', () => { }); await TestBed.configureTestingModule({ - imports: [TranslateModule.forRoot()], - declarations: [SubmissionSectionCoarNotifyComponent], + imports: [TranslateModule.forRoot(), SubmissionSectionCoarNotifyComponent], providers: [ { provide: LdnServicesService, useValue: ldnServicesService }, { provide: CoarNotifyConfigDataService, useValue: coarNotifyConfigDataService }, diff --git a/src/app/submission/sections/section-coar-notify/section-coar-notify.component.ts b/src/app/submission/sections/section-coar-notify/section-coar-notify.component.ts index 6415e21f31..c47e4c644d 100644 --- a/src/app/submission/sections/section-coar-notify/section-coar-notify.component.ts +++ b/src/app/submission/sections/section-coar-notify/section-coar-notify.component.ts @@ -1,9 +1,20 @@ +import { + AsyncPipe, + NgClass, + NgForOf, + NgIf, +} from '@angular/common'; import { ChangeDetectorRef, Component, Inject, } from '@angular/core'; -import { NgbDropdown } from '@ng-bootstrap/ng-bootstrap'; +import { + NgbDropdown, + NgbDropdownModule, +} from '@ng-bootstrap/ng-bootstrap'; +import { TranslateModule } from '@ngx-translate/core'; +import { InfiniteScrollModule } from 'ngx-infinite-scroll'; import { Observable, Subscription, @@ -36,8 +47,6 @@ import { SubmissionSectionError } from '../../objects/submission-section-error.m import { SectionModelComponent } from '../models/section.model'; import { SectionDataObject } from '../models/section-data.model'; import { SectionsService } from '../sections.service'; -import { renderSectionFor } from '../sections-decorator'; -import { SectionsType } from '../sections-type'; import { CoarNotifyConfigDataService } from './coar-notify-config-data.service'; import { LdnPattern } from './submission-coar-notify.config'; @@ -48,9 +57,18 @@ import { LdnPattern } from './submission-coar-notify.config'; selector: 'ds-submission-section-coar-notify', templateUrl: './section-coar-notify.component.html', styleUrls: ['./section-coar-notify.component.scss'], + standalone: true, + imports: [ + NgIf, + NgForOf, + AsyncPipe, + TranslateModule, + NgbDropdownModule, + NgClass, + InfiniteScrollModule, + ], providers: [NgbDropdown], }) -@renderSectionFor(SectionsType.CoarNotify) export class SubmissionSectionCoarNotifyComponent extends SectionModelComponent { hasSectionData = false; @@ -117,7 +135,7 @@ export class SubmissionSectionCoarNotifyComponent extends SectionModelComponent /** * Method called when section is initialized - * Retriev available NotifyConfigs + * Retrieve available NotifyConfigs */ setCoarNotifyConfig() { this.subs.push( diff --git a/src/app/submission/sections/section-coar-notify/submission-coar-notify-workspaceitem.model.ts b/src/app/submission/sections/section-coar-notify/submission-coar-notify-workspaceitem.model.ts index 3ca16a7f24..b80244c272 100644 --- a/src/app/submission/sections/section-coar-notify/submission-coar-notify-workspaceitem.model.ts +++ b/src/app/submission/sections/section-coar-notify/submission-coar-notify-workspaceitem.model.ts @@ -18,16 +18,16 @@ export class SubmissionCoarNotifyWorkspaceitemModel extends CacheableObject { @excludeFromEquals @autoserialize - endorsement?: number[]; + endorsement?: number[]; @deserializeAs('id') - review?: number[]; + review?: number[]; @autoserialize - ingest?: number[]; + ingest?: number[]; @deserialize - _links: { + _links: { self: { href: string; }; diff --git a/src/app/submission/sections/section-coar-notify/submission-coar-notify.config.ts b/src/app/submission/sections/section-coar-notify/submission-coar-notify.config.ts index 59a9125fed..04decc6459 100644 --- a/src/app/submission/sections/section-coar-notify/submission-coar-notify.config.ts +++ b/src/app/submission/sections/section-coar-notify/submission-coar-notify.config.ts @@ -23,19 +23,19 @@ export class SubmissionCoarNotifyConfig extends CacheableObject { @excludeFromEquals @autoserialize - type: ResourceType; + type: ResourceType; @autoserialize - id: string; + id: string; @deserializeAs('id') - uuid: string; + uuid: string; @autoserialize - patterns: LdnPattern[]; + patterns: LdnPattern[]; @deserialize - _links: { + _links: { self: { href: string; }; diff --git a/src/app/submission/sections/sections-decorator.ts b/src/app/submission/sections/sections-decorator.ts index 7e7840adfd..8e0df1cb23 100644 --- a/src/app/submission/sections/sections-decorator.ts +++ b/src/app/submission/sections/sections-decorator.ts @@ -1,7 +1,29 @@ - +import { SubmissionSectionAccessesComponent } from './accesses/section-accesses.component'; +import { SubmissionSectionCcLicensesComponent } from './cc-license/submission-section-cc-licenses.component'; +import { SubmissionSectionDuplicatesComponent } from './duplicates/section-duplicates.component'; +import { SubmissionSectionFormComponent } from './form/section-form.component'; +import { SubmissionSectionIdentifiersComponent } from './identifiers/section-identifiers.component'; +import { SubmissionSectionLicenseComponent } from './license/section-license.component'; +import { SubmissionSectionCoarNotifyComponent } from './section-coar-notify/section-coar-notify.component'; import { SectionsType } from './sections-type'; +import { SubmissionSectionSherpaPoliciesComponent } from './sherpa-policies/section-sherpa-policies.component'; +import { SubmissionSectionUploadComponent } from './upload/section-upload.component'; const submissionSectionsMap = new Map(); + +submissionSectionsMap.set(SectionsType.AccessesCondition, SubmissionSectionAccessesComponent); +submissionSectionsMap.set(SectionsType.License, SubmissionSectionLicenseComponent); +submissionSectionsMap.set(SectionsType.CcLicense, SubmissionSectionCcLicensesComponent); +submissionSectionsMap.set(SectionsType.SherpaPolicies, SubmissionSectionSherpaPoliciesComponent); +submissionSectionsMap.set(SectionsType.Upload, SubmissionSectionUploadComponent); +submissionSectionsMap.set(SectionsType.SubmissionForm, SubmissionSectionFormComponent); +submissionSectionsMap.set(SectionsType.Identifiers, SubmissionSectionIdentifiersComponent); +submissionSectionsMap.set(SectionsType.CoarNotify, SubmissionSectionCoarNotifyComponent); +submissionSectionsMap.set(SectionsType.Duplicates, SubmissionSectionDuplicatesComponent); + +/** + * @deprecated + */ export function renderSectionFor(sectionType: SectionsType) { return function decorator(objectElement: any) { if (!objectElement) { diff --git a/src/app/submission/sections/sections-type.ts b/src/app/submission/sections/sections-type.ts index 50d15427d2..60b4cedfdc 100644 --- a/src/app/submission/sections/sections-type.ts +++ b/src/app/submission/sections/sections-type.ts @@ -4,7 +4,6 @@ export enum SectionsType { Upload = 'upload', License = 'license', CcLicense = 'cclicense', - collection = 'collection', AccessesCondition = 'accessCondition', SherpaPolicies = 'sherpaPolicy', Identifiers = 'identifiers', diff --git a/src/app/submission/sections/sections.directive.ts b/src/app/submission/sections/sections.directive.ts index 9d32699770..7cf1921ff0 100644 --- a/src/app/submission/sections/sections.directive.ts +++ b/src/app/submission/sections/sections.directive.ts @@ -29,6 +29,7 @@ import { SectionsType } from './sections-type'; @Directive({ selector: '[dsSection]', exportAs: 'sectionRef', + standalone: true, }) export class SectionsDirective implements OnDestroy, OnInit { diff --git a/src/app/submission/sections/sections.service.spec.ts b/src/app/submission/sections/sections.service.spec.ts index d8644a0db8..0241564ab7 100644 --- a/src/app/submission/sections/sections.service.spec.ts +++ b/src/app/submission/sections/sections.service.spec.ts @@ -35,6 +35,7 @@ import { TranslateLoaderMock } from '../../shared/mocks/translate-loader.mock'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub'; import { SubmissionServiceStub } from '../../shared/testing/submission-service.stub'; +import { SectionScope } from '../objects/section-visibility.model'; import { DisableSectionAction, EnableSectionAction, @@ -265,46 +266,282 @@ describe('SectionsService test suite', () => { }); describe('isSectionReadOnly', () => { - it('should return an observable of true when it\'s a readonly section and scope is not workspace', () => { - store.select.and.returnValue(observableOf({ - visibility: { - main: null, - other: 'READONLY', - }, - })); + describe('when submission scope is workspace', () => { + describe('and section scope is workspace', () => { + it('should return an observable of true when visibility main is READONLY and visibility other is null', () => { + store.select.and.returnValue(observableOf({ + scope: SectionScope.Submission, + visibility: { + main: 'READONLY', + other: null, + }, + })); + + const expected = cold('(b|)', { + b: true, + }); + + expect(service.isSectionReadOnly(submissionId, sectionId, SubmissionScopeType.WorkspaceItem)).toBeObservable(expected); + }); + it('should return an observable of true when both visibility main and other are READONLY', () => { + store.select.and.returnValue(observableOf({ + scope: SectionScope.Submission, + visibility: { + main: 'READONLY', + other: 'READONLY', + }, + })); + + const expected = cold('(b|)', { + b: true, + }); + + expect(service.isSectionReadOnly(submissionId, sectionId, SubmissionScopeType.WorkspaceItem)).toBeObservable(expected); + }); + it('should return an observable of false when visibility main is null and visibility other is READONLY', () => { + store.select.and.returnValue(observableOf({ + scope: SectionScope.Submission, + visibility: { + main: null, + other: 'READONLY', + }, + })); + + const expected = cold('(b|)', { + b: false, + }); + + expect(service.isSectionReadOnly(submissionId, sectionId, SubmissionScopeType.WorkspaceItem)).toBeObservable(expected); + }); + it('should return an observable of false when visibility is null', () => { + store.select.and.returnValue(observableOf({ + scope: SectionScope.Submission, + visibility: null, + })); + + const expected = cold('(b|)', { + b: false, + }); + + expect(service.isSectionReadOnly(submissionId, sectionId, SubmissionScopeType.WorkspaceItem)).toBeObservable(expected); + }); - const expected = cold('(b|)', { - b: true, }); - expect(service.isSectionReadOnly(submissionId, sectionId, SubmissionScopeType.WorkflowItem)).toBeObservable(expected); + describe('and section scope is workflow', () => { + it('should return an observable of false when visibility main is READONLY and visibility other is null', () => { + store.select.and.returnValue(observableOf({ + scope: SectionScope.Workflow, + visibility: { + main: 'READONLY', + other: null, + }, + })); + + const expected = cold('(b|)', { + b: false, + }); + + expect(service.isSectionReadOnly(submissionId, sectionId, SubmissionScopeType.WorkspaceItem)).toBeObservable(expected); + }); + it('should return an observable of true when both visibility main and other are READONLY', () => { + store.select.and.returnValue(observableOf({ + scope: SectionScope.Workflow, + visibility: { + main: 'READONLY', + other: 'READONLY', + }, + })); + + const expected = cold('(b|)', { + b: true, + }); + + expect(service.isSectionReadOnly(submissionId, sectionId, SubmissionScopeType.WorkspaceItem)).toBeObservable(expected); + }); + it('should return an observable of true when visibility main is null and visibility other is READONLY', () => { + store.select.and.returnValue(observableOf({ + scope: SectionScope.Workflow, + visibility: { + main: null, + other: 'READONLY', + }, + })); + + const expected = cold('(b|)', { + b: true, + }); + + expect(service.isSectionReadOnly(submissionId, sectionId, SubmissionScopeType.WorkspaceItem)).toBeObservable(expected); + }); + it('should return an observable of false when visibility is null', () => { + store.select.and.returnValue(observableOf({ + scope: SectionScope.Workflow, + visibility: null, + })); + + const expected = cold('(b|)', { + b: false, + }); + + expect(service.isSectionReadOnly(submissionId, sectionId, SubmissionScopeType.WorkspaceItem)).toBeObservable(expected); + }); + + }); + + describe('and section scope is null', () => { + it('should return an observable of false', () => { + store.select.and.returnValue(observableOf({ + scope: null, + visibility: null, + })); + + const expected = cold('(b|)', { + b: false, + }); + + expect(service.isSectionReadOnly(submissionId, sectionId, SubmissionScopeType.WorkspaceItem)).toBeObservable(expected); + }); + }); }); - it('should return an observable of false when it\'s a readonly section and scope is workspace', () => { - store.select.and.returnValue(observableOf({ - visibility: { - main: null, - other: 'READONLY', - }, - })); + describe('when submission scope is workflow', () => { + describe('and section scope is workspace', () => { + it('should return an observable of false when visibility main is READONLY and visibility other is null', () => { + store.select.and.returnValue(observableOf({ + scope: SectionScope.Submission, + visibility: { + main: 'READONLY', + other: null, + }, + })); + + const expected = cold('(b|)', { + b: false, + }); + + expect(service.isSectionReadOnly(submissionId, sectionId, SubmissionScopeType.WorkflowItem)).toBeObservable(expected); + }); + it('should return an observable of true when both visibility main and other are READONLY', () => { + store.select.and.returnValue(observableOf({ + scope: SectionScope.Submission, + visibility: { + main: 'READONLY', + other: 'READONLY', + }, + })); + + const expected = cold('(b|)', { + b: true, + }); + + expect(service.isSectionReadOnly(submissionId, sectionId, SubmissionScopeType.WorkflowItem)).toBeObservable(expected); + }); + it('should return an observable of true when visibility main is null and visibility other is READONLY', () => { + store.select.and.returnValue(observableOf({ + scope: SectionScope.Submission, + visibility: { + main: null, + other: 'READONLY', + }, + })); + + const expected = cold('(b|)', { + b: true, + }); + + expect(service.isSectionReadOnly(submissionId, sectionId, SubmissionScopeType.WorkflowItem)).toBeObservable(expected); + }); + it('should return an observable of false when visibility is null', () => { + store.select.and.returnValue(observableOf({ + scope: SectionScope.Submission, + visibility: null, + })); + + const expected = cold('(b|)', { + b: false, + }); + + expect(service.isSectionReadOnly(submissionId, sectionId, SubmissionScopeType.WorkflowItem)).toBeObservable(expected); + }); - const expected = cold('(b|)', { - b: false, }); - expect(service.isSectionReadOnly(submissionId, sectionId, SubmissionScopeType.WorkspaceItem)).toBeObservable(expected); - }); + describe('and section scope is workflow', () => { + it('should return an observable of true when visibility main is READONLY and visibility other is null', () => { + store.select.and.returnValue(observableOf({ + scope: SectionScope.Workflow, + visibility: { + main: 'READONLY', + other: null, + }, + })); - it('should return an observable of false when it\'s not a readonly section', () => { - store.select.and.returnValue(observableOf({ - visibility: null, - })); + const expected = cold('(b|)', { + b: true, + }); + + expect(service.isSectionReadOnly(submissionId, sectionId, SubmissionScopeType.WorkflowItem)).toBeObservable(expected); + }); + it('should return an observable of true when both visibility main and other is READONLY', () => { + store.select.and.returnValue(observableOf({ + scope: SectionScope.Workflow, + visibility: { + main: 'READONLY', + other: 'READONLY', + }, + })); + + const expected = cold('(b|)', { + b: true, + }); + + expect(service.isSectionReadOnly(submissionId, sectionId, SubmissionScopeType.WorkflowItem)).toBeObservable(expected); + }); + it('should return an observable of false when visibility main is null and visibility other is READONLY', () => { + store.select.and.returnValue(observableOf({ + scope: SectionScope.Workflow, + visibility: { + main: null, + other: 'READONLY', + }, + })); + + const expected = cold('(b|)', { + b: false, + }); + + expect(service.isSectionReadOnly(submissionId, sectionId, SubmissionScopeType.WorkflowItem)).toBeObservable(expected); + }); + it('should return an observable of false when visibility is null', () => { + store.select.and.returnValue(observableOf({ + scope: SectionScope.Workflow, + visibility: null, + })); + + const expected = cold('(b|)', { + b: false, + }); + + expect(service.isSectionReadOnly(submissionId, sectionId, SubmissionScopeType.WorkflowItem)).toBeObservable(expected); + }); - const expected = cold('(b|)', { - b: false, }); - expect(service.isSectionReadOnly(submissionId, sectionId, SubmissionScopeType.WorkflowItem)).toBeObservable(expected); + describe('and section scope is null', () => { + it('should return an observable of false', () => { + store.select.and.returnValue(observableOf({ + scope: null, + visibility: null, + })); + + const expected = cold('(b|)', { + b: false, + }); + + expect(service.isSectionReadOnly(submissionId, sectionId, SubmissionScopeType.WorkflowItem)).toBeObservable(expected); + }); + }); }); }); diff --git a/src/app/submission/sections/sections.service.ts b/src/app/submission/sections/sections.service.ts index 58a4e90fbc..cec60a9cdf 100644 --- a/src/app/submission/sections/sections.service.ts +++ b/src/app/submission/sections/sections.service.ts @@ -36,6 +36,7 @@ import { FormClearErrorsAction } from '../../shared/form/form.actions'; import { FormError } from '../../shared/form/form.reducer'; import { FormService } from '../../shared/form/form.service'; import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { SectionScope } from '../objects/section-visibility.model'; import { DisableSectionAction, EnableSectionAction, @@ -63,7 +64,7 @@ import { SectionsType } from './sections-type'; /** * A service that provides methods used in submission process. */ -@Injectable() +@Injectable({ providedIn: 'root' }) export class SectionsService { /** @@ -124,7 +125,7 @@ export class SectionsService { }); }); - // Itereate over the previous error list + // Iterate over the previous error list prevErrors.forEach((error: SubmissionSectionError) => { const errorPaths: SectionErrorPath[] = parseSectionErrorPaths(error.path); @@ -347,10 +348,14 @@ export class SectionsService { return this.store.select(submissionSectionFromIdSelector(submissionId, sectionId)).pipe( filter((sectionObj) => hasValue(sectionObj)), map((sectionObj: SubmissionSectionObject) => { - return isNotEmpty(sectionObj.visibility) - && ((sectionObj.visibility.other === 'READONLY' && submissionScope !== SubmissionScopeType.WorkspaceItem) - || (sectionObj.visibility.main === 'READONLY' && submissionScope === SubmissionScopeType.WorkspaceItem) - ); + if (isEmpty(submissionScope) || isEmpty(sectionObj.visibility) || isEmpty(sectionObj.scope)) { + return false; + } + const convertedSubmissionScope: SectionScope = submissionScope.valueOf() === SubmissionScopeType.WorkspaceItem.valueOf() ? + SectionScope.Submission : SectionScope.Workflow; + const visibility = convertedSubmissionScope.valueOf() === sectionObj.scope.valueOf() ? + sectionObj.visibility.main : sectionObj.visibility.other; + return visibility === 'READONLY'; }), distinctUntilChanged()); } diff --git a/src/app/submission/sections/sherpa-policies/content-accordion/content-accordion.component.spec.ts b/src/app/submission/sections/sherpa-policies/content-accordion/content-accordion.component.spec.ts index 45822c5898..2abe8e02ec 100644 --- a/src/app/submission/sections/sherpa-policies/content-accordion/content-accordion.component.spec.ts +++ b/src/app/submission/sections/sherpa-policies/content-accordion/content-accordion.component.spec.ts @@ -29,8 +29,8 @@ describe('ContentAccordionComponent', () => { }, }), NgbCollapseModule, + ContentAccordionComponent, ], - declarations: [ContentAccordionComponent], }) .compileComponents(); }); diff --git a/src/app/submission/sections/sherpa-policies/content-accordion/content-accordion.component.ts b/src/app/submission/sections/sherpa-policies/content-accordion/content-accordion.component.ts index 05fdad693e..2fde4f37cd 100644 --- a/src/app/submission/sections/sherpa-policies/content-accordion/content-accordion.component.ts +++ b/src/app/submission/sections/sherpa-policies/content-accordion/content-accordion.component.ts @@ -1,7 +1,14 @@ +import { + NgForOf, + NgIf, + TitleCasePipe, +} from '@angular/common'; import { Component, Input, } from '@angular/core'; +import { NgbCollapseModule } from '@ng-bootstrap/ng-bootstrap'; +import { TranslateModule } from '@ngx-translate/core'; import { PermittedVersions } from '../../../../core/submission/models/sherpa-policies-details.model'; @@ -12,6 +19,14 @@ import { PermittedVersions } from '../../../../core/submission/models/sherpa-pol selector: 'ds-content-accordion', templateUrl: './content-accordion.component.html', styleUrls: ['./content-accordion.component.scss'], + imports: [ + NgForOf, + TranslateModule, + NgIf, + NgbCollapseModule, + TitleCasePipe, + ], + standalone: true, }) export class ContentAccordionComponent { /** diff --git a/src/app/submission/sections/sherpa-policies/metadata-information/metadata-information.component.spec.ts b/src/app/submission/sections/sherpa-policies/metadata-information/metadata-information.component.spec.ts index 3255e7d5de..5accbf3c5f 100644 --- a/src/app/submission/sections/sherpa-policies/metadata-information/metadata-information.component.spec.ts +++ b/src/app/submission/sections/sherpa-policies/metadata-information/metadata-information.component.spec.ts @@ -27,8 +27,8 @@ describe('MetadataInformationComponent', () => { useClass: TranslateLoaderMock, }, }), + MetadataInformationComponent, ], - declarations: [MetadataInformationComponent], }) .compileComponents(); }); diff --git a/src/app/submission/sections/sherpa-policies/metadata-information/metadata-information.component.ts b/src/app/submission/sections/sherpa-policies/metadata-information/metadata-information.component.ts index f96479c70c..4eb9567abb 100644 --- a/src/app/submission/sections/sherpa-policies/metadata-information/metadata-information.component.ts +++ b/src/app/submission/sections/sherpa-policies/metadata-information/metadata-information.component.ts @@ -1,17 +1,28 @@ +import { + DatePipe, + NgIf, +} from '@angular/common'; import { Component, Input, } from '@angular/core'; +import { TranslateModule } from '@ngx-translate/core'; import { Metadata } from '../../../../core/submission/models/sherpa-policies-details.model'; /** - * This component represents a section that contains the matadata informations. + * This component represents a section that contains the metadata information. */ @Component({ selector: 'ds-metadata-information', templateUrl: './metadata-information.component.html', styleUrls: ['./metadata-information.component.scss'], + imports: [ + NgIf, + TranslateModule, + DatePipe, + ], + standalone: true, }) export class MetadataInformationComponent { /** diff --git a/src/app/submission/sections/sherpa-policies/publication-information/publication-information.component.spec.ts b/src/app/submission/sections/sherpa-policies/publication-information/publication-information.component.spec.ts index 0815fedf50..fae68cd8a4 100644 --- a/src/app/submission/sections/sherpa-policies/publication-information/publication-information.component.spec.ts +++ b/src/app/submission/sections/sherpa-policies/publication-information/publication-information.component.spec.ts @@ -28,8 +28,8 @@ describe('PublicationInformationComponent', () => { useClass: TranslateLoaderMock, }, }), + PublicationInformationComponent, ], - declarations: [PublicationInformationComponent], }) .compileComponents(); }); diff --git a/src/app/submission/sections/sherpa-policies/publication-information/publication-information.component.ts b/src/app/submission/sections/sherpa-policies/publication-information/publication-information.component.ts index bb62e57564..8f256700a0 100644 --- a/src/app/submission/sections/sherpa-policies/publication-information/publication-information.component.ts +++ b/src/app/submission/sections/sherpa-policies/publication-information/publication-information.component.ts @@ -1,7 +1,12 @@ +import { + NgForOf, + NgIf, +} from '@angular/common'; import { Component, Input, } from '@angular/core'; +import { TranslateModule } from '@ngx-translate/core'; import { Journal } from '../../../../core/submission/models/sherpa-policies-details.model'; @@ -12,6 +17,12 @@ import { Journal } from '../../../../core/submission/models/sherpa-policies-deta selector: 'ds-publication-information', templateUrl: './publication-information.component.html', styleUrls: ['./publication-information.component.scss'], + imports: [ + NgIf, + TranslateModule, + NgForOf, + ], + standalone: true, }) export class PublicationInformationComponent { /** diff --git a/src/app/submission/sections/sherpa-policies/publisher-policy/publisher-policy.component.spec.ts b/src/app/submission/sections/sherpa-policies/publisher-policy/publisher-policy.component.spec.ts index 0780370d89..773c416f1c 100644 --- a/src/app/submission/sections/sherpa-policies/publisher-policy/publisher-policy.component.spec.ts +++ b/src/app/submission/sections/sherpa-policies/publisher-policy/publisher-policy.component.spec.ts @@ -11,6 +11,7 @@ import { import { SherpaDataResponse } from '../../../../shared/mocks/section-sherpa-policies.service.mock'; import { TranslateLoaderMock } from '../../../../shared/mocks/translate-loader.mock'; +import { ContentAccordionComponent } from '../content-accordion/content-accordion.component'; import { PublisherPolicyComponent } from './publisher-policy.component'; describe('PublisherPolicyComponent', () => { @@ -27,9 +28,14 @@ describe('PublisherPolicyComponent', () => { useClass: TranslateLoaderMock, }, }), + PublisherPolicyComponent, ], - declarations: [PublisherPolicyComponent], }) + .overrideComponent(PublisherPolicyComponent, { + remove: { + imports: [ContentAccordionComponent], + }, + }) .compileComponents(); }); diff --git a/src/app/submission/sections/sherpa-policies/publisher-policy/publisher-policy.component.ts b/src/app/submission/sections/sherpa-policies/publisher-policy/publisher-policy.component.ts index 58ca300865..8cbe2f6904 100644 --- a/src/app/submission/sections/sherpa-policies/publisher-policy/publisher-policy.component.ts +++ b/src/app/submission/sections/sherpa-policies/publisher-policy/publisher-policy.component.ts @@ -1,18 +1,33 @@ +import { + KeyValuePipe, + NgForOf, + NgIf, +} from '@angular/common'; import { Component, Input, } from '@angular/core'; +import { TranslateModule } from '@ngx-translate/core'; import { Policy } from '../../../../core/submission/models/sherpa-policies-details.model'; import { AlertType } from '../../../../shared/alert/alert-type'; +import { ContentAccordionComponent } from '../content-accordion/content-accordion.component'; /** - * This component represents a section that contains the publisher policy informations. + * This component represents a section that contains the publisher policy information. */ @Component({ selector: 'ds-publisher-policy', templateUrl: './publisher-policy.component.html', styleUrls: ['./publisher-policy.component.scss'], + imports: [ + ContentAccordionComponent, + TranslateModule, + KeyValuePipe, + NgForOf, + NgIf, + ], + standalone: true, }) export class PublisherPolicyComponent { diff --git a/src/app/submission/sections/sherpa-policies/section-sherpa-policies.component.spec.ts b/src/app/submission/sections/sherpa-policies/section-sherpa-policies.component.spec.ts index 11c0fdb97f..5882a277e6 100644 --- a/src/app/submission/sections/sherpa-policies/section-sherpa-policies.component.spec.ts +++ b/src/app/submission/sections/sherpa-policies/section-sherpa-policies.component.spec.ts @@ -17,15 +17,19 @@ import { } from '@ngx-translate/core'; import { of as observableOf } from 'rxjs'; +import { APP_DATA_SERVICES_MAP } from '../../../../config/app-config.interface'; import { AppState } from '../../../app.reducer'; import { JsonPatchOperationsBuilder } from '../../../core/json-patch/builder/json-patch-operations-builder'; +import { AlertComponent } from '../../../shared/alert/alert.component'; import { SherpaDataResponse } from '../../../shared/mocks/section-sherpa-policies.service.mock'; import { TranslateLoaderMock } from '../../../shared/mocks/translate-loader.mock'; -import { SharedModule } from '../../../shared/shared.module'; import { SectionsServiceStub } from '../../../shared/testing/sections-service.stub'; import { SubmissionServiceStub } from '../../../shared/testing/submission-service.stub'; import { SubmissionService } from '../../submission.service'; import { SectionsService } from '../sections.service'; +import { MetadataInformationComponent } from './metadata-information/metadata-information.component'; +import { PublicationInformationComponent } from './publication-information/publication-information.component'; +import { PublisherPolicyComponent } from './publisher-policy/publisher-policy.component'; import { SubmissionSectionSherpaPoliciesComponent } from './section-sherpa-policies.component'; describe('SubmissionSectionSherpaPoliciesComponent', () => { @@ -71,9 +75,8 @@ describe('SubmissionSectionSherpaPoliciesComponent', () => { }, }), NgbCollapseModule, - SharedModule, + SubmissionSectionSherpaPoliciesComponent, ], - declarations: [SubmissionSectionSherpaPoliciesComponent], providers: [ { provide: SectionsService, useValue: sectionsServiceStub }, { provide: JsonPatchOperationsBuilder, useValue: operationsBuilder }, @@ -81,8 +84,17 @@ describe('SubmissionSectionSherpaPoliciesComponent', () => { { provide: Store, useValue: storeStub }, { provide: 'sectionDataProvider', useValue: sectionData }, { provide: 'submissionIdProvider', useValue: '1508' }, + { provide: APP_DATA_SERVICES_MAP, useValue: {} }, ], }) + .overrideComponent(SubmissionSectionSherpaPoliciesComponent, { + remove: { imports: [ + MetadataInformationComponent, + AlertComponent, + PublisherPolicyComponent, + PublicationInformationComponent, + ] }, + }) .compileComponents(); }); diff --git a/src/app/submission/sections/sherpa-policies/section-sherpa-policies.component.ts b/src/app/submission/sections/sherpa-policies/section-sherpa-policies.component.ts index 1f37c887bf..86d55ce8d0 100644 --- a/src/app/submission/sections/sherpa-policies/section-sherpa-policies.component.ts +++ b/src/app/submission/sections/sherpa-policies/section-sherpa-policies.component.ts @@ -1,7 +1,14 @@ +import { + AsyncPipe, + NgForOf, + NgIf, +} from '@angular/common'; import { Component, Inject, } from '@angular/core'; +import { NgbCollapseModule } from '@ng-bootstrap/ng-bootstrap'; +import { TranslateModule } from '@ngx-translate/core'; import { BehaviorSubject, Observable, @@ -12,27 +19,42 @@ import { import { JsonPatchOperationPathCombiner } from '../../../core/json-patch/builder/json-patch-operation-path-combiner'; import { JsonPatchOperationsBuilder } from '../../../core/json-patch/builder/json-patch-operations-builder'; import { WorkspaceitemSectionSherpaPoliciesObject } from '../../../core/submission/models/workspaceitem-section-sherpa-policies.model'; +import { AlertComponent } from '../../../shared/alert/alert.component'; import { AlertType } from '../../../shared/alert/alert-type'; import { hasValue, isEmpty, } from '../../../shared/empty.util'; +import { VarDirective } from '../../../shared/utils/var.directive'; import { SubmissionService } from '../../submission.service'; import { SectionModelComponent } from '../models/section.model'; import { SectionDataObject } from '../models/section-data.model'; import { SectionsService } from '../sections.service'; -import { renderSectionFor } from '../sections-decorator'; -import { SectionsType } from '../sections-type'; +import { MetadataInformationComponent } from './metadata-information/metadata-information.component'; +import { PublicationInformationComponent } from './publication-information/publication-information.component'; +import { PublisherPolicyComponent } from './publisher-policy/publisher-policy.component'; /** - * This component represents a section for the sherpa policy informations structure. + * This component represents a section for the sherpa policy information structure. */ @Component({ selector: 'ds-section-sherpa-policies', templateUrl: './section-sherpa-policies.component.html', styleUrls: ['./section-sherpa-policies.component.scss'], + imports: [ + MetadataInformationComponent, + NgbCollapseModule, + AlertComponent, + TranslateModule, + PublisherPolicyComponent, + NgIf, + PublicationInformationComponent, + AsyncPipe, + VarDirective, + NgForOf, + ], + standalone: true, }) -@renderSectionFor(SectionsType.SherpaPolicies) export class SubmissionSectionSherpaPoliciesComponent extends SectionModelComponent { /** diff --git a/src/app/submission/sections/upload/accessConditions/submission-section-upload-access-conditions.component.ts b/src/app/submission/sections/upload/accessConditions/submission-section-upload-access-conditions.component.ts index de614b45e3..84eff58f72 100644 --- a/src/app/submission/sections/upload/accessConditions/submission-section-upload-access-conditions.component.ts +++ b/src/app/submission/sections/upload/accessConditions/submission-section-upload-access-conditions.component.ts @@ -1,15 +1,19 @@ +import { + NgForOf, + NgIf, +} from '@angular/common'; import { Component, Input, OnInit, } from '@angular/core'; -import { find } from 'rxjs/operators'; import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service'; import { RemoteData } from '../../../../core/data/remote-data'; import { GroupDataService } from '../../../../core/eperson/group-data.service'; import { Group } from '../../../../core/eperson/models/group.model'; import { ResourcePolicy } from '../../../../core/resource-policy/models/resource-policy.model'; +import { getFirstCompletedRemoteData } from '../../../../core/shared/operators'; import { isEmpty } from '../../../../shared/empty.util'; /** @@ -18,6 +22,11 @@ import { isEmpty } from '../../../../shared/empty.util'; @Component({ selector: 'ds-submission-section-upload-access-conditions', templateUrl: './submission-section-upload-access-conditions.component.html', + imports: [ + NgForOf, + NgIf, + ], + standalone: true, }) export class SubmissionSectionUploadAccessConditionsComponent implements OnInit { @@ -46,13 +55,15 @@ export class SubmissionSectionUploadAccessConditionsComponent implements OnInit this.accessConditions.forEach((accessCondition: ResourcePolicy) => { if (isEmpty(accessCondition.name)) { this.groupService.findByHref(accessCondition._links.group.href).pipe( - find((rd: RemoteData) => !rd.isResponsePending && rd.hasSucceeded)) - .subscribe((rd: RemoteData) => { + getFirstCompletedRemoteData(), + ).subscribe((rd: RemoteData) => { + if (rd.hasSucceeded) { const group: Group = rd.payload; const accessConditionEntry = Object.assign({}, accessCondition); accessConditionEntry.name = this.dsoNameService.getName(group); this.accessConditionsList.push(accessConditionEntry); - }); + } + }); } else { this.accessConditionsList.push(accessCondition); } diff --git a/src/app/submission/sections/upload/file/edit/section-upload-file-edit.component.spec.ts b/src/app/submission/sections/upload/file/edit/section-upload-file-edit.component.spec.ts index 4e4f487fbf..015ccd4ae4 100644 --- a/src/app/submission/sections/upload/file/edit/section-upload-file-edit.component.spec.ts +++ b/src/app/submission/sections/upload/file/edit/section-upload-file-edit.component.spec.ts @@ -7,7 +7,6 @@ import { import { ComponentFixture, fakeAsync, - inject, TestBed, tick, waitForAsync, @@ -16,7 +15,6 @@ import { FormsModule, ReactiveFormsModule, } from '@angular/forms'; -import { BrowserModule } from '@angular/platform-browser'; import { NgbActiveModal, NgbModal, @@ -27,13 +25,22 @@ import { DynamicFormGroupModel, DynamicSelectModel, } from '@ng-dynamic-forms/core'; +import { provideMockStore } from '@ngrx/store/testing'; import { TranslateModule } from '@ngx-translate/core'; +import { NgxMaskModule } from 'ngx-mask'; import { of } from 'rxjs'; +import { + APP_CONFIG, + APP_DATA_SERVICES_MAP, +} from '../../../../../../config/app-config.interface'; +import { environment } from '../../../../../../environments/environment.test'; import { JsonPatchOperationPathCombiner } from '../../../../../core/json-patch/builder/json-patch-operation-path-combiner'; import { JsonPatchOperationsBuilder } from '../../../../../core/json-patch/builder/json-patch-operations-builder'; import { SubmissionJsonPatchOperationsService } from '../../../../../core/submission/submission-json-patch-operations.service'; +import { XSRFService } from '../../../../../core/xsrf/xsrf.service'; import { dateToISOFormat } from '../../../../../shared/date.util'; +import { DsDynamicTypeBindRelationService } from '../../../../../shared/form/builder/ds-dynamic-form-ui/ds-dynamic-type-bind-relation.service'; import { DynamicCustomSwitchModel } from '../../../../../shared/form/builder/ds-dynamic-form-ui/models/custom-switch/custom-switch.model'; import { FormBuilderService } from '../../../../../shared/form/builder/form-builder.service'; import { FormFieldMetadataValueObject } from '../../../../../shared/form/builder/models/form-field-metadata-value.model'; @@ -54,10 +61,18 @@ import { SubmissionJsonPatchOperationsServiceStub } from '../../../../../shared/ import { SubmissionServiceStub } from '../../../../../shared/testing/submission-service.stub'; import { createTestComponent } from '../../../../../shared/testing/utils.test'; import { SubmissionService } from '../../../../submission.service'; -import { POLICY_DEFAULT_WITH_LIST } from '../../section-upload.component'; import { SectionUploadService } from '../../section-upload.service'; +import { POLICY_DEFAULT_WITH_LIST } from '../../section-upload-constants'; import { SubmissionSectionUploadFileEditComponent } from './section-upload-file-edit.component'; +function getMockDsDynamicTypeBindRelationService(): DsDynamicTypeBindRelationService { + return jasmine.createSpyObj('DsDynamicTypeBindRelationService', { + getRelatedFormModel: jasmine.createSpy('getRelatedFormModel'), + matchesCondition: jasmine.createSpy('matchesCondition'), + subscribeRelations: jasmine.createSpy('subscribeRelations'), + }); +} + const jsonPatchOpBuilder: any = jasmine.createSpyObj('jsonPatchOpBuilder', { add: jasmine.createSpy('add'), replace: jasmine.createSpy('replace'), @@ -66,6 +81,22 @@ const jsonPatchOpBuilder: any = jasmine.createSpyObj('jsonPatchOpBuilder', { const formMetadataMock = ['dc.title', 'dc.description']; +const initialState: any = { + core: { + 'bitstreamFormats': {}, + 'cache/object': {}, + 'cache/syncbuffer': {}, + 'cache/object-updates': {}, + 'data/request': {}, + 'history': {}, + 'index': {}, + 'auth': {}, + 'json/patch': {}, + 'metaTag': {}, + 'route': {}, + }, +}; + describe('SubmissionSectionUploadFileEditComponent test suite', () => { let comp: SubmissionSectionUploadFileEditComponent; @@ -93,19 +124,21 @@ describe('SubmissionSectionUploadFileEditComponent test suite', () => { let noAccessConditionsMock = Object.assign({}, mockFileFormData); delete noAccessConditionsMock.accessConditions; + const mockCdRef = Object.assign({ + detectChanges: () => undefined, + }); + beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [ - BrowserModule, CommonModule, FormsModule, ReactiveFormsModule, TranslateModule.forRoot(), - ], - declarations: [ FormComponent, SubmissionSectionUploadFileEditComponent, TestComponent, + NgxMaskModule.forRoot(), ], providers: [ { provide: FormService, useValue: getMockFormService() }, @@ -113,12 +146,16 @@ describe('SubmissionSectionUploadFileEditComponent test suite', () => { { provide: SubmissionJsonPatchOperationsService, useValue: submissionJsonPatchOperationsServiceStub }, { provide: JsonPatchOperationsBuilder, useValue: jsonPatchOpBuilder }, { provide: SectionUploadService, useValue: getMockSectionUploadService() }, + provideMockStore({ initialState }), FormBuilderService, - ChangeDetectorRef, + { provide: ChangeDetectorRef, useValue: mockCdRef }, SubmissionSectionUploadFileEditComponent, NgbModal, NgbActiveModal, - FormComponent, + { provide: DsDynamicTypeBindRelationService, useValue: getMockDsDynamicTypeBindRelationService() }, + { provide: APP_CONFIG, useValue: environment }, + { provide: APP_DATA_SERVICES_MAP, useValue: {} }, + { provide: XSRFService, useValue: {} }, ], schemas: [NO_ERRORS_SCHEMA], }).compileComponents().then(); @@ -150,11 +187,10 @@ describe('SubmissionSectionUploadFileEditComponent test suite', () => { testFixture.destroy(); }); - it('should create SubmissionSectionUploadFileEditComponent', inject([SubmissionSectionUploadFileEditComponent], (app: SubmissionSectionUploadFileEditComponent) => { - + it('should create SubmissionSectionUploadFileEditComponent', () => { + let app = TestBed.inject(SubmissionSectionUploadFileEditComponent); expect(app).toBeDefined(); - - })); + }); }); describe('', () => { @@ -194,7 +230,7 @@ describe('SubmissionSectionUploadFileEditComponent test suite', () => { const maxStartDate = { year: 2022, month: 1, day: 12 }; const maxEndDate = { year: 2019, month: 7, day: 12 }; - comp.ngOnInit(); + comp.formModel = compAsAny.buildFileEditForm(); const models = [DynamicCustomSwitchModel, DynamicFormGroupModel, DynamicFormArrayModel]; @@ -232,7 +268,7 @@ describe('SubmissionSectionUploadFileEditComponent test suite', () => { comp.fileData = fileData; comp.formId = 'testFileForm'; - comp.ngOnInit(); + comp.formModel = compAsAny.buildFileEditForm(); const model: DynamicSelectModel = formbuilderService.findById('name', comp.formModel, 0); const formGroup = formbuilderService.createFormGroup(comp.formModel); @@ -364,16 +400,14 @@ describe('SubmissionSectionUploadFileEditComponent test suite', () => { @Component({ selector: 'ds-test-cmp', template: ``, + standalone: true, + imports: [ + SubmissionSectionUploadFileEditComponent, + CommonModule, + FormsModule, + FormComponent, + ReactiveFormsModule, + ], }) class TestComponent { - - availableGroups; - availableAccessConditionOptions; - collectionId = mockSubmissionCollectionId; - collectionPolicyType; - fileIndexes = []; - fileList = []; - fileNames = []; - sectionId = 'upload'; - submissionId = mockSubmissionId; } diff --git a/src/app/submission/sections/upload/file/edit/section-upload-file-edit.component.ts b/src/app/submission/sections/upload/file/edit/section-upload-file-edit.component.ts index 8d5779c12c..24488d1adb 100644 --- a/src/app/submission/sections/upload/file/edit/section-upload-file-edit.component.ts +++ b/src/app/submission/sections/upload/file/edit/section-upload-file-edit.component.ts @@ -1,3 +1,4 @@ +import { NgIf } from '@angular/common'; import { ChangeDetectorRef, Component, @@ -20,20 +21,21 @@ import { } from '@ng-dynamic-forms/core'; import { DynamicDateControlValue } from '@ng-dynamic-forms/core/lib/model/dynamic-date-control.model'; import { DynamicFormControlCondition } from '@ng-dynamic-forms/core/lib/model/misc/dynamic-form-control-relation.model'; +import { TranslateModule } from '@ngx-translate/core'; import { Subscription } from 'rxjs'; import { filter, mergeMap, take, } from 'rxjs/operators'; +import { SubmissionObject } from 'src/app/core/submission/models/submission-object.model'; +import { WorkspaceitemSectionUploadObject } from 'src/app/core/submission/models/workspaceitem-section-upload.model'; import { DynamicCustomSwitchModel } from 'src/app/shared/form/builder/ds-dynamic-form-ui/models/custom-switch/custom-switch.model'; import { AccessConditionOption } from '../../../../../core/config/models/config-access-condition-option.model'; import { SubmissionFormsModel } from '../../../../../core/config/models/config-submission-forms.model'; import { JsonPatchOperationPathCombiner } from '../../../../../core/json-patch/builder/json-patch-operation-path-combiner'; import { JsonPatchOperationsBuilder } from '../../../../../core/json-patch/builder/json-patch-operations-builder'; -import { SubmissionObject } from '../../../../../core/submission/models/submission-object.model'; -import { WorkspaceitemSectionUploadObject } from '../../../../../core/submission/models/workspaceitem-section-upload.model'; import { WorkspaceitemSectionUploadFileObject } from '../../../../../core/submission/models/workspaceitem-section-upload-file.model'; import { SubmissionJsonPatchOperationsService } from '../../../../../core/submission/submission-json-patch-operations.service'; import { dateToISOFormat } from '../../../../../shared/date.util'; @@ -48,8 +50,8 @@ import { FormFieldModel } from '../../../../../shared/form/builder/models/form-f import { FormComponent } from '../../../../../shared/form/form.component'; import { FormService } from '../../../../../shared/form/form.service'; import { SubmissionService } from '../../../../submission.service'; -import { POLICY_DEFAULT_WITH_LIST } from '../../section-upload.component'; import { SectionUploadService } from '../../section-upload.service'; +import { POLICY_DEFAULT_WITH_LIST } from '../../section-upload-constants'; import { BITSTREAM_ACCESS_CONDITION_GROUP_CONFIG, BITSTREAM_ACCESS_CONDITION_GROUP_LAYOUT, @@ -74,6 +76,12 @@ import { selector: 'ds-submission-section-upload-file-edit', styleUrls: ['./section-upload-file-edit.component.scss'], templateUrl: './section-upload-file-edit.component.html', + imports: [ + FormComponent, + NgIf, + TranslateModule, + ], + standalone: true, }) export class SubmissionSectionUploadFileEditComponent implements OnInit, OnDestroy { diff --git a/src/app/submission/sections/upload/file/section-upload-file.component.html b/src/app/submission/sections/upload/file/section-upload-file.component.html index 436c5a3323..4698fdf68a 100644 --- a/src/app/submission/sections/upload/file/section-upload-file.component.html +++ b/src/app/submission/sections/upload/file/section-upload-file.component.html @@ -18,14 +18,14 @@
-

{{fileName}} ({{fileData?.sizeBytes | dsFileSize}})

+
- - +
-
diff --git a/src/app/submission/sections/upload/file/section-upload-file.component.spec.ts b/src/app/submission/sections/upload/file/section-upload-file.component.spec.ts index 83b3a33689..c1487b0559 100644 --- a/src/app/submission/sections/upload/file/section-upload-file.component.spec.ts +++ b/src/app/submission/sections/upload/file/section-upload-file.component.spec.ts @@ -1,4 +1,7 @@ -import { CommonModule } from '@angular/common'; +import { + AsyncPipe, + CommonModule, +} from '@angular/common'; import { ChangeDetectorRef, Component, @@ -6,14 +9,10 @@ import { } from '@angular/core'; import { ComponentFixture, - inject, TestBed, waitForAsync, } from '@angular/core/testing'; -import { - BrowserModule, - By, -} from '@angular/platform-browser'; +import { By } from '@angular/platform-browser'; import { NgbModal, NgbModule, @@ -24,10 +23,12 @@ import { of, } from 'rxjs'; +import { APP_DATA_SERVICES_MAP } from '../../../../../config/app-config.interface'; import { JsonPatchOperationPathCombiner } from '../../../../core/json-patch/builder/json-patch-operation-path-combiner'; import { JsonPatchOperationsBuilder } from '../../../../core/json-patch/builder/json-patch-operations-builder'; import { HALEndpointService } from '../../../../core/shared/hal-endpoint.service'; import { SubmissionJsonPatchOperationsService } from '../../../../core/submission/submission-json-patch-operations.service'; +import { ThemedFileDownloadLinkComponent } from '../../../../shared/file-download-link/themed-file-download-link.component'; import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service'; import { FormService } from '../../../../shared/form/form.service'; import { getMockFormService } from '../../../../shared/mocks/form-service.mock'; @@ -38,16 +39,20 @@ import { mockUploadConfigResponse, mockUploadFiles, } from '../../../../shared/mocks/submission.mock'; +import { getMockThemeService } from '../../../../shared/mocks/theme-service.mock'; import { HALEndpointServiceStub } from '../../../../shared/testing/hal-endpoint-service.stub'; import { SubmissionJsonPatchOperationsServiceStub } from '../../../../shared/testing/submission-json-patch-operations-service.stub'; import { SubmissionServiceStub } from '../../../../shared/testing/submission-service.stub'; import { createTestComponent } from '../../../../shared/testing/utils.test'; +import { ThemeService } from '../../../../shared/theme-support/theme.service'; import { FileSizePipe } from '../../../../shared/utils/file-size-pipe'; import { SubmissionService } from '../../../submission.service'; -import { POLICY_DEFAULT_WITH_LIST } from '../section-upload.component'; import { SectionUploadService } from '../section-upload.service'; +import { POLICY_DEFAULT_WITH_LIST } from '../section-upload-constants'; import { SubmissionSectionUploadFileEditComponent } from './edit/section-upload-file-edit.component'; import { SubmissionSectionUploadFileComponent } from './section-upload-file.component'; +import { ThemedSubmissionSectionUploadFileComponent } from './themed-section-upload-file.component'; +import { SubmissionSectionUploadFileViewComponent } from './view/section-upload-file-view.component'; const configMetadataFormMock = { rows: [{ @@ -93,12 +98,9 @@ describe('SubmissionSectionUploadFileComponent test suite', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [ - BrowserModule, CommonModule, NgbModule, TranslateModule.forRoot(), - ], - declarations: [ FileSizePipe, SubmissionSectionUploadFileComponent, TestComponent, @@ -110,6 +112,8 @@ describe('SubmissionSectionUploadFileComponent test suite', () => { { provide: SubmissionJsonPatchOperationsService, useValue: submissionJsonPatchOperationsServiceStub }, { provide: SubmissionService, useClass: SubmissionServiceStub }, { provide: SectionUploadService, useValue: getMockSectionUploadService() }, + { provide: ThemeService, useValue: getMockThemeService() }, + { provide: APP_DATA_SERVICES_MAP, useValue: {} }, ChangeDetectorRef, NgbModal, SubmissionSectionUploadFileComponent, @@ -117,7 +121,14 @@ describe('SubmissionSectionUploadFileComponent test suite', () => { FormBuilderService, ], schemas: [NO_ERRORS_SCHEMA], - }).compileComponents().then(); + }) + .overrideComponent(SubmissionSectionUploadFileComponent, { + remove: { imports: [ + SubmissionSectionUploadFileViewComponent, + ThemedFileDownloadLinkComponent, + ] }, + }) + .compileComponents().then(); })); describe('', () => { @@ -147,9 +158,10 @@ describe('SubmissionSectionUploadFileComponent test suite', () => { testFixture.destroy(); }); - it('should create SubmissionSectionUploadFileComponent', inject([SubmissionSectionUploadFileComponent], (app: SubmissionSectionUploadFileComponent) => { + it('should create SubmissionSectionUploadFileComponent', () => { + let app = TestBed.inject(SubmissionSectionUploadFileComponent); expect(app).toBeDefined(); - })); + }); }); describe('', () => { @@ -271,6 +283,12 @@ describe('SubmissionSectionUploadFileComponent test suite', () => { @Component({ selector: 'ds-test-cmp', template: ``, + standalone: true, + imports: [ + ThemedSubmissionSectionUploadFileComponent, + CommonModule, + AsyncPipe, + NgbModule], }) class TestComponent { diff --git a/src/app/submission/sections/upload/file/section-upload-file.component.ts b/src/app/submission/sections/upload/file/section-upload-file.component.ts index 8b86b1ec0f..a0519f6c85 100644 --- a/src/app/submission/sections/upload/file/section-upload-file.component.ts +++ b/src/app/submission/sections/upload/file/section-upload-file.component.ts @@ -1,3 +1,7 @@ +import { + AsyncPipe, + NgIf, +} from '@angular/common'; import { Component, Input, @@ -10,6 +14,7 @@ import { import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { NgbModalOptions } from '@ng-bootstrap/ng-bootstrap/modal/modal-config'; import { DynamicFormControlModel } from '@ng-dynamic-forms/core'; +import { TranslateModule } from '@ngx-translate/core'; import { BehaviorSubject, Observable, @@ -27,18 +32,30 @@ import { hasValue, isNotUndefined, } from '../../../../shared/empty.util'; +import { ThemedFileDownloadLinkComponent } from '../../../../shared/file-download-link/themed-file-download-link.component'; import { FormService } from '../../../../shared/form/form.service'; +import { FileSizePipe } from '../../../../shared/utils/file-size-pipe'; import { SubmissionService } from '../../../submission.service'; import { SectionUploadService } from '../section-upload.service'; import { SubmissionSectionUploadFileEditComponent } from './edit/section-upload-file-edit.component'; +import { SubmissionSectionUploadFileViewComponent } from './view/section-upload-file-view.component'; /** * This component represents a single bitstream contained in the submission */ @Component({ - selector: 'ds-submission-upload-section-file', + selector: 'ds-base-submission-upload-section-file', styleUrls: ['./section-upload-file.component.scss'], templateUrl: './section-upload-file.component.html', + imports: [ + TranslateModule, + SubmissionSectionUploadFileViewComponent, + NgIf, + AsyncPipe, + ThemedFileDownloadLinkComponent, + FileSizePipe, + ], + standalone: true, }) export class SubmissionSectionUploadFileComponent implements OnChanges, OnInit, OnDestroy { /** diff --git a/src/app/submission/sections/upload/file/themed-section-upload-file.component.ts b/src/app/submission/sections/upload/file/themed-section-upload-file.component.ts index 2280013147..39de40d85c 100644 --- a/src/app/submission/sections/upload/file/themed-section-upload-file.component.ts +++ b/src/app/submission/sections/upload/file/themed-section-upload-file.component.ts @@ -8,9 +8,11 @@ import { ThemedComponent } from 'src/app/shared/theme-support/themed.component'; import { SubmissionSectionUploadFileComponent } from './section-upload-file.component'; @Component({ - selector: 'ds-themed-submission-upload-section-file', + selector: 'ds-submission-upload-section-file', styleUrls: [], templateUrl: '../../../../shared/theme-support/themed.component.html', + standalone: true, + imports: [SubmissionSectionUploadFileComponent], }) export class ThemedSubmissionSectionUploadFileComponent extends ThemedComponent { diff --git a/src/app/submission/sections/upload/file/view/section-upload-file-view.component.html b/src/app/submission/sections/upload/file/view/section-upload-file-view.component.html index cc12b5dea6..dc72fbdad0 100644 --- a/src/app/submission/sections/upload/file/view/section-upload-file-view.component.html +++ b/src/app/submission/sections/upload/file/view/section-upload-file-view.component.html @@ -2,9 +2,10 @@ -
+

{{entry.value}} -

+ ({{fileData?.sizeBytes | dsFileSize}}) +
diff --git a/src/app/submission/sections/upload/file/view/section-upload-file-view.component.spec.ts b/src/app/submission/sections/upload/file/view/section-upload-file-view.component.spec.ts index cfbeb4c5ae..1ff35abc48 100644 --- a/src/app/submission/sections/upload/file/view/section-upload-file-view.component.spec.ts +++ b/src/app/submission/sections/upload/file/view/section-upload-file-view.component.spec.ts @@ -1,4 +1,5 @@ import { + ChangeDetectionStrategy, Component, NO_ERRORS_SCHEMA, } from '@angular/core'; @@ -15,6 +16,7 @@ import { FormComponent } from '../../../../../shared/form/form.component'; import { mockUploadFiles } from '../../../../../shared/mocks/submission.mock'; import { createTestComponent } from '../../../../../shared/testing/utils.test'; import { TruncatePipe } from '../../../../../shared/utils/truncate.pipe'; +import { SubmissionSectionUploadAccessConditionsComponent } from '../../accessConditions/submission-section-upload-access-conditions.component'; import { SubmissionSectionUploadFileViewComponent } from './section-upload-file-view.component'; describe('SubmissionSectionUploadFileViewComponent test suite', () => { @@ -25,12 +27,10 @@ describe('SubmissionSectionUploadFileViewComponent test suite', () => { const fileData: any = mockUploadFiles[0]; - beforeEach(waitForAsync(() => { - TestBed.configureTestingModule({ + beforeEach(waitForAsync(async () => { + await TestBed.configureTestingModule({ imports: [ TranslateModule.forRoot(), - ], - declarations: [ TruncatePipe, FormComponent, SubmissionSectionUploadFileViewComponent, @@ -40,7 +40,16 @@ describe('SubmissionSectionUploadFileViewComponent test suite', () => { SubmissionSectionUploadFileViewComponent, ], schemas: [NO_ERRORS_SCHEMA], - }).compileComponents().then(); + }) + .overrideComponent(SubmissionSectionUploadFileViewComponent, { + remove: { + imports: [SubmissionSectionUploadAccessConditionsComponent], + }, + add: { + changeDetection: ChangeDetectionStrategy.Default, + }, + }) + .compileComponents().then(); })); describe('', () => { @@ -100,6 +109,7 @@ describe('SubmissionSectionUploadFileViewComponent test suite', () => { @Component({ selector: 'ds-test-cmp', template: ``, + standalone: true, }) class TestComponent { diff --git a/src/app/submission/sections/upload/file/view/section-upload-file-view.component.ts b/src/app/submission/sections/upload/file/view/section-upload-file-view.component.ts index 5aa2af2e2f..f065fc9e19 100644 --- a/src/app/submission/sections/upload/file/view/section-upload-file-view.component.ts +++ b/src/app/submission/sections/upload/file/view/section-upload-file-view.component.ts @@ -1,8 +1,13 @@ +import { + NgForOf, + NgIf, +} from '@angular/common'; import { Component, Input, OnInit, } from '@angular/core'; +import { TranslateModule } from '@ngx-translate/core'; import { MetadataMap, @@ -11,6 +16,9 @@ import { import { Metadata } from '../../../../../core/shared/metadata.utils'; import { WorkspaceitemSectionUploadFileObject } from '../../../../../core/submission/models/workspaceitem-section-upload-file.model'; import { isNotEmpty } from '../../../../../shared/empty.util'; +import { FileSizePipe } from '../../../../../shared/utils/file-size-pipe'; +import { TruncatePipe } from '../../../../../shared/utils/truncate.pipe'; +import { SubmissionSectionUploadAccessConditionsComponent } from '../../accessConditions/submission-section-upload-access-conditions.component'; /** * This component allow to show bitstream's metadata @@ -18,6 +26,15 @@ import { isNotEmpty } from '../../../../../shared/empty.util'; @Component({ selector: 'ds-submission-section-upload-file-view', templateUrl: './section-upload-file-view.component.html', + imports: [ + SubmissionSectionUploadAccessConditionsComponent, + TranslateModule, + TruncatePipe, + NgIf, + NgForOf, + FileSizePipe, + ], + standalone: true, }) export class SubmissionSectionUploadFileViewComponent implements OnInit { diff --git a/src/app/submission/sections/upload/section-upload-constants.ts b/src/app/submission/sections/upload/section-upload-constants.ts new file mode 100644 index 0000000000..26f35a094d --- /dev/null +++ b/src/app/submission/sections/upload/section-upload-constants.ts @@ -0,0 +1,2 @@ +export const POLICY_DEFAULT_NO_LIST = 1; // Banner1 +export const POLICY_DEFAULT_WITH_LIST = 2; // Banner2 diff --git a/src/app/submission/sections/upload/section-upload.component.html b/src/app/submission/sections/upload/section-upload.component.html index 41e912e613..9d916a4f98 100644 --- a/src/app/submission/sections/upload/section-upload.component.html +++ b/src/app/submission/sections/upload/section-upload.component.html @@ -29,7 +29,7 @@
- + [submissionId]="submissionId">

@@ -51,7 +51,7 @@
-

{{'submission.sections.upload.no-file-uploaded' | translate}}

+
{{'submission.sections.upload.no-file-uploaded' | translate}}
diff --git a/src/app/submission/sections/upload/section-upload.component.spec.ts b/src/app/submission/sections/upload/section-upload.component.spec.ts index 59179166a3..61db6c6885 100644 --- a/src/app/submission/sections/upload/section-upload.component.spec.ts +++ b/src/app/submission/sections/upload/section-upload.component.spec.ts @@ -10,11 +10,11 @@ import { TestBed, waitForAsync, } from '@angular/core/testing'; -import { BrowserModule } from '@angular/platform-browser'; import { TranslateModule } from '@ngx-translate/core'; import { cold } from 'jasmine-marbles'; import { of as observableOf } from 'rxjs'; +import { APP_DATA_SERVICES_MAP } from '../../../../config/app-config.interface'; import { SubmissionUploadsModel } from '../../../core/config/models/config-submission-uploads.model'; import { SubmissionFormsConfigDataService } from '../../../core/config/submission-forms-config-data.service'; import { SubmissionUploadsConfigDataService } from '../../../core/config/submission-uploads-config-data.service'; @@ -26,6 +26,7 @@ import { ResourcePolicy } from '../../../core/resource-policy/models/resource-po import { ResourcePolicyDataService } from '../../../core/resource-policy/resource-policy-data.service'; import { Collection } from '../../../core/shared/collection.model'; import { PageInfo } from '../../../core/shared/page-info.model'; +import { AlertComponent } from '../../../shared/alert/alert.component'; import { getMockSectionUploadService } from '../../../shared/mocks/section-upload.service.mock'; import { mockGroup, @@ -37,10 +38,12 @@ import { mockUploadFiles, mockUploadFilesData, } from '../../../shared/mocks/submission.mock'; +import { getMockThemeService } from '../../../shared/mocks/theme-service.mock'; import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; import { SectionsServiceStub } from '../../../shared/testing/sections-service.stub'; import { SubmissionServiceStub } from '../../../shared/testing/submission-service.stub'; import { createTestComponent } from '../../../shared/testing/utils.test'; +import { ThemeService } from '../../../shared/theme-support/theme.service'; import { SubmissionObjectState } from '../../objects/submission-objects.reducer'; import { SubmissionService } from '../../submission.service'; import { SectionDataObject } from '../models/section-data.model'; @@ -174,11 +177,8 @@ describe('SubmissionSectionUploadComponent test suite', () => { TestBed.configureTestingModule({ imports: [ - BrowserModule, CommonModule, TranslateModule.forRoot(), - ], - declarations: [ SubmissionSectionUploadComponent, TestComponent, ], @@ -192,11 +192,19 @@ describe('SubmissionSectionUploadComponent test suite', () => { { provide: SectionUploadService, useValue: bitstreamService }, { provide: 'sectionDataProvider', useValue: sectionObject }, { provide: 'submissionIdProvider', useValue: submissionId }, + { provide: ThemeService, useValue: getMockThemeService() }, + { provide: APP_DATA_SERVICES_MAP, useValue: {} }, ChangeDetectorRef, SubmissionSectionUploadComponent, ], schemas: [NO_ERRORS_SCHEMA], - }).compileComponents().then(); + }) + .overrideComponent(SubmissionSectionUploadComponent, { + remove: { + imports: [AlertComponent], + }, + }) + .compileComponents().then(); })); describe('', () => { @@ -373,6 +381,9 @@ describe('SubmissionSectionUploadComponent test suite', () => { @Component({ selector: 'ds-test-cmp', template: ``, + standalone: true, + imports: [ + CommonModule], }) class TestComponent { diff --git a/src/app/submission/sections/upload/section-upload.component.ts b/src/app/submission/sections/upload/section-upload.component.ts index 2afd059bfc..58008c9dfb 100644 --- a/src/app/submission/sections/upload/section-upload.component.ts +++ b/src/app/submission/sections/upload/section-upload.component.ts @@ -1,8 +1,14 @@ +import { + AsyncPipe, + NgForOf, + NgIf, +} from '@angular/common'; import { ChangeDetectorRef, Component, Inject, } from '@angular/core'; +import { TranslateModule } from '@ngx-translate/core'; import { BehaviorSubject, combineLatest, @@ -32,6 +38,7 @@ import { Group } from '../../../core/eperson/models/group.model'; import { ResourcePolicyDataService } from '../../../core/resource-policy/resource-policy-data.service'; import { Collection } from '../../../core/shared/collection.model'; import { getFirstSucceededRemoteData } from '../../../core/shared/operators'; +import { AlertComponent } from '../../../shared/alert/alert.component'; import { AlertType } from '../../../shared/alert/alert-type'; import { hasValue, @@ -45,8 +52,8 @@ import { SubmissionService } from '../../submission.service'; import { SectionModelComponent } from '../models/section.model'; import { SectionDataObject } from '../models/section-data.model'; import { SectionsService } from '../sections.service'; -import { renderSectionFor } from '../sections-decorator'; -import { SectionsType } from '../sections-type'; +import { SubmissionSectionUploadAccessConditionsComponent } from './accessConditions/submission-section-upload-access-conditions.component'; +import { ThemedSubmissionSectionUploadFileComponent } from './file/themed-section-upload-file.component'; import { SectionUploadService } from './section-upload.service'; export const POLICY_DEFAULT_NO_LIST = 1; // Banner1 @@ -64,8 +71,17 @@ export interface AccessConditionGroupsMapEntry { selector: 'ds-submission-section-upload', styleUrls: ['./section-upload.component.scss'], templateUrl: './section-upload.component.html', + imports: [ + ThemedSubmissionSectionUploadFileComponent, + SubmissionSectionUploadAccessConditionsComponent, + NgIf, + AlertComponent, + TranslateModule, + NgForOf, + AsyncPipe, + ], + standalone: true, }) -@renderSectionFor(SectionsType.Upload) export class SubmissionSectionUploadComponent extends SectionModelComponent { /** @@ -211,20 +227,21 @@ export class SubmissionSectionUploadComponent extends SectionModelComponent { this.changeDetectorRef.detectChanges(); }), - // retrieve submission's bitstream data from state - combineLatest([this.configMetadataForm$, - this.bitstreamService.getUploadedFilesData(this.submissionId, this.sectionData.id)]).pipe( - filter(([configMetadataForm, { files }]: [SubmissionFormsModel, WorkspaceitemSectionUploadObject]) => { - return isNotEmpty(configMetadataForm) && isNotEmpty(files); + combineLatest([ + this.configMetadataForm$, + this.bitstreamService.getUploadedFilesData(this.submissionId, this.sectionData.id), + ]).pipe( + filter(([configMetadataForm, sectionUploadObject]: [SubmissionFormsModel, WorkspaceitemSectionUploadObject]) => { + return isNotEmpty(configMetadataForm) && isNotEmpty(sectionUploadObject); }), - distinctUntilChanged()) - .subscribe(([configMetadataForm, { primary, files }]: [SubmissionFormsModel, WorkspaceitemSectionUploadObject]) => { - this.primaryBitstreamUUID = primary; - this.fileList = files; - this.fileNames = Array.from(files, file => this.getFileName(configMetadataForm, file)); - }, - ), + distinctUntilChanged(), + ).subscribe(([configMetadataForm, { primary, files }]: [SubmissionFormsModel, WorkspaceitemSectionUploadObject]) => { + this.primaryBitstreamUUID = primary; + this.fileList = files; + this.fileNames = Array.from(files, file => this.getFileName(configMetadataForm, file)); + this.changeDetectorRef.detectChanges(); + }), ); } diff --git a/src/app/submission/sections/upload/section-upload.service.ts b/src/app/submission/sections/upload/section-upload.service.ts index 152d4e174a..b4abe366c0 100644 --- a/src/app/submission/sections/upload/section-upload.service.ts +++ b/src/app/submission/sections/upload/section-upload.service.ts @@ -28,7 +28,7 @@ import { SubmissionState } from '../../submission.reducers'; /** * A service that provides methods to handle submission's bitstream state. */ -@Injectable() +@Injectable({ providedIn: 'root' }) export class SectionUploadService { /** diff --git a/src/app/submission/submission.module.ts b/src/app/submission/submission.module.ts deleted file mode 100644 index 88c81bdee5..0000000000 --- a/src/app/submission/submission.module.ts +++ /dev/null @@ -1,164 +0,0 @@ -import { - CommonModule, - NgOptimizedImage, -} from '@angular/common'; -import { NgModule } from '@angular/core'; -import { - NgbAccordionModule, - NgbCollapseModule, - NgbModalModule, -} from '@ng-bootstrap/ng-bootstrap'; -import { EffectsModule } from '@ngrx/effects'; -import { - Action, - StoreConfig, - StoreModule, -} from '@ngrx/store'; - -import { LdnServicesService } from '../admin/admin-ldn-services/ldn-services-data/ldn-services-data.service'; -import { storeModuleConfig } from '../app.reducer'; -import { SubmissionAccessesConfigDataService } from '../core/config/submission-accesses-config-data.service'; -import { SubmissionUploadsConfigDataService } from '../core/config/submission-uploads-config-data.service'; -import { CoreModule } from '../core/core.module'; -import { JournalEntitiesModule } from '../entity-groups/journal-entities/journal-entities.module'; -import { ResearchEntitiesModule } from '../entity-groups/research-entities/research-entities.module'; -import { FormModule } from '../shared/form/form.module'; -import { SharedModule } from '../shared/shared.module'; -import { UploadModule } from '../shared/upload/upload.module'; -import { SubmissionEditComponent } from './edit/submission-edit.component'; -import { ThemedSubmissionEditComponent } from './edit/themed-submission-edit.component'; -import { SubmissionFormCollectionComponent } from './form/collection/submission-form-collection.component'; -import { SubmissionFormFooterComponent } from './form/footer/submission-form-footer.component'; -import { SubmissionFormSectionAddComponent } from './form/section-add/submission-form-section-add.component'; -import { SubmissionFormComponent } from './form/submission-form.component'; -import { SubmissionUploadFilesComponent } from './form/submission-upload-files/submission-upload-files.component'; -import { SubmissionImportExternalCollectionComponent } from './import-external/import-external-collection/submission-import-external-collection.component'; -import { SubmissionImportExternalPreviewComponent } from './import-external/import-external-preview/submission-import-external-preview.component'; -import { SubmissionImportExternalSearchbarComponent } from './import-external/import-external-searchbar/submission-import-external-searchbar.component'; -import { SubmissionImportExternalComponent } from './import-external/submission-import-external.component'; -import { ThemedSubmissionImportExternalComponent } from './import-external/themed-submission-import-external.component'; -import { SubmissionSectionAccessesComponent } from './sections/accesses/section-accesses.component'; -import { SectionAccessesService } from './sections/accesses/section-accesses.service'; -import { SubmissionSectionCcLicensesComponent } from './sections/cc-license/submission-section-cc-licenses.component'; -import { SubmissionSectionContainerComponent } from './sections/container/section-container.component'; -import { SubmissionSectionDuplicatesComponent } from './sections/duplicates/section-duplicates.component'; -import { SubmissionSectionFormComponent } from './sections/form/section-form.component'; -import { SectionFormOperationsService } from './sections/form/section-form-operations.service'; -import { SubmissionSectionIdentifiersComponent } from './sections/identifiers/section-identifiers.component'; -import { SubmissionSectionLicenseComponent } from './sections/license/section-license.component'; -import { CoarNotifyConfigDataService } from './sections/section-coar-notify/coar-notify-config-data.service'; -import { SubmissionSectionCoarNotifyComponent } from './sections/section-coar-notify/section-coar-notify.component'; -import { SectionsDirective } from './sections/sections.directive'; -import { SectionsService } from './sections/sections.service'; -import { ContentAccordionComponent } from './sections/sherpa-policies/content-accordion/content-accordion.component'; -import { MetadataInformationComponent } from './sections/sherpa-policies/metadata-information/metadata-information.component'; -import { PublicationInformationComponent } from './sections/sherpa-policies/publication-information/publication-information.component'; -import { PublisherPolicyComponent } from './sections/sherpa-policies/publisher-policy/publisher-policy.component'; -import { SubmissionSectionSherpaPoliciesComponent } from './sections/sherpa-policies/section-sherpa-policies.component'; -import { SubmissionSectionUploadAccessConditionsComponent } from './sections/upload/accessConditions/submission-section-upload-access-conditions.component'; -import { SubmissionSectionUploadFileEditComponent } from './sections/upload/file/edit/section-upload-file-edit.component'; -import { SubmissionSectionUploadFileComponent } from './sections/upload/file/section-upload-file.component'; -import { ThemedSubmissionSectionUploadFileComponent } from './sections/upload/file/themed-section-upload-file.component'; -import { SubmissionSectionUploadFileViewComponent } from './sections/upload/file/view/section-upload-file-view.component'; -import { SubmissionSectionUploadComponent } from './sections/upload/section-upload.component'; -import { SectionUploadService } from './sections/upload/section-upload.service'; -import { submissionEffects } from './submission.effects'; -import { - submissionReducers, - SubmissionState, -} from './submission.reducers'; -import { SubmissionSubmitComponent } from './submit/submission-submit.component'; -import { ThemedSubmissionSubmitComponent } from './submit/themed-submission-submit.component'; - -const ENTRY_COMPONENTS = [ - // put only entry components that use custom decorator - SubmissionSectionUploadComponent, - SubmissionSectionFormComponent, - SubmissionSectionLicenseComponent, - SubmissionSectionCcLicensesComponent, - SubmissionSectionAccessesComponent, - SubmissionSectionSherpaPoliciesComponent, - SubmissionSectionCoarNotifyComponent, - SubmissionSectionDuplicatesComponent, -]; - -const DECLARATIONS = [ - ...ENTRY_COMPONENTS, - SectionsDirective, - SubmissionEditComponent, - ThemedSubmissionEditComponent, - SubmissionFormSectionAddComponent, - SubmissionFormCollectionComponent, - SubmissionFormComponent, - SubmissionFormFooterComponent, - SubmissionSubmitComponent, - ThemedSubmissionSubmitComponent, - SubmissionUploadFilesComponent, - SubmissionSectionContainerComponent, - SubmissionSectionUploadAccessConditionsComponent, - SubmissionSectionUploadFileComponent, - SubmissionSectionUploadFileEditComponent, - SubmissionSectionUploadFileViewComponent, - SubmissionSectionIdentifiersComponent, - SubmissionSectionDuplicatesComponent, - SubmissionImportExternalComponent, - ThemedSubmissionImportExternalComponent, - SubmissionImportExternalSearchbarComponent, - SubmissionImportExternalPreviewComponent, - SubmissionImportExternalCollectionComponent, - ContentAccordionComponent, - PublisherPolicyComponent, - PublicationInformationComponent, - MetadataInformationComponent, - ThemedSubmissionSectionUploadFileComponent, -]; - -@NgModule({ - imports: [ - CommonModule, - CoreModule.forRoot(), - SharedModule, - StoreModule.forFeature('submission', submissionReducers, storeModuleConfig as StoreConfig), - EffectsModule.forFeature(), - EffectsModule.forFeature(submissionEffects), - JournalEntitiesModule.withEntryComponents(), - ResearchEntitiesModule.withEntryComponents(), - FormModule, - NgbModalModule, - NgbCollapseModule, - NgbAccordionModule, - UploadModule, - NgOptimizedImage, - ], - declarations: DECLARATIONS, - exports: [ - ...DECLARATIONS, - FormModule, - ], - providers: [ - SectionUploadService, - SectionsService, - SubmissionUploadsConfigDataService, - SubmissionAccessesConfigDataService, - SectionAccessesService, - SectionFormOperationsService, - CoarNotifyConfigDataService, - LdnServicesService, - ], -}) - -/** - * This module handles all components that are necessary for the submission process - */ -export class SubmissionModule { - /** - * NOTE: this method allows to resolve issue with components that using a custom decorator - * which are not loaded during SSR otherwise - */ - static withEntryComponents() { - return { - ngModule: SubmissionModule, - providers: ENTRY_COMPONENTS.map((component) => ({ provide: component })), - }; - } -} diff --git a/src/app/submission/submission.service.spec.ts b/src/app/submission/submission.service.spec.ts index 0ba8e5d9a2..b8f982101c 100644 --- a/src/app/submission/submission.service.spec.ts +++ b/src/app/submission/submission.service.spec.ts @@ -52,6 +52,7 @@ import { NotificationsService } from '../shared/notifications/notifications.serv import { createFailedRemoteDataObject } from '../shared/remote-data.utils'; import { SubmissionJsonPatchOperationsServiceStub } from '../shared/testing/submission-json-patch-operations-service.stub'; import { SubmissionRestServiceStub } from '../shared/testing/submission-rest-service.stub'; +import { SectionScope } from './objects/section-visibility.model'; import { CancelSubmissionFormAction, ChangeSubmissionCollectionAction, @@ -82,6 +83,7 @@ describe('SubmissionService test suite', () => { extraction: { config: '', mandatory: true, + scope: SectionScope.Submission, sectionType: 'utils', visibility: { main: 'HIDDEN', @@ -98,6 +100,7 @@ describe('SubmissionService test suite', () => { collection: { config: '', mandatory: true, + scope: SectionScope.Submission, sectionType: 'collection', visibility: { main: 'HIDDEN', @@ -237,6 +240,7 @@ describe('SubmissionService test suite', () => { extraction: { config: '', mandatory: true, + scope: SectionScope.Submission, sectionType: 'utils', visibility: { main: 'HIDDEN', @@ -253,6 +257,7 @@ describe('SubmissionService test suite', () => { collection: { config: '', mandatory: true, + scope: SectionScope.Submission, sectionType: 'collection', visibility: { main: 'HIDDEN', @@ -590,6 +595,7 @@ describe('SubmissionService test suite', () => { describe('getSubmissionSections', () => { it('should return submission form sections', () => { + spyOn(service, 'getSubmissionScope').and.returnValue(SubmissionScopeType.WorkspaceItem); spyOn((service as any).store, 'select').and.returnValue(hot('a|', { a: subState.objects[826], })); @@ -759,6 +765,7 @@ describe('SubmissionService test suite', () => { describe('getSubmissionStatus', () => { it('should return properly submission status', () => { + spyOn(service, 'getSubmissionScope').and.returnValue(SubmissionScopeType.WorkspaceItem); spyOn((service as any).store, 'select').and.returnValue(hot('-a-b', { a: subState, b: validSubState, @@ -818,41 +825,207 @@ describe('SubmissionService test suite', () => { }); describe('isSectionHidden', () => { - it('should return true/false when section is hidden/visible', () => { - let section: any = { - config: '', - header: '', - mandatory: true, - sectionType: 'collection' as any, - visibility: { - main: 'HIDDEN', - other: 'HIDDEN', - }, - collapsed: false, - enabled: true, - data: {}, - errorsToShow: [], - serverValidationErrors: [], - isLoading: false, - isValid: false, - }; - expect(service.isSectionHidden(section)).toBeTruthy(); + describe('when submission scope is workspace', () => { + beforeEach(() => { + spyOn(service, 'getSubmissionScope').and.returnValue(SubmissionScopeType.WorkspaceItem); + }); + + describe('and section scope is workspace', () => { + it('should return true when visibility main is HIDDEN and visibility other is null', () => { + let section: any = { + scope: SectionScope.Submission, + visibility: { + main: 'HIDDEN', + other: null, + }, + }; + expect(service.isSectionHidden(section)).toBeTrue(); + }); + it('should return true when both visibility main and other are HIDDEN', () => { + let section: any = { + scope: SectionScope.Submission, + visibility: { + main: 'HIDDEN', + other: 'HIDDEN', + }, + }; + expect(service.isSectionHidden(section)).toBeTrue(); + }); + it('should return false when visibility main is null and visibility other is HIDDEN', () => { + let section: any = { + scope: SectionScope.Submission, + visibility: { + main: null, + other: 'HIDDEN', + }, + }; + expect(service.isSectionHidden(section)).toBeFalse(); + }); + it('should return false when visibility is null', () => { + let section: any = { + scope: SectionScope.Submission, + visibility: null, + }; + expect(service.isSectionHidden(section)).toBeFalse(); + }); + }); + + describe('and section scope is workflow', () => { + it('should return false when visibility main is HIDDEN and visibility other is null', () => { + let section: any = { + scope: SectionScope.Workflow, + visibility: { + main: 'HIDDEN', + other: null, + }, + }; + expect(service.isSectionHidden(section)).toBeFalse(); + }); + it('should return true when both visibility main and other are HIDDEN', () => { + let section: any = { + scope: SectionScope.Workflow, + visibility: { + main: 'HIDDEN', + other: 'HIDDEN', + }, + }; + expect(service.isSectionHidden(section)).toBeTrue(); + }); + it('should return true when visibility main is null and visibility other is HIDDEN', () => { + let section: any = { + scope: SectionScope.Workflow, + visibility: { + main: null, + other: 'HIDDEN', + }, + }; + expect(service.isSectionHidden(section)).toBeTrue(); + }); + it('should return false when visibility is null', () => { + let section: any = { + scope: SectionScope.Workflow, + visibility: null, + }; + expect(service.isSectionHidden(section)).toBeFalse(); + }); + }); + + describe('and section scope is null', () => { + it('should return false', () => { + let section: any = { + scope: null, + visibility: { + main: 'HIDDEN', + other: null, + }, + }; + expect(service.isSectionHidden(section)).toBeFalse(); + }); + }); - section = { - header: 'submit.progressbar.describe.keyinformation', - config: 'https://rest.api/dspace-spring-rest/api/config/submissionforms/keyinformation', - mandatory: true, - sectionType: 'submission-form', - collapsed: false, - enabled: true, - data: {}, - errorsToShow: [], - serverValidationErrors: [], - isLoading: false, - isValid: false, - }; - expect(service.isSectionHidden(section)).toBeFalsy(); }); + + describe('when submission scope is workflow', () => { + beforeEach(() => { + spyOn(service, 'getSubmissionScope').and.returnValue(SubmissionScopeType.WorkflowItem); + }); + + describe('and section scope is workspace', () => { + it('should return false when visibility main is HIDDEN and visibility other is null', () => { + let section: any = { + scope: SectionScope.Submission, + visibility: { + main: 'HIDDEN', + other: null, + }, + }; + expect(service.isSectionHidden(section)).toBeFalse(); + }); + it('should return true when both visibility main and other are HIDDEN', () => { + let section: any = { + scope: SectionScope.Submission, + visibility: { + main: 'HIDDEN', + other: 'HIDDEN', + }, + }; + expect(service.isSectionHidden(section)).toBeTrue(); + }); + it('should return true when visibility main is null and visibility other is HIDDEN', () => { + let section: any = { + scope: SectionScope.Submission, + visibility: { + main: null, + other: 'HIDDEN', + }, + }; + expect(service.isSectionHidden(section)).toBeTrue(); + }); + it('should return false when visibility is null', () => { + let section: any = { + scope: SectionScope.Submission, + visibility: null, + }; + expect(service.isSectionHidden(section)).toBeFalse(); + }); + }); + + describe('and section scope is workflow', () => { + it('should return true when visibility main is HIDDEN and visibility other is null', () => { + let section: any = { + scope: SectionScope.Workflow, + visibility: { + main: 'HIDDEN', + other: null, + }, + }; + expect(service.isSectionHidden(section)).toBeTrue(); + }); + it('should return true when both visibility main and other are HIDDEN', () => { + let section: any = { + scope: SectionScope.Workflow, + visibility: { + main: 'HIDDEN', + other: 'HIDDEN', + }, + }; + expect(service.isSectionHidden(section)).toBeTrue(); + }); + it('should return false when visibility main is null and visibility other is HIDDEN', () => { + let section: any = { + scope: SectionScope.Workflow, + visibility: { + main: null, + other: 'HIDDEN', + }, + }; + expect(service.isSectionHidden(section)).toBeFalse(); + }); + it('should return false when visibility is null', () => { + let section: any = { + scope: SectionScope.Workflow, + visibility: null, + }; + expect(service.isSectionHidden(section)).toBeFalse(); + }); + }); + + describe('and section scope is null', () => { + it('should return false', () => { + let section: any = { + scope: null, + visibility: { + main: 'HIDDEN', + other: null, + }, + }; + expect(service.isSectionHidden(section)).toBeFalse(); + }); + }); + + }); + + }); describe('isSubmissionLoading', () => { diff --git a/src/app/submission/submission.service.ts b/src/app/submission/submission.service.ts index 312d74bc8a..ba8eff4f38 100644 --- a/src/app/submission/submission.service.ts +++ b/src/app/submission/submission.service.ts @@ -1,7 +1,12 @@ import { HttpHeaders } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Router } from '@angular/router'; -import { Store } from '@ngrx/store'; +import { + createSelector, + MemoizedSelector, + select, + Store, +} from '@ngrx/store'; import { TranslateService } from '@ngx-translate/core'; import { Observable, @@ -45,6 +50,7 @@ import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject, } from '../shared/remote-data.utils'; +import { SectionScope } from './objects/section-visibility.model'; import { SubmissionError } from './objects/submission-error.model'; import { CancelSubmissionFormAction, @@ -71,6 +77,20 @@ import { SubmissionState, } from './submission.reducers'; +function getSubmissionSelector(submissionId: string): MemoizedSelector { + return createSelector( + submissionSelector, + (state: SubmissionState) => state.objects[submissionId], + ); +} + +function getSubmissionCollectionIdSelector(submissionId: string): MemoizedSelector { + return createSelector( + getSubmissionSelector(submissionId), + (submission: SubmissionObjectEntry) => submission?.collection, + ); +} + /** * A service that provides methods used in submission process. */ @@ -120,10 +140,19 @@ export class SubmissionService { * @param collectionId * The collection id */ - changeSubmissionCollection(submissionId, collectionId) { + changeSubmissionCollection(submissionId: string, collectionId: string): void { this.store.dispatch(new ChangeSubmissionCollectionAction(submissionId, collectionId)); } + /** + * Listen to collection changes for a certain {@link SubmissionObject} + * + * @param submissionId The submission id + */ + getSubmissionCollectionId(submissionId: string): Observable { + return this.store.pipe(select(getSubmissionCollectionIdSelector(submissionId))); + } + /** * Perform a REST call to create a new workspaceitem and return response * @@ -476,9 +505,15 @@ export class SubmissionService { * true if section is hidden, false otherwise */ isSectionHidden(sectionData: SubmissionSectionObject): boolean { - return (isNotUndefined(sectionData.visibility) - && sectionData.visibility.main === 'HIDDEN' - && sectionData.visibility.other === 'HIDDEN'); + const submissionScope: SubmissionScopeType = this.getSubmissionScope(); + if (isEmpty(submissionScope) || isEmpty(sectionData.visibility) || isEmpty(sectionData.scope)) { + return false; + } + const convertedSubmissionScope: SectionScope = submissionScope.valueOf() === SubmissionScopeType.WorkspaceItem.valueOf() ? + SectionScope.Submission : SectionScope.Workflow; + const visibility = convertedSubmissionScope.valueOf() === sectionData.scope.valueOf() ? + sectionData.visibility.main : sectionData.visibility.other; + return visibility === 'HIDDEN'; } /** diff --git a/src/app/submission/submit/submission-submit.component.spec.ts b/src/app/submission/submit/submission-submit.component.spec.ts index 6b44714ff8..d54eaaeccb 100644 --- a/src/app/submission/submit/submission-submit.component.spec.ts +++ b/src/app/submission/submit/submission-submit.component.spec.ts @@ -50,8 +50,8 @@ describe('SubmissionSubmitComponent Component', () => { RouterTestingModule.withRoutes([ { path: '', component: SubmissionSubmitComponent, pathMatch: 'full' }, ]), + SubmissionSubmitComponent, ], - declarations: [SubmissionSubmitComponent], providers: [ { provide: NotificationsService, useClass: NotificationsServiceStub }, { provide: SubmissionService, useClass: SubmissionServiceStub }, diff --git a/src/app/submission/submit/submission-submit.component.ts b/src/app/submission/submit/submission-submit.component.ts index c76d90a5bc..c9e087f7d8 100644 --- a/src/app/submission/submit/submission-submit.component.ts +++ b/src/app/submission/submit/submission-submit.component.ts @@ -39,9 +39,10 @@ import { SubmissionService } from '../submission.service'; * This component allows to submit a new workspaceitem. */ @Component({ - selector: 'ds-submission-submit', + selector: 'ds-base-submission-submit', styleUrls: ['./submission-submit.component.scss'], templateUrl: './submission-submit.component.html', + standalone: true, }) export class SubmissionSubmitComponent implements OnDestroy, OnInit { diff --git a/src/app/submission/submit/themed-submission-submit.component.ts b/src/app/submission/submit/themed-submission-submit.component.ts index 6ea3eb9690..ee5ee74746 100644 --- a/src/app/submission/submit/themed-submission-submit.component.ts +++ b/src/app/submission/submit/themed-submission-submit.component.ts @@ -7,9 +7,11 @@ import { SubmissionSubmitComponent } from './submission-submit.component'; * Themed wrapper for SubmissionSubmitComponent */ @Component({ - selector: 'ds-themed-submission-submit', + selector: 'ds-submission-submit', styleUrls: [], templateUrl: './../../shared/theme-support/themed.component.html', + standalone: true, + imports: [SubmissionSubmitComponent], }) export class ThemedSubmissionSubmitComponent extends ThemedComponent { protected getComponentName(): string { diff --git a/src/app/submit-page/submit-page-routes.ts b/src/app/submit-page/submit-page-routes.ts new file mode 100644 index 0000000000..338a81af3e --- /dev/null +++ b/src/app/submit-page/submit-page-routes.ts @@ -0,0 +1,18 @@ +import { Route } from '@angular/router'; + +import { authenticatedGuard } from '../core/auth/authenticated.guard'; +import { i18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver'; +import { ThemedSubmissionSubmitComponent } from '../submission/submit/themed-submission-submit.component'; + +export const ROUTES: Route[] = [ + { + canActivate: [authenticatedGuard], + path: '', + pathMatch: 'full', + component: ThemedSubmissionSubmitComponent, + resolve: { + breadcrumb: i18nBreadcrumbResolver, + }, + data: { title: 'submission.submit.title', breadcrumbKey: 'submission.submit' }, + }, +]; diff --git a/src/app/submit-page/submit-page-routing.module.ts b/src/app/submit-page/submit-page-routing.module.ts deleted file mode 100644 index b7e6d3e6f5..0000000000 --- a/src/app/submit-page/submit-page-routing.module.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { NgModule } from '@angular/core'; -import { RouterModule } from '@angular/router'; - -import { AuthenticatedGuard } from '../core/auth/authenticated.guard'; -import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver'; -import { ThemedSubmissionSubmitComponent } from '../submission/submit/themed-submission-submit.component'; - -@NgModule({ - imports: [ - RouterModule.forChild([ - { - canActivate: [AuthenticatedGuard], - path: '', - pathMatch: 'full', - component: ThemedSubmissionSubmitComponent, - resolve: { - breadcrumb: I18nBreadcrumbResolver, - }, - data: { title: 'submission.submit.title', breadcrumbKey: 'submission.submit' }, - }, - ]), - ], -}) -/** - * This module defines the default component to load when navigating to the submit page path. - */ -export class SubmitPageRoutingModule { } diff --git a/src/app/submit-page/submit-page.module.ts b/src/app/submit-page/submit-page.module.ts deleted file mode 100644 index f6428e47e8..0000000000 --- a/src/app/submit-page/submit-page.module.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { CommonModule } from '@angular/common'; -import { NgModule } from '@angular/core'; - -import { FormModule } from '../shared/form/form.module'; -import { SharedModule } from '../shared/shared.module'; -import { SubmissionModule } from '../submission/submission.module'; -import { SubmitPageRoutingModule } from './submit-page-routing.module'; - -@NgModule({ - imports: [ - SubmitPageRoutingModule, - CommonModule, - SharedModule, - SubmissionModule, - FormModule, - ], -}) -/** - * This module handles all modules that need to access the submit page. - */ -export class SubmitPageModule { - -} diff --git a/src/app/subscriptions-page/subscriptions-page-routes.ts b/src/app/subscriptions-page/subscriptions-page-routes.ts new file mode 100644 index 0000000000..0dbdad2f02 --- /dev/null +++ b/src/app/subscriptions-page/subscriptions-page-routes.ts @@ -0,0 +1,18 @@ +import { Route } from '@angular/router'; + +import { SubscriptionsPageComponent } from './subscriptions-page.component'; + +export const ROUTES: Route[] = [ + { + path: '', + data: { + title: 'subscriptions.title', + }, + children: [ + { + path: '', + component: SubscriptionsPageComponent, + }, + ], + }, +]; diff --git a/src/app/subscriptions-page/subscriptions-page-routing.module.ts b/src/app/subscriptions-page/subscriptions-page-routing.module.ts deleted file mode 100644 index 15600688e7..0000000000 --- a/src/app/subscriptions-page/subscriptions-page-routing.module.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { NgModule } from '@angular/core'; -import { RouterModule } from '@angular/router'; - -import { SubscriptionsPageComponent } from './subscriptions-page.component'; -import { SubscriptionsPageModule } from './subscriptions-page.module'; - - -@NgModule({ - imports: [ - SubscriptionsPageModule, - RouterModule.forChild([ - { - path: '', - data: { - title: 'subscriptions.title', - }, - children: [ - { - path: '', - component: SubscriptionsPageComponent, - }, - ], - }, - ]), - ], -}) -export class SubscriptionsPageRoutingModule { -} diff --git a/src/app/subscriptions-page/subscriptions-page.component.html b/src/app/subscriptions-page/subscriptions-page.component.html index fcdaaaeace..e8dfc8fc6a 100644 --- a/src/app/subscriptions-page/subscriptions-page.component.html +++ b/src/app/subscriptions-page/subscriptions-page.component.html @@ -4,7 +4,7 @@

{{'subscriptions.title' | translate}}

- + { }, }), NoopAnimationsModule, + SubscriptionsPageComponent, SubscriptionViewComponent, VarDirective, ], - declarations: [SubscriptionsPageComponent, SubscriptionViewComponent, VarDirective], providers: [ { provide: SubscriptionsDataService, useValue: subscriptionServiceStub }, { provide: ActivatedRoute, useValue: new MockActivatedRoute() }, @@ -87,6 +90,11 @@ describe('SubscriptionsPageComponent', () => { ], schemas: [NO_ERRORS_SCHEMA], }) + .overrideComponent(SubscriptionsPageComponent, { + remove: { + imports: [ThemedLoadingComponent, PaginationComponent, AlertComponent], + }, + }) .compileComponents(); })); diff --git a/src/app/subscriptions-page/subscriptions-page.component.ts b/src/app/subscriptions-page/subscriptions-page.component.ts index e468a3918e..5d0b4b258d 100644 --- a/src/app/subscriptions-page/subscriptions-page.component.ts +++ b/src/app/subscriptions-page/subscriptions-page.component.ts @@ -1,8 +1,14 @@ +import { + AsyncPipe, + NgFor, + NgIf, +} from '@angular/common'; import { Component, OnDestroy, OnInit, } from '@angular/core'; +import { TranslateModule } from '@ngx-translate/core'; import { BehaviorSubject, combineLatestWith, @@ -27,16 +33,23 @@ import { EPerson } from '../core/eperson/models/eperson.model'; import { PaginationService } from '../core/pagination/pagination.service'; import { getAllCompletedRemoteData } from '../core/shared/operators'; import { PageInfo } from '../core/shared/page-info.model'; +import { AlertComponent } from '../shared/alert/alert.component'; import { AlertType } from '../shared/alert/alert-type'; import { hasValue } from '../shared/empty.util'; +import { ThemedLoadingComponent } from '../shared/loading/themed-loading.component'; +import { PaginationComponent } from '../shared/pagination/pagination.component'; import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model'; import { Subscription } from '../shared/subscriptions/models/subscription.model'; +import { SubscriptionViewComponent } from '../shared/subscriptions/subscription-view/subscription-view.component'; import { SubscriptionsDataService } from '../shared/subscriptions/subscriptions-data.service'; +import { VarDirective } from '../shared/utils/var.directive'; @Component({ selector: 'ds-subscriptions-page', templateUrl: './subscriptions-page.component.html', styleUrls: ['./subscriptions-page.component.scss'], + standalone: true, + imports: [NgIf, ThemedLoadingComponent, VarDirective, PaginationComponent, NgFor, SubscriptionViewComponent, AlertComponent, AsyncPipe, TranslateModule], }) /** * List and allow to manage all the active subscription for the current user diff --git a/src/app/subscriptions-page/subscriptions-page.module.ts b/src/app/subscriptions-page/subscriptions-page.module.ts deleted file mode 100644 index 1527c84e6e..0000000000 --- a/src/app/subscriptions-page/subscriptions-page.module.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { CommonModule } from '@angular/common'; -import { NgModule } from '@angular/core'; - -import { SharedModule } from '../shared/shared.module'; -import { SubscriptionsModule } from '../shared/subscriptions/subscriptions.module'; -import { SubscriptionsPageComponent } from './subscriptions-page.component'; - -@NgModule({ - declarations: [SubscriptionsPageComponent], - imports: [ - CommonModule, - SharedModule, - SubscriptionsModule, - ], -}) -export class SubscriptionsPageModule { } diff --git a/src/app/suggestion-notifications/selectors.ts b/src/app/suggestion-notifications/selectors.ts deleted file mode 100644 index a8ec8c7def..0000000000 --- a/src/app/suggestion-notifications/selectors.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { - createFeatureSelector, - createSelector, - MemoizedSelector, -} from '@ngrx/store'; - -import { SuggestionTarget } from '../core/notifications/models/suggestion-target.model'; -import { - suggestionNotificationsSelector, - SuggestionNotificationsState, -} from '../notifications/notifications.reducer'; -import { SuggestionTargetState } from '../notifications/suggestion-targets/suggestion-targets.reducer'; -import { subStateSelector } from '../submission/selectors'; - -/** - * Returns the Reciter Suggestion Target state. - * @function _getSuggestionTargetState - * @param {AppState} state Top level state. - * @return {SuggestionNotificationsState} - */ -const _getSuggestionTargetState = createFeatureSelector('suggestionNotifications'); - -// Reciter Suggestion Targets -// ---------------------------------------------------------------------------- - -/** - * Returns the Suggestion Targets State. - * @function suggestionTargetStateSelector - * @return {SuggestionNotificationsState} - */ -export function suggestionTargetStateSelector(): MemoizedSelector { - return subStateSelector(suggestionNotificationsSelector, 'suggestionTarget'); -} - -/** - * Returns the Suggestion Targets list. - * @function suggestionTargetObjectSelector - * @return {SuggestionTarget[]} - */ -export function suggestionTargetObjectSelector(): MemoizedSelector { - return subStateSelector(suggestionTargetStateSelector(), 'targets'); -} - -/** - * Returns true if the Suggestion Targets are loaded. - * @function isSuggestionTargetLoadedSelector - * @return {boolean} - */ -export const isSuggestionTargetLoadedSelector = createSelector(_getSuggestionTargetState, - (state: SuggestionNotificationsState) => state.suggestionTarget.loaded, -); - -/** - * Returns true if the deduplication sets are processing. - * @function isDeduplicationSetsProcessingSelector - * @return {boolean} - */ -export const isReciterSuggestionTargetProcessingSelector = createSelector(_getSuggestionTargetState, - (state: SuggestionNotificationsState) => state.suggestionTarget.processing, -); - -/** - * Returns the total available pages of Reciter Suggestion Targets. - * @function getSuggestionTargetTotalPagesSelector - * @return {number} - */ -export const getSuggestionTargetTotalPagesSelector = createSelector(_getSuggestionTargetState, - (state: SuggestionNotificationsState) => state.suggestionTarget.totalPages, -); - -/** - * Returns the current page of Suggestion Targets. - * @function getSuggestionTargetCurrentPageSelector - * @return {number} - */ -export const getSuggestionTargetCurrentPageSelector = createSelector(_getSuggestionTargetState, - (state: SuggestionNotificationsState) => state.suggestionTarget.currentPage, -); - -/** - * Returns the total number of Suggestion Targets. - * @function getSuggestionTargetTotalsSelector - * @return {number} - */ -export const getSuggestionTargetTotalsSelector = createSelector(_getSuggestionTargetState, - (state: SuggestionNotificationsState) => state.suggestionTarget.totalElements, -); - -/** - * Returns Suggestion Targets for the current user. - * @function getCurrentUserSuggestionTargetSelector - * @return {SuggestionTarget[]} - */ -export const getCurrentUserSuggestionTargetsSelector = createSelector(_getSuggestionTargetState, - (state: SuggestionNotificationsState) => state.suggestionTarget.currentUserTargets, -); - -/** - * Returns whether or not the user has consulted their suggestions - * @function getCurrentUserSuggestionTargetSelector - * @return {boolean} - */ -export const getCurrentUserSuggestionTargetsVisitedSelector = createSelector(_getSuggestionTargetState, - (state: SuggestionNotificationsState) => state.suggestionTarget.currentUserTargetsVisited, -); diff --git a/src/app/suggestions-page/suggestions-page-routes.ts b/src/app/suggestions-page/suggestions-page-routes.ts new file mode 100644 index 0000000000..f270a1ef66 --- /dev/null +++ b/src/app/suggestions-page/suggestions-page-routes.ts @@ -0,0 +1,24 @@ +import { Route } from '@angular/router'; + +import { authenticatedGuard } from '../core/auth/authenticated.guard'; +import { publicationClaimBreadcrumbResolver } from '../core/breadcrumbs/publication-claim-breadcrumb.resolver'; +import { SuggestionsPageComponent } from './suggestions-page.component'; +import { suggestionsPageResolver } from './suggestions-page.resolver'; + +export const ROUTES: Route[] = [ + { + path: ':targetId', + resolve: { + suggestionTargets: suggestionsPageResolver, + breadcrumb: publicationClaimBreadcrumbResolver,//i18nBreadcrumbResolver + }, + data: { + title: 'admin.notifications.publicationclaim.page.title', + breadcrumbKey: 'admin.notifications.publicationclaim', + showBreadcrumbsFluid: false, + }, + canActivate: [authenticatedGuard], + runGuardsAndResolvers: 'always', + component: SuggestionsPageComponent, + }, +]; diff --git a/src/app/suggestions-page/suggestions-page-routing.module.ts b/src/app/suggestions-page/suggestions-page-routing.module.ts deleted file mode 100644 index 0e5add0a7c..0000000000 --- a/src/app/suggestions-page/suggestions-page-routing.module.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { NgModule } from '@angular/core'; -import { RouterModule } from '@angular/router'; - -import { AuthenticatedGuard } from '../core/auth/authenticated.guard'; -import { PublicationClaimBreadcrumbResolver } from '../core/breadcrumbs/publication-claim-breadcrumb.resolver'; -import { SuggestionsPageComponent } from './suggestions-page.component'; -import { SuggestionsPageResolver } from './suggestions-page.resolver'; - -@NgModule({ - imports: [ - RouterModule.forChild([ - { - path: ':targetId', - resolve: { - suggestionTargets: SuggestionsPageResolver, - breadcrumb: PublicationClaimBreadcrumbResolver,//I18nBreadcrumbResolver - }, - data: { - title: 'admin.notifications.publicationclaim.page.title', - breadcrumbKey: 'admin.notifications.publicationclaim', - showBreadcrumbsFluid: false, - }, - canActivate: [AuthenticatedGuard], - runGuardsAndResolvers: 'always', - component: SuggestionsPageComponent, - }, - ]), - ], - providers: [ - SuggestionsPageResolver, - PublicationClaimBreadcrumbResolver, - ], -}) -export class SuggestionsPageRoutingModule { - -} diff --git a/src/app/suggestions-page/suggestions-page.component.html b/src/app/suggestions-page/suggestions-page.component.html index 75ba0315f3..45bdff32bf 100644 --- a/src/app/suggestions-page/suggestions-page.component.html +++ b/src/app/suggestions-page/suggestions-page.component.html @@ -2,7 +2,8 @@
-
+ +

{{'suggestion.suggestionFor' | translate}} @@ -21,7 +22,6 @@ (ignoreSuggestionClicked)="ignoreSuggestionAllSelected()">

-
-
{{ 'suggestion.count.missing' | translate }}
+ + {{'suggestion.count.missing' | translate}} +
diff --git a/src/app/suggestions-page/suggestions-page.component.spec.ts b/src/app/suggestions-page/suggestions-page.component.spec.ts index 9863f0012d..6c19405e94 100644 --- a/src/app/suggestions-page/suggestions-page.component.spec.ts +++ b/src/app/suggestions-page/suggestions-page.component.spec.ts @@ -1,11 +1,10 @@ import { CommonModule } from '@angular/common'; import { NO_ERRORS_SCHEMA } from '@angular/core'; import { - async, ComponentFixture, fakeAsync, TestBed, - tick, + waitForAsync, } from '@angular/core/testing'; import { BrowserModule } from '@angular/platform-browser'; import { @@ -23,11 +22,9 @@ import { TestScheduler } from 'rxjs/testing'; import { AuthService } from '../core/auth/auth.service'; import { PaginationService } from '../core/pagination/pagination.service'; import { WorkspaceitemDataService } from '../core/submission/workspaceitem-data.service'; +import { SuggestionApproveAndImport } from '../notifications/suggestion-list-element/suggestion-approve-and-import'; import { SuggestionEvidencesComponent } from '../notifications/suggestion-list-element/suggestion-evidences/suggestion-evidences.component'; -import { - SuggestionApproveAndImport, - SuggestionListElementComponent, -} from '../notifications/suggestion-list-element/suggestion-list-element.component'; +import { SuggestionListElementComponent } from '../notifications/suggestion-list-element/suggestion-list-element.component'; import { SuggestionTargetsStateService } from '../notifications/suggestion-targets/suggestion-targets.state.service'; import { SuggestionsService } from '../notifications/suggestions.service'; import { @@ -72,14 +69,12 @@ describe('SuggestionPageComponent', () => { }); const paginationService = new PaginationServiceStub(); - beforeEach(async(() => { + beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [ BrowserModule, CommonModule, TranslateModule.forRoot(), - ], - declarations: [ SuggestionEvidencesComponent, SuggestionListElementComponent, SuggestionsPageComponent, @@ -110,7 +105,7 @@ describe('SuggestionPageComponent', () => { }); it('should create', () => { - spyOn(component, 'updatePage').and.stub(); + spyOn(component, 'updatePage').and.callThrough(); scheduler.schedule(() => fixture.detectChanges()); scheduler.flush(); @@ -122,70 +117,72 @@ describe('SuggestionPageComponent', () => { }); it('should update page on pagination change', () => { - spyOn(component, 'updatePage').and.stub(); + spyOn(component, 'updatePage').and.callThrough(); + component.targetId$ = observableOf('testid'); - scheduler.schedule(() => fixture.detectChanges()); + scheduler.schedule(() => component.onPaginationChange()); scheduler.flush(); - component.onPaginationChange(); + expect(component.updatePage).toHaveBeenCalled(); }); - it('should update suggestion on page update', (done) => { + it('should update suggestion on page update', () => { spyOn(component.processing$, 'next'); spyOn(component.suggestionsRD$, 'next'); - scheduler.schedule(() => fixture.detectChanges()); + component.targetId$ = observableOf('testid'); + scheduler.schedule(() => component.updatePage().subscribe()); scheduler.flush(); - paginationService.getFindListOptions().subscribe(() => { - expect(component.processing$.next).toHaveBeenCalled(); - expect(mockSuggestionsService.getSuggestions).toHaveBeenCalled(); - expect(component.suggestionsRD$.next).toHaveBeenCalled(); - expect(mockSuggestionsService.clearSuggestionRequests).toHaveBeenCalled(); - done(); - }); - component.updatePage(); + + expect(component.processing$.next).toHaveBeenCalledTimes(2); + expect(mockSuggestionsService.getSuggestions).toHaveBeenCalled(); + expect(component.suggestionsRD$.next).toHaveBeenCalled(); + expect(mockSuggestionsService.clearSuggestionRequests).toHaveBeenCalled(); }); it('should flag suggestion for deletion', fakeAsync(() => { - spyOn(component, 'updatePage').and.stub(); + spyOn(component, 'updatePage').and.callThrough(); + component.targetId$ = observableOf('testid'); - scheduler.schedule(() => fixture.detectChanges()); + scheduler.schedule(() => component.ignoreSuggestion('1')); scheduler.flush(); - component.ignoreSuggestion('1'); + expect(mockSuggestionsService.ignoreSuggestion).toHaveBeenCalledWith('1'); expect(mockSuggestionsTargetStateService.dispatchRefreshUserSuggestionsAction).toHaveBeenCalled(); - tick(201); expect(component.updatePage).toHaveBeenCalled(); })); it('should flag all suggestion for deletion', () => { - spyOn(component, 'updatePage').and.stub(); + spyOn(component, 'updatePage').and.callThrough(); + component.targetId$ = observableOf('testid'); - scheduler.schedule(() => fixture.detectChanges()); + scheduler.schedule(() => component.ignoreSuggestionAllSelected()); scheduler.flush(); - component.ignoreSuggestionAllSelected(); + expect(mockSuggestionsService.ignoreSuggestionMultiple).toHaveBeenCalled(); expect(mockSuggestionsTargetStateService.dispatchRefreshUserSuggestionsAction).toHaveBeenCalled(); expect(component.updatePage).toHaveBeenCalled(); }); it('should approve and import', () => { - spyOn(component, 'updatePage').and.stub(); + spyOn(component, 'updatePage').and.callThrough(); + component.targetId$ = observableOf('testid'); - scheduler.schedule(() => fixture.detectChanges()); + scheduler.schedule(() => component.approveAndImport({ collectionId: '1234' } as unknown as SuggestionApproveAndImport)); scheduler.flush(); - component.approveAndImport({ collectionId: '1234' } as unknown as SuggestionApproveAndImport); + expect(mockSuggestionsService.approveAndImport).toHaveBeenCalled(); expect(mockSuggestionsTargetStateService.dispatchRefreshUserSuggestionsAction).toHaveBeenCalled(); expect(component.updatePage).toHaveBeenCalled(); }); it('should approve and import multiple suggestions', () => { - spyOn(component, 'updatePage').and.stub(); + spyOn(component, 'updatePage').and.callThrough(); + component.targetId$ = observableOf('testid'); - scheduler.schedule(() => fixture.detectChanges()); + scheduler.schedule(() => component.approveAndImportAllSelected({ collectionId: '1234' } as unknown as SuggestionApproveAndImport)); scheduler.flush(); - component.approveAndImportAllSelected({ collectionId: '1234' } as unknown as SuggestionApproveAndImport); + expect(mockSuggestionsService.approveAndImportMultiple).toHaveBeenCalled(); expect(mockSuggestionsTargetStateService.dispatchRefreshUserSuggestionsAction).toHaveBeenCalled(); expect(component.updatePage).toHaveBeenCalled(); diff --git a/src/app/suggestions-page/suggestions-page.component.ts b/src/app/suggestions-page/suggestions-page.component.ts index fefc655070..0fc790a125 100644 --- a/src/app/suggestions-page/suggestions-page.component.ts +++ b/src/app/suggestions-page/suggestions-page.component.ts @@ -1,3 +1,8 @@ +import { + AsyncPipe, + NgForOf, + NgIf, +} from '@angular/common'; import { Component, OnInit, @@ -6,8 +11,12 @@ import { ActivatedRoute, Data, Router, + RouterLink, } from '@angular/router'; -import { TranslateService } from '@ngx-translate/core'; +import { + TranslateModule, + TranslateService, +} from '@ngx-translate/core'; import { BehaviorSubject, combineLatest, @@ -17,7 +26,7 @@ import { distinctUntilChanged, map, switchMap, - take, + tap, } from 'rxjs/operators'; import { AuthService } from '../core/auth/auth.service'; @@ -28,27 +37,50 @@ import { import { FindListOptions } from '../core/data/find-list-options.model'; import { PaginatedList } from '../core/data/paginated-list.model'; import { RemoteData } from '../core/data/remote-data'; -import { Suggestion } from '../core/notifications/models/suggestion.model'; -import { SuggestionTarget } from '../core/notifications/models/suggestion-target.model'; +import { Suggestion } from '../core/notifications/suggestions/models/suggestion.model'; +import { SuggestionTarget } from '../core/notifications/suggestions/models/suggestion-target.model'; import { PaginationService } from '../core/pagination/pagination.service'; import { redirectOn4xx } from '../core/shared/authorized.operators'; -import { getFirstSucceededRemoteDataPayload } from '../core/shared/operators'; +import { + getFirstCompletedRemoteData, + getFirstSucceededRemoteDataPayload, +} from '../core/shared/operators'; import { WorkspaceItem } from '../core/submission/models/workspaceitem.model'; import { WorkspaceitemDataService } from '../core/submission/workspaceitem-data.service'; -import { SuggestionApproveAndImport } from '../notifications/suggestion-list-element/suggestion-list-element.component'; +import { SuggestionActionsComponent } from '../notifications/suggestion-actions/suggestion-actions.component'; +import { SuggestionApproveAndImport } from '../notifications/suggestion-list-element/suggestion-approve-and-import'; +import { SuggestionListElementComponent } from '../notifications/suggestion-list-element/suggestion-list-element.component'; import { SuggestionTargetsStateService } from '../notifications/suggestion-targets/suggestion-targets.state.service'; import { SuggestionBulkResult, SuggestionsService, } from '../notifications/suggestions.service'; +import { AlertComponent } from '../shared/alert/alert.component'; +import { ThemedLoadingComponent } from '../shared/loading/themed-loading.component'; import { NotificationsService } from '../shared/notifications/notifications.service'; +import { PaginationComponent } from '../shared/pagination/pagination.component'; import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model'; +import { VarDirective } from '../shared/utils/var.directive'; import { getWorkspaceItemEditRoute } from '../workflowitems-edit-page/workflowitems-edit-page-routing-paths'; @Component({ selector: 'ds-suggestion-page', templateUrl: './suggestions-page.component.html', styleUrls: ['./suggestions-page.component.scss'], + imports: [ + AsyncPipe, + VarDirective, + NgIf, + RouterLink, + TranslateModule, + SuggestionActionsComponent, + ThemedLoadingComponent, + PaginationComponent, + SuggestionListElementComponent, + NgForOf, + AlertComponent, + ], + standalone: true, }) /** @@ -122,14 +154,15 @@ export class SuggestionsPageComponent implements OnInit { ); this.targetRD$.pipe( getFirstSucceededRemoteDataPayload(), - ).subscribe((suggestionTarget: SuggestionTarget) => { - this.suggestionTarget = suggestionTarget; - this.suggestionId = suggestionTarget.id; - this.researcherName = suggestionTarget.display; - this.suggestionSource = suggestionTarget.source; - this.researcherUuid = this.suggestionService.getTargetUuid(suggestionTarget); - this.updatePage(); - }); + tap((suggestionTarget: SuggestionTarget) => { + this.suggestionTarget = suggestionTarget; + this.suggestionId = suggestionTarget.id; + this.researcherName = suggestionTarget.display; + this.suggestionSource = suggestionTarget.source; + this.researcherUuid = this.suggestionService.getTargetUuid(suggestionTarget); + }), + switchMap(() => this.updatePage()), + ).subscribe(); this.suggestionTargetsStateService.dispatchMarkUserSuggestionsAsVisitedAction(); } @@ -138,13 +171,13 @@ export class SuggestionsPageComponent implements OnInit { * Called when one of the pagination settings is changed */ onPaginationChange() { - this.updatePage(); + this.updatePage().subscribe(); } /** * Update the list of suggestions */ - updatePage() { + updatePage(): Observable>> { this.processing$.next(true); const pageConfig$: Observable = this.paginationService.getFindListOptions( this.paginationOptions.id, @@ -152,7 +185,8 @@ export class SuggestionsPageComponent implements OnInit { ).pipe( distinctUntilChanged(), ); - combineLatest([this.targetId$, pageConfig$]).pipe( + + return combineLatest([this.targetId$, pageConfig$]).pipe( switchMap(([targetId, config]: [string, FindListOptions]) => { return this.suggestionService.getSuggestions( targetId, @@ -161,12 +195,18 @@ export class SuggestionsPageComponent implements OnInit { config.sort, ); }), - take(1), - ).subscribe((results: PaginatedList) => { - this.processing$.next(false); - this.suggestionsRD$.next(results); - this.suggestionService.clearSuggestionRequests(); - }); + getFirstCompletedRemoteData(), + tap((resultsRD: RemoteData>) => { + this.processing$.next(false); + if (resultsRD.hasSucceeded) { + this.suggestionsRD$.next(resultsRD.payload); + } else { + this.suggestionsRD$.next(null); + } + + this.suggestionService.clearSuggestionRequests(); + }), + ); } /** @@ -174,11 +214,10 @@ export class SuggestionsPageComponent implements OnInit { * @suggestionId */ ignoreSuggestion(suggestionId) { - this.suggestionService.ignoreSuggestion(suggestionId).subscribe(() => { - this.suggestionTargetsStateService.dispatchRefreshUserSuggestionsAction(); - //We add a little delay in the page refresh so that we ensure the deletion has been propagated - setTimeout(() => this.updatePage(), 200); - }); + this.suggestionService.ignoreSuggestion(suggestionId).pipe( + tap(() => this.suggestionTargetsStateService.dispatchRefreshUserSuggestionsAction()), + switchMap(() => this.updatePage()), + ).subscribe(); } /** @@ -186,11 +225,9 @@ export class SuggestionsPageComponent implements OnInit { */ ignoreSuggestionAllSelected() { this.isBulkOperationPending = true; - this.suggestionService - .ignoreSuggestionMultiple(Object.values(this.selectedSuggestions)) - .subscribe((results: SuggestionBulkResult) => { + this.suggestionService.ignoreSuggestionMultiple(Object.values(this.selectedSuggestions)).pipe( + tap((results: SuggestionBulkResult) => { this.suggestionTargetsStateService.dispatchRefreshUserSuggestionsAction(); - this.updatePage(); this.isBulkOperationPending = false; this.selectedSuggestions = {}; if (results.success > 0) { @@ -203,7 +240,9 @@ export class SuggestionsPageComponent implements OnInit { this.translateService.get('suggestion.ignoreSuggestion.bulk.error', { count: results.fails })); } - }); + }), + switchMap(() => this.updatePage()), + ).subscribe(); } /** @@ -211,13 +250,14 @@ export class SuggestionsPageComponent implements OnInit { * @param event contains the suggestion and the target collection */ approveAndImport(event: SuggestionApproveAndImport) { - this.suggestionService.approveAndImport(this.workspaceItemService, event.suggestion, event.collectionId) - .subscribe((workspaceitem: WorkspaceItem) => { + this.suggestionService.approveAndImport(this.workspaceItemService, event.suggestion, event.collectionId).pipe( + tap((workspaceitem: WorkspaceItem) => { const content = this.translateService.instant('suggestion.approveAndImport.success', { url: getWorkspaceItemEditRoute(workspaceitem.id) }); this.notificationService.success('', content, { timeOut:0 }, true); this.suggestionTargetsStateService.dispatchRefreshUserSuggestionsAction(); - this.updatePage(); - }); + }), + switchMap(() => this.updatePage()), + ).subscribe(); } /** @@ -226,11 +266,9 @@ export class SuggestionsPageComponent implements OnInit { */ approveAndImportAllSelected(event: SuggestionApproveAndImport) { this.isBulkOperationPending = true; - this.suggestionService - .approveAndImportMultiple(this.workspaceItemService, Object.values(this.selectedSuggestions), event.collectionId) - .subscribe((results: SuggestionBulkResult) => { + this.suggestionService.approveAndImportMultiple(this.workspaceItemService, Object.values(this.selectedSuggestions), event.collectionId).pipe( + tap((results: SuggestionBulkResult) => { this.suggestionTargetsStateService.dispatchRefreshUserSuggestionsAction(); - this.updatePage(); this.isBulkOperationPending = false; this.selectedSuggestions = {}; if (results.success > 0) { @@ -243,7 +281,9 @@ export class SuggestionsPageComponent implements OnInit { this.translateService.get('suggestion.approveAndImport.bulk.error', { count: results.fails })); } - }); + }), + switchMap(() => this.updatePage()), + ).subscribe(); } /** diff --git a/src/app/suggestions-page/suggestions-page.module.ts b/src/app/suggestions-page/suggestions-page.module.ts deleted file mode 100644 index 8ba4f587f6..0000000000 --- a/src/app/suggestions-page/suggestions-page.module.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { CommonModule } from '@angular/common'; -import { NgModule } from '@angular/core'; - -import { SuggestionsDataService } from '../core/notifications/suggestions-data.service'; -import { NotificationsModule } from '../notifications/notifications.module'; -import { SuggestionsService } from '../notifications/suggestions.service'; -import { SharedModule } from '../shared/shared.module'; -import { SuggestionsPageComponent } from './suggestions-page.component'; -import { SuggestionsPageRoutingModule } from './suggestions-page-routing.module'; - -@NgModule({ - declarations: [SuggestionsPageComponent], - imports: [ - CommonModule, - SharedModule, - SuggestionsPageRoutingModule, - NotificationsModule, - ], - providers: [ - SuggestionsDataService, - SuggestionsService, - ], -}) -export class SuggestionsPageModule { } diff --git a/src/app/suggestions-page/suggestions-page.resolver.ts b/src/app/suggestions-page/suggestions-page.resolver.ts index e066065c9a..1314846b6e 100644 --- a/src/app/suggestions-page/suggestions-page.resolver.ts +++ b/src/app/suggestions-page/suggestions-page.resolver.ts @@ -1,35 +1,30 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, - Resolve, + ResolveFn, RouterStateSnapshot, } from '@angular/router'; import { Observable } from 'rxjs'; -import { find } from 'rxjs/operators'; import { RemoteData } from '../core/data/remote-data'; -import { SuggestionTarget } from '../core/notifications/models/suggestion-target.model'; -import { SuggestionTargetDataService } from '../core/notifications/target/suggestion-target-data.service'; -import { hasValue } from '../shared/empty.util'; +import { SuggestionTarget } from '../core/notifications/suggestions/models/suggestion-target.model'; +import { SuggestionTargetDataService } from '../core/notifications/suggestions/target/suggestion-target-data.service'; +import { getFirstCompletedRemoteData } from '../core/shared/operators'; /** - * This class represents a resolver that requests a specific collection before the route is activated + * Method for resolving a suggestion target based on the parameters in the current route + * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot + * @param {RouterStateSnapshot} state The current RouterStateSnapshot + * @param {SuggestionTargetDataService} suggestionsDataService + * @returns Observable<> Emits the found collection based on the parameters in the current route, + * or an error if something went wrong */ -@Injectable() -export class SuggestionsPageResolver implements Resolve> { - constructor(private suggestionsDataService: SuggestionTargetDataService) { - } - - /** - * Method for resolving a suggestion target based on the parameters in the current route - * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot - * @param {RouterStateSnapshot} state The current RouterStateSnapshot - * @returns Observable<> Emits the found collection based on the parameters in the current route, - * or an error if something went wrong - */ - resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable> { - return this.suggestionsDataService.getTargetById(route.params.targetId).pipe( - find((RD) => hasValue(RD.hasFailed) || RD.hasSucceeded), - ); - } -} +export const suggestionsPageResolver: ResolveFn> = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + suggestionsDataService: SuggestionTargetDataService = inject(SuggestionTargetDataService), +): Observable> => { + return suggestionsDataService.getTargetById(route.params.targetId).pipe( + getFirstCompletedRemoteData(), + ); +}; diff --git a/src/app/system-wide-alert/alert-banner/system-wide-alert-banner.component.html b/src/app/system-wide-alert/alert-banner/system-wide-alert-banner.component.html index d97d3d5d70..c2c1942cdd 100644 --- a/src/app/system-wide-alert/alert-banner/system-wide-alert-banner.component.html +++ b/src/app/system-wide-alert/alert-banner/system-wide-alert-banner.component.html @@ -2,26 +2,20 @@
- - {{'system-wide-alert-banner.countdown.prefix' | translate }} - - - {{'system-wide-alert-banner.countdown.days' | translate: { - days: countDownDays|async - } }} - - - {{'system-wide-alert-banner.countdown.hours' | translate: { - hours: countDownHours| async - } }} - - - {{'system-wide-alert-banner.countdown.minutes' | translate: { - minutes: countDownMinutes|async - } }} - + + {{ 'system-wide-alert-banner.countdown.prefix' | translate }} + + + {{ 'system-wide-alert-banner.countdown.days' | translate: { days: countDownDays|async } }} + + + {{ 'system-wide-alert-banner.countdown.hours' | translate: { hours: countDownHours| async } }} + + + {{ 'system-wide-alert-banner.countdown.minutes' | translate: { minutes: countDownMinutes|async } }} + - {{(systemWideAlert$ |async)?.message}} +
-
+
\ No newline at end of file diff --git a/src/app/system-wide-alert/alert-banner/system-wide-alert-banner.component.spec.ts b/src/app/system-wide-alert/alert-banner/system-wide-alert-banner.component.spec.ts index 155893f160..a1d6e3b27e 100644 --- a/src/app/system-wide-alert/alert-banner/system-wide-alert-banner.component.spec.ts +++ b/src/app/system-wide-alert/alert-banner/system-wide-alert-banner.component.spec.ts @@ -49,8 +49,7 @@ describe('SystemWideAlertBannerComponent', () => { }); TestBed.configureTestingModule({ - imports: [TranslateModule.forRoot()], - declarations: [SystemWideAlertBannerComponent], + imports: [TranslateModule.forRoot(), SystemWideAlertBannerComponent], providers: [ { provide: SystemWideAlertDataService, useValue: systemWideAlertDataService }, { provide: NotificationsService, useValue: new NotificationsServiceStub() }, diff --git a/src/app/system-wide-alert/alert-banner/system-wide-alert-banner.component.ts b/src/app/system-wide-alert/alert-banner/system-wide-alert-banner.component.ts index a9550944d7..79ecf2117a 100644 --- a/src/app/system-wide-alert/alert-banner/system-wide-alert-banner.component.ts +++ b/src/app/system-wide-alert/alert-banner/system-wide-alert-banner.component.ts @@ -1,4 +1,8 @@ -import { isPlatformBrowser } from '@angular/common'; +import { + AsyncPipe, + isPlatformBrowser, + NgIf, +} from '@angular/common'; import { Component, Inject, @@ -6,6 +10,7 @@ import { OnInit, PLATFORM_ID, } from '@angular/core'; +import { TranslateModule } from '@ngx-translate/core'; import { zonedTimeToUtc } from 'date-fns-tz'; import { BehaviorSubject, @@ -36,6 +41,8 @@ import { SystemWideAlert } from '../system-wide-alert.model'; selector: 'ds-system-wide-alert-banner', styleUrls: ['./system-wide-alert-banner.component.scss'], templateUrl: './system-wide-alert-banner.component.html', + standalone: true, + imports: [NgIf, AsyncPipe, TranslateModule], }) export class SystemWideAlertBannerComponent implements OnInit, OnDestroy { diff --git a/src/app/system-wide-alert/alert-form/system-wide-alert-form.component.html b/src/app/system-wide-alert/alert-form/system-wide-alert-form.component.html index 95c7a69448..770f465a4b 100644 --- a/src/app/system-wide-alert/alert-form/system-wide-alert-form.component.html +++ b/src/app/system-wide-alert/alert-form/system-wide-alert-form.component.html @@ -5,23 +5,22 @@
+ [uncheckedLabel]="'system-wide-alert.form.label.inactive' | translate" + [checked]="formActive.value" (change)="setActive($event)">
- - {{ 'system-wide-alert.form.error.message' | translate }} - + class="invalid-feedback show-feedback"> + + {{ 'system-wide-alert.form.error.message' | translate }} +
@@ -29,10 +28,8 @@
- + {{ 'system-wide-alert.form.label.countdownTo.enable' | translate }}
@@ -40,18 +37,10 @@
- +
@@ -68,49 +57,38 @@
- {{'system-wide-alert.form.label.countdownTo.hint' | translate}} + {{ 'system-wide-alert.form.label.countdownTo.hint' | translate }}
- -
- +
- - {{'system-wide-alert-banner.countdown.prefix' | translate }} - - - {{'system-wide-alert-banner.countdown.days' | translate: { - days: previewDays - } }} - - - {{'system-wide-alert-banner.countdown.hours' | translate: { - hours: previewHours - } }} - - - {{'system-wide-alert-banner.countdown.minutes' | translate: { - minutes: previewMinutes - } }} - + + {{ 'system-wide-alert-banner.countdown.prefix' | translate }} + + + {{ 'system-wide-alert-banner.countdown.days' | translate: { days: previewDays } }} + + + {{ 'system-wide-alert-banner.countdown.hours' | translate: { hours: previewHours } }} + + + {{ 'system-wide-alert-banner.countdown.minutes' | translate: { minutes: previewMinutes } }} + - {{formMessage.value}} +
-
- - +
- - + \ No newline at end of file diff --git a/src/app/system-wide-alert/alert-form/system-wide-alert-form.component.spec.ts b/src/app/system-wide-alert/alert-form/system-wide-alert-form.component.spec.ts index c5beed4439..8e0462d084 100644 --- a/src/app/system-wide-alert/alert-form/system-wide-alert-form.component.spec.ts +++ b/src/app/system-wide-alert/alert-form/system-wide-alert-form.component.spec.ts @@ -23,7 +23,6 @@ import { NotificationsServiceStub } from '../../shared/testing/notifications-ser import { RouterStub } from '../../shared/testing/router.stub'; import { createPaginatedList } from '../../shared/testing/utils.test'; import { SystemWideAlert } from '../system-wide-alert.model'; -import { SystemWideAlertModule } from '../system-wide-alert.module'; import { SystemWideAlertFormComponent } from './system-wide-alert-form.component'; describe('SystemWideAlertFormComponent', () => { @@ -63,8 +62,7 @@ describe('SystemWideAlertFormComponent', () => { router = new RouterStub(); TestBed.configureTestingModule({ - imports: [FormsModule, SystemWideAlertModule, UiSwitchModule, TranslateModule.forRoot()], - declarations: [SystemWideAlertFormComponent], + imports: [FormsModule, UiSwitchModule, TranslateModule.forRoot(), SystemWideAlertFormComponent], providers: [ { provide: SystemWideAlertDataService, useValue: systemWideAlertDataService }, { provide: NotificationsService, useValue: notificationsService }, @@ -172,7 +170,7 @@ describe('SystemWideAlertFormComponent', () => { }); describe('save', () => { - it('should update the exising alert with the form values and show a success notification on success and navigate back', () => { + it('should update the existing alert with the form values and show a success notification on success and navigate back', () => { spyOn(comp, 'back'); comp.currentAlert = systemWideAlert; @@ -195,7 +193,7 @@ describe('SystemWideAlertFormComponent', () => { expect(requestService.setStaleByHrefSubstring).toHaveBeenCalledWith('systemwidealerts'); expect(comp.back).toHaveBeenCalled(); }); - it('should update the exising alert with the form values and show a success notification on success and not navigate back when false is provided to the save method', () => { + it('should update the existing alert with the form values and show a success notification on success and not navigate back when false is provided to the save method', () => { spyOn(comp, 'back'); comp.currentAlert = systemWideAlert; @@ -218,7 +216,7 @@ describe('SystemWideAlertFormComponent', () => { expect(requestService.setStaleByHrefSubstring).toHaveBeenCalledWith('systemwidealerts'); expect(comp.back).not.toHaveBeenCalled(); }); - it('should update the exising alert with the form values but add an empty countdown date when disabled and show a success notification on success', () => { + it('should update the existing alert with the form values but add an empty countdown date when disabled and show a success notification on success', () => { spyOn(comp, 'back'); comp.currentAlert = systemWideAlert; @@ -241,7 +239,7 @@ describe('SystemWideAlertFormComponent', () => { expect(requestService.setStaleByHrefSubstring).toHaveBeenCalledWith('systemwidealerts'); expect(comp.back).toHaveBeenCalled(); }); - it('should update the exising alert with the form values and show a error notification on error', () => { + it('should update the existing alert with the form values and show a error notification on error', () => { spyOn(comp, 'back'); (systemWideAlertDataService.put as jasmine.Spy).and.returnValue(createFailedRemoteDataObject$()); comp.currentAlert = systemWideAlert; @@ -313,6 +311,14 @@ describe('SystemWideAlertFormComponent', () => { expect(comp.back).not.toHaveBeenCalled(); }); + it('should not create the new alert when the enable button is clicked on an invalid the form', () => { + spyOn(comp as any, 'handleResponse'); + + comp.formMessage.patchValue(''); + comp.save(); + + expect((comp as any).handleResponse).not.toHaveBeenCalled(); + }); }); describe('back', () => { it('should navigate back to the home page', () => { diff --git a/src/app/system-wide-alert/alert-form/system-wide-alert-form.component.ts b/src/app/system-wide-alert/alert-form/system-wide-alert-form.component.ts index bfe5e39c5d..b30e864fa1 100644 --- a/src/app/system-wide-alert/alert-form/system-wide-alert-form.component.ts +++ b/src/app/system-wide-alert/alert-form/system-wide-alert-form.component.ts @@ -1,20 +1,34 @@ +import { + AsyncPipe, + NgIf, +} from '@angular/common'; import { Component, OnInit, } from '@angular/core'; import { + FormsModule, + ReactiveFormsModule, UntypedFormBuilder, UntypedFormControl, UntypedFormGroup, Validators, } from '@angular/forms'; import { Router } from '@angular/router'; -import { NgbDateStruct } from '@ng-bootstrap/ng-bootstrap'; -import { TranslateService } from '@ngx-translate/core'; +import { + NgbDatepickerModule, + NgbDateStruct, + NgbTimepickerModule, +} from '@ng-bootstrap/ng-bootstrap'; +import { + TranslateModule, + TranslateService, +} from '@ngx-translate/core'; import { utcToZonedTime, zonedTimeToUtc, } from 'date-fns-tz'; +import { UiSwitchModule } from 'ngx-ui-switch'; import { BehaviorSubject, Observable, @@ -44,6 +58,8 @@ import { SystemWideAlert } from '../system-wide-alert.model'; selector: 'ds-system-wide-alert-form', styleUrls: ['./system-wide-alert-form.component.scss'], templateUrl: './system-wide-alert-form.component.html', + standalone: true, + imports: [FormsModule, ReactiveFormsModule, UiSwitchModule, NgIf, NgbDatepickerModule, NgbTimepickerModule, AsyncPipe, TranslateModule], }) export class SystemWideAlertFormComponent implements OnInit { @@ -240,11 +256,13 @@ export class SystemWideAlertFormComponent implements OnInit { } else { alert.countdownTo = null; } - if (hasValue(this.currentAlert)) { - const updatedAlert = Object.assign(new SystemWideAlert(), this.currentAlert, alert); - this.handleResponse(this.systemWideAlertDataService.put(updatedAlert), 'system-wide-alert.form.update', navigateToHomePage); - } else { - this.handleResponse(this.systemWideAlertDataService.create(alert), 'system-wide-alert.form.create', navigateToHomePage); + if (this.alertForm.valid) { + if (hasValue(this.currentAlert)) { + const updatedAlert = Object.assign(new SystemWideAlert(), this.currentAlert, alert); + this.handleResponse(this.systemWideAlertDataService.put(updatedAlert), 'system-wide-alert.form.update', navigateToHomePage); + } else { + this.handleResponse(this.systemWideAlertDataService.create(alert), 'system-wide-alert.form.create', navigateToHomePage); + } } } diff --git a/src/app/system-wide-alert/system-wide-alert-routes.ts b/src/app/system-wide-alert/system-wide-alert-routes.ts new file mode 100644 index 0000000000..a71007b6b3 --- /dev/null +++ b/src/app/system-wide-alert/system-wide-alert-routes.ts @@ -0,0 +1,13 @@ +import { Route } from '@angular/router'; + +import { siteAdministratorGuard } from '../core/data/feature-authorization/feature-authorization-guard/site-administrator.guard'; +import { SystemWideAlertFormComponent } from './alert-form/system-wide-alert-form.component'; + +export const ROUTES: Route[] = [ + { + path: '', + canActivate: [siteAdministratorGuard], + component: SystemWideAlertFormComponent, + }, + +]; diff --git a/src/app/system-wide-alert/system-wide-alert-routing.module.ts b/src/app/system-wide-alert/system-wide-alert-routing.module.ts deleted file mode 100644 index f9c9664600..0000000000 --- a/src/app/system-wide-alert/system-wide-alert-routing.module.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { NgModule } from '@angular/core'; -import { RouterModule } from '@angular/router'; - -import { SiteAdministratorGuard } from '../core/data/feature-authorization/feature-authorization-guard/site-administrator.guard'; -import { SystemWideAlertFormComponent } from './alert-form/system-wide-alert-form.component'; - -@NgModule({ - imports: [ - RouterModule.forChild([ - { - path: '', - canActivate: [SiteAdministratorGuard], - component: SystemWideAlertFormComponent, - }, - - ]), - ], -}) -export class SystemWideAlertRoutingModule { - -} diff --git a/src/app/system-wide-alert/system-wide-alert.model.ts b/src/app/system-wide-alert/system-wide-alert.model.ts index 3c2b524c1f..73bd7610a9 100644 --- a/src/app/system-wide-alert/system-wide-alert.model.ts +++ b/src/app/system-wide-alert/system-wide-alert.model.ts @@ -22,38 +22,38 @@ export class SystemWideAlert implements CacheableObject { */ @excludeFromEquals @autoserialize - type: ResourceType; + type: ResourceType; /** * The identifier for this system-wide alert */ @autoserialize - alertId: string; + alertId: string; /** * The message for this system-wide alert */ @autoserialize - message: string; + message: string; /** * A string representation of the date to which this system-wide alert will count down when active */ @autoserialize - countdownTo: string; + countdownTo: string; /** * Whether the system-wide alert is active */ @autoserialize - active: boolean; + active: boolean; /** * The {@link HALLink}s for this system-wide alert */ @deserialize - _links: { + _links: { self: HALLink, }; } diff --git a/src/app/system-wide-alert/system-wide-alert.module.ts b/src/app/system-wide-alert/system-wide-alert.module.ts deleted file mode 100644 index bad87363bc..0000000000 --- a/src/app/system-wide-alert/system-wide-alert.module.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { NgModule } from '@angular/core'; -import { FormsModule } from '@angular/forms'; -import { - NgbDatepickerModule, - NgbTimepickerModule, -} from '@ng-bootstrap/ng-bootstrap'; -import { UiSwitchModule } from 'ngx-ui-switch'; - -import { SystemWideAlertDataService } from '../core/data/system-wide-alert-data.service'; -import { SharedModule } from '../shared/shared.module'; -import { SystemWideAlertBannerComponent } from './alert-banner/system-wide-alert-banner.component'; -import { SystemWideAlertFormComponent } from './alert-form/system-wide-alert-form.component'; -import { SystemWideAlertRoutingModule } from './system-wide-alert-routing.module'; - -@NgModule({ - imports: [ - FormsModule, - SharedModule, - UiSwitchModule, - SystemWideAlertRoutingModule, - NgbTimepickerModule, - NgbDatepickerModule, - ], - exports: [ - SystemWideAlertBannerComponent, - ], - declarations: [ - SystemWideAlertBannerComponent, - SystemWideAlertFormComponent, - ], - providers: [ - SystemWideAlertDataService, - ], -}) -export class SystemWideAlertModule { - -} diff --git a/src/app/thumbnail/themed-thumbnail.component.ts b/src/app/thumbnail/themed-thumbnail.component.ts index 2073ec1171..6d14378d3a 100644 --- a/src/app/thumbnail/themed-thumbnail.component.ts +++ b/src/app/thumbnail/themed-thumbnail.component.ts @@ -9,9 +9,11 @@ import { ThemedComponent } from '../shared/theme-support/themed.component'; import { ThumbnailComponent } from './thumbnail.component'; @Component({ - selector: 'ds-themed-thumbnail', + selector: 'ds-thumbnail', styleUrls: [], templateUrl: '../shared/theme-support/themed.component.html', + standalone: true, + imports: [ThumbnailComponent], }) export class ThemedThumbnailComponent extends ThemedComponent { diff --git a/src/app/thumbnail/thumbnail.component.html b/src/app/thumbnail/thumbnail.component.html index 6a0516b0d4..e151684a01 100644 --- a/src/app/thumbnail/thumbnail.component.html +++ b/src/app/thumbnail/thumbnail.component.html @@ -1,21 +1,19 @@ -
+
- +
- - - -
-
-
- {{ placeholder | translate }} -
+ + +
+
+
+ {{ placeholder | translate }}
- +
diff --git a/src/app/thumbnail/thumbnail.component.spec.ts b/src/app/thumbnail/thumbnail.component.spec.ts index 696427630e..10e461112b 100644 --- a/src/app/thumbnail/thumbnail.component.spec.ts +++ b/src/app/thumbnail/thumbnail.component.spec.ts @@ -9,6 +9,7 @@ import { waitForAsync, } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; +import { TranslateModule } from '@ngx-translate/core'; import { of as observableOf } from 'rxjs'; import { AuthService } from '../core/auth/auth.service'; @@ -16,16 +17,21 @@ import { AuthorizationDataService } from '../core/data/feature-authorization/aut import { RemoteData } from '../core/data/remote-data'; import { Bitstream } from '../core/shared/bitstream.model'; import { FileService } from '../core/shared/file.service'; +import { getMockThemeService } from '../shared/mocks/theme-service.mock'; import { createFailedRemoteDataObject, createSuccessfulRemoteDataObject, } from '../shared/remote-data.utils'; +import { ThemeService } from '../shared/theme-support/theme.service'; import { SafeUrlPipe } from '../shared/utils/safe-url-pipe'; import { VarDirective } from '../shared/utils/var.directive'; import { ThumbnailComponent } from './thumbnail.component'; -// eslint-disable-next-line @angular-eslint/pipe-prefix -@Pipe({ name: 'translate' }) +@Pipe({ + // eslint-disable-next-line @angular-eslint/pipe-prefix + name: 'translate', + standalone: true, +}) class MockTranslatePipe implements PipeTransform { transform(key: string): string { return 'TRANSLATED ' + key; @@ -56,13 +62,25 @@ describe('ThumbnailComponent', () => { fileService.retrieveFileDownloadLink.and.callFake((url) => observableOf(`${url}?authentication-token=fake`)); TestBed.configureTestingModule({ - declarations: [ThumbnailComponent, SafeUrlPipe, MockTranslatePipe, VarDirective], + imports: [ + TranslateModule.forRoot(), + ThumbnailComponent, + SafeUrlPipe, + MockTranslatePipe, + VarDirective, + ], providers: [ { provide: AuthService, useValue: authService }, { provide: AuthorizationDataService, useValue: authorizationService }, { provide: FileService, useValue: fileService }, + { provide: ThemeService, useValue: getMockThemeService() }, ], - }).compileComponents(); + }).overrideComponent(ThumbnailComponent, { + add: { + imports: [MockTranslatePipe], + }, + }) + .compileComponents(); })); beforeEach(() => { @@ -78,31 +96,31 @@ describe('ThumbnailComponent', () => { describe('loading', () => { it('should start out with isLoading$ true', () => { - expect(comp.isLoading$.getValue()).toBeTrue(); + expect(comp.isLoading).toBeTrue(); }); it('should set isLoading$ to false once an image is successfully loaded', () => { comp.setSrc('http://bit.stream'); fixture.debugElement.query(By.css('img.thumbnail-content')).triggerEventHandler('load', new Event('load')); - expect(comp.isLoading$.getValue()).toBeFalse(); + expect(comp.isLoading).toBeFalse(); }); it('should set isLoading$ to false once the src is set to null', () => { comp.setSrc(null); - expect(comp.isLoading$.getValue()).toBeFalse(); + expect(comp.isLoading).toBeFalse(); }); it('should show a loading animation while isLoading$ is true', () => { - expect(de.query(By.css('ds-themed-loading'))).toBeTruthy(); + expect(de.query(By.css('ds-loading'))).toBeTruthy(); - comp.isLoading$.next(false); + comp.isLoading = false; fixture.detectChanges(); - expect(fixture.debugElement.query(By.css('ds-themed-loading'))).toBeFalsy(); + expect(fixture.debugElement.query(By.css('ds-loading'))).toBeFalsy(); }); describe('with a thumbnail image', () => { beforeEach(() => { - comp.src$.next('https://bit.stream'); + comp.src = 'https://bit.stream'; fixture.detectChanges(); }); @@ -111,7 +129,7 @@ describe('ThumbnailComponent', () => { expect(img).toBeTruthy(); expect(img.classes['d-none']).toBeTrue(); - comp.isLoading$.next(false); + comp.isLoading = false; fixture.detectChanges(); img = fixture.debugElement.query(By.css('img.thumbnail-content')); expect(img).toBeTruthy(); @@ -122,14 +140,14 @@ describe('ThumbnailComponent', () => { describe('without a thumbnail image', () => { beforeEach(() => { - comp.src$.next(null); + comp.src = null; fixture.detectChanges(); }); it('should only show the HTML placeholder once done loading', () => { expect(fixture.debugElement.query(By.css('div.thumbnail-placeholder'))).toBeFalsy(); - comp.isLoading$.next(false); + comp.isLoading = false; fixture.detectChanges(); expect(fixture.debugElement.query(By.css('div.thumbnail-placeholder'))).toBeTruthy(); }); @@ -225,14 +243,14 @@ describe('ThumbnailComponent', () => { describe('fallback', () => { describe('if there is a default image', () => { it('should display the default image', () => { - comp.src$.next('http://bit.stream'); + comp.src = 'http://bit.stream'; comp.defaultImage = 'http://default.img'; comp.errorHandler(); - expect(comp.src$.getValue()).toBe(comp.defaultImage); + expect(comp.src).toBe(comp.defaultImage); }); it('should include the alt text', () => { - comp.src$.next('http://bit.stream'); + comp.src = 'http://bit.stream'; comp.defaultImage = 'http://default.img'; comp.errorHandler(); @@ -244,10 +262,10 @@ describe('ThumbnailComponent', () => { describe('if there is no default image', () => { it('should display the HTML placeholder', () => { - comp.src$.next('http://default.img'); + comp.src = 'http://default.img'; comp.defaultImage = null; comp.errorHandler(); - expect(comp.src$.getValue()).toBe(null); + expect(comp.src).toBe(null); fixture.detectChanges(); const placeholder = fixture.debugElement.query(By.css('div.thumbnail-placeholder')).nativeElement; @@ -339,7 +357,7 @@ describe('ThumbnailComponent', () => { it('should show the default image', () => { comp.defaultImage = 'default/image.jpg'; comp.ngOnChanges({}); - expect(comp.src$.getValue()).toBe('default/image.jpg'); + expect(comp.src).toBe('default/image.jpg'); }); }); }); diff --git a/src/app/thumbnail/thumbnail.component.ts b/src/app/thumbnail/thumbnail.component.ts index 765a81961f..cc583c3998 100644 --- a/src/app/thumbnail/thumbnail.component.ts +++ b/src/app/thumbnail/thumbnail.component.ts @@ -1,13 +1,12 @@ +import { CommonModule } from '@angular/common'; import { Component, Input, OnChanges, SimpleChanges, } from '@angular/core'; -import { - BehaviorSubject, - of as observableOf, -} from 'rxjs'; +import { TranslateModule } from '@ngx-translate/core'; +import { of as observableOf } from 'rxjs'; import { switchMap } from 'rxjs/operators'; import { AuthService } from '../core/auth/auth.service'; @@ -20,6 +19,9 @@ import { hasNoValue, hasValue, } from '../shared/empty.util'; +import { ThemedLoadingComponent } from '../shared/loading/themed-loading.component'; +import { SafeUrlPipe } from '../shared/utils/safe-url-pipe'; +import { VarDirective } from '../shared/utils/var.directive'; /** * This component renders a given Bitstream as a thumbnail. @@ -27,9 +29,11 @@ import { * If no Bitstream is provided, an HTML placeholder will be rendered instead. */ @Component({ - selector: 'ds-thumbnail', + selector: 'ds-base-thumbnail', styleUrls: ['./thumbnail.component.scss'], templateUrl: './thumbnail.component.html', + standalone: true, + imports: [VarDirective, CommonModule, ThemedLoadingComponent, TranslateModule, SafeUrlPipe], }) export class ThumbnailComponent implements OnChanges { /** @@ -46,7 +50,7 @@ export class ThumbnailComponent implements OnChanges { /** * The src attribute used in the template to render the image. */ - src$ = new BehaviorSubject(undefined); + src: string = undefined; retriedWithToken = false; @@ -69,7 +73,7 @@ export class ThumbnailComponent implements OnChanges { * Whether the thumbnail is currently loading * Start out as true to avoid flashing the alt text while a thumbnail is being loaded. */ - isLoading$ = new BehaviorSubject(true); + isLoading = true; constructor( protected auth: AuthService, @@ -122,7 +126,7 @@ export class ThumbnailComponent implements OnChanges { * Otherwise, fall back to the default image or a HTML placeholder */ errorHandler() { - const src = this.src$.getValue(); + const src = this.src; const thumbnail = this.bitstream; const thumbnailSrc = thumbnail?._links?.content?.href; @@ -174,9 +178,9 @@ export class ThumbnailComponent implements OnChanges { * @param src */ setSrc(src: string): void { - this.src$.next(src); + this.src = src; if (src === null) { - this.isLoading$.next(false); + this.isLoading = false; } } @@ -184,6 +188,6 @@ export class ThumbnailComponent implements OnChanges { * Stop the loading animation once the thumbnail is successfully loaded */ successHandler() { - this.isLoading$.next(false); + this.isLoading = false; } } diff --git a/src/app/workflowitems-edit-page/advanced-workflow-action/advanced-workflow-action-page/advanced-workflow-action-page.component.spec.ts b/src/app/workflowitems-edit-page/advanced-workflow-action/advanced-workflow-action-page/advanced-workflow-action-page.component.spec.ts index 4ab935a4e9..d370c83a93 100644 --- a/src/app/workflowitems-edit-page/advanced-workflow-action/advanced-workflow-action-page/advanced-workflow-action-page.component.spec.ts +++ b/src/app/workflowitems-edit-page/advanced-workflow-action/advanced-workflow-action-page/advanced-workflow-action-page.component.spec.ts @@ -5,6 +5,7 @@ import { import { ActivatedRoute } from '@angular/router'; import { TranslateModule } from '@ngx-translate/core'; +import { AdvancedWorkflowActionsLoaderComponent } from '../advanced-workflow-actions-loader/advanced-workflow-actions-loader.component'; import { AdvancedWorkflowActionPageComponent } from './advanced-workflow-action-page.component'; describe('AdvancedWorkflowActionPageComponent', () => { @@ -15,8 +16,6 @@ describe('AdvancedWorkflowActionPageComponent', () => { await TestBed.configureTestingModule({ imports: [ TranslateModule.forRoot(), - ], - declarations: [ AdvancedWorkflowActionPageComponent, ], providers: [ @@ -31,7 +30,10 @@ describe('AdvancedWorkflowActionPageComponent', () => { }, }, ], - }).compileComponents(); + }).overrideComponent(AdvancedWorkflowActionPageComponent, { + remove: { imports: [AdvancedWorkflowActionsLoaderComponent] }, + }) + .compileComponents(); }); beforeEach(() => { diff --git a/src/app/workflowitems-edit-page/advanced-workflow-action/advanced-workflow-action-page/advanced-workflow-action-page.component.ts b/src/app/workflowitems-edit-page/advanced-workflow-action/advanced-workflow-action-page/advanced-workflow-action-page.component.ts index a1e475e57f..91796e826f 100644 --- a/src/app/workflowitems-edit-page/advanced-workflow-action/advanced-workflow-action-page/advanced-workflow-action-page.component.ts +++ b/src/app/workflowitems-edit-page/advanced-workflow-action/advanced-workflow-action-page/advanced-workflow-action-page.component.ts @@ -3,6 +3,9 @@ import { OnInit, } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; +import { TranslateModule } from '@ngx-translate/core'; + +import { AdvancedWorkflowActionsLoaderComponent } from '../advanced-workflow-actions-loader/advanced-workflow-actions-loader.component'; /** * The Advanced Workflow page containing the correct {@link AdvancedWorkflowActionComponent} @@ -12,6 +15,11 @@ import { ActivatedRoute } from '@angular/router'; selector: 'ds-advanced-workflow-action-page', templateUrl: './advanced-workflow-action-page.component.html', styleUrls: ['./advanced-workflow-action-page.component.scss'], + imports: [ + AdvancedWorkflowActionsLoaderComponent, + TranslateModule, + ], + standalone: true, }) export class AdvancedWorkflowActionPageComponent implements OnInit { diff --git a/src/app/workflowitems-edit-page/advanced-workflow-action/advanced-workflow-action-rating/advanced-workflow-action-rating.component.spec.ts b/src/app/workflowitems-edit-page/advanced-workflow-action/advanced-workflow-action-rating/advanced-workflow-action-rating.component.spec.ts index eb148a9f0f..fe45023718 100644 --- a/src/app/workflowitems-edit-page/advanced-workflow-action/advanced-workflow-action-rating/advanced-workflow-action-rating.component.spec.ts +++ b/src/app/workflowitems-edit-page/advanced-workflow-action/advanced-workflow-action-rating/advanced-workflow-action-rating.component.spec.ts @@ -70,8 +70,6 @@ describe('AdvancedWorkflowActionRatingComponent', () => { NgbModule, ReactiveFormsModule, TranslateModule.forRoot(), - ], - declarations: [ AdvancedWorkflowActionRatingComponent, VarDirective, ], diff --git a/src/app/workflowitems-edit-page/advanced-workflow-action/advanced-workflow-action-rating/advanced-workflow-action-rating.component.ts b/src/app/workflowitems-edit-page/advanced-workflow-action/advanced-workflow-action-rating/advanced-workflow-action-rating.component.ts index d92ed73ff3..b8620e7d89 100644 --- a/src/app/workflowitems-edit-page/advanced-workflow-action/advanced-workflow-action-rating/advanced-workflow-action-rating.component.ts +++ b/src/app/workflowitems-edit-page/advanced-workflow-action/advanced-workflow-action-rating/advanced-workflow-action-rating.component.ts @@ -1,16 +1,25 @@ +import { + AsyncPipe, + NgClass, + NgIf, +} from '@angular/common'; import { Component, OnInit, } from '@angular/core'; import { + ReactiveFormsModule, UntypedFormControl, UntypedFormGroup, Validators, } from '@angular/forms'; +import { NgbRatingModule } from '@ng-bootstrap/ng-bootstrap'; +import { TranslateModule } from '@ngx-translate/core'; import { RatingAdvancedWorkflowInfo } from '../../../core/tasks/models/rating-advanced-workflow-info.model'; import { WorkflowAction } from '../../../core/tasks/models/workflow-action-object.model'; -import { rendersAdvancedWorkflowTaskOption } from '../../../shared/mydspace-actions/claimed-task/switcher/claimed-task-actions-decorator'; +import { ModifyItemOverviewComponent } from '../../../item-page/edit-item-page/modify-item-overview/modify-item-overview.component'; +import { VarDirective } from '../../../shared/utils/var.directive'; import { AdvancedWorkflowActionComponent } from '../advanced-workflow-action/advanced-workflow-action.component'; export const ADVANCED_WORKFLOW_TASK_OPTION_RATING = 'submit_score'; @@ -19,12 +28,22 @@ export const ADVANCED_WORKFLOW_ACTION_RATING = 'scorereviewaction'; /** * The page on which reviewers can rate submitted items. */ -@rendersAdvancedWorkflowTaskOption(ADVANCED_WORKFLOW_ACTION_RATING) @Component({ selector: 'ds-advanced-workflow-action-rating-reviewer', templateUrl: './advanced-workflow-action-rating.component.html', styleUrls: ['./advanced-workflow-action-rating.component.scss'], preserveWhitespaces: false, + imports: [ + ModifyItemOverviewComponent, + NgIf, + AsyncPipe, + TranslateModule, + NgbRatingModule, + NgClass, + ReactiveFormsModule, + VarDirective, + ], + standalone: true, }) export class AdvancedWorkflowActionRatingComponent extends AdvancedWorkflowActionComponent implements OnInit { diff --git a/src/app/workflowitems-edit-page/advanced-workflow-action/advanced-workflow-action-select-reviewer/advanced-workflow-action-select-reviewer.component.spec.ts b/src/app/workflowitems-edit-page/advanced-workflow-action/advanced-workflow-action-select-reviewer/advanced-workflow-action-select-reviewer.component.spec.ts index ea6ef0706f..eb79aa390c 100644 --- a/src/app/workflowitems-edit-page/advanced-workflow-action/advanced-workflow-action-select-reviewer/advanced-workflow-action-select-reviewer.component.spec.ts +++ b/src/app/workflowitems-edit-page/advanced-workflow-action/advanced-workflow-action-select-reviewer/advanced-workflow-action-select-reviewer.component.spec.ts @@ -68,8 +68,6 @@ describe('AdvancedWorkflowActionSelectReviewerComponent', () => { await TestBed.configureTestingModule({ imports: [ TranslateModule.forRoot(), - ], - declarations: [ AdvancedWorkflowActionSelectReviewerComponent, ], providers: [ diff --git a/src/app/workflowitems-edit-page/advanced-workflow-action/advanced-workflow-action-select-reviewer/advanced-workflow-action-select-reviewer.component.ts b/src/app/workflowitems-edit-page/advanced-workflow-action/advanced-workflow-action-select-reviewer/advanced-workflow-action-select-reviewer.component.ts index 9d4fd3ff30..72f5623626 100644 --- a/src/app/workflowitems-edit-page/advanced-workflow-action/advanced-workflow-action-select-reviewer/advanced-workflow-action-select-reviewer.component.ts +++ b/src/app/workflowitems-edit-page/advanced-workflow-action/advanced-workflow-action-select-reviewer/advanced-workflow-action-select-reviewer.component.ts @@ -1,4 +1,7 @@ -import { Location } from '@angular/common'; +import { + CommonModule, + Location, +} from '@angular/common'; import { Component, OnDestroy, @@ -9,7 +12,10 @@ import { Params, Router, } from '@angular/router'; -import { TranslateService } from '@ngx-translate/core'; +import { + TranslateModule, + TranslateService, +} from '@ngx-translate/core'; import { Subscription } from 'rxjs'; import { EPersonListActionConfig } from '../../../access-control/group-registry/group-form/members-list/members-list.component'; @@ -21,10 +27,11 @@ import { WorkflowItemDataService } from '../../../core/submission/workflowitem-d import { ClaimedTaskDataService } from '../../../core/tasks/claimed-task-data.service'; import { SelectReviewerAdvancedWorkflowInfo } from '../../../core/tasks/models/select-reviewer-advanced-workflow-info.model'; import { WorkflowAction } from '../../../core/tasks/models/workflow-action-object.model'; +import { ModifyItemOverviewComponent } from '../../../item-page/edit-item-page/modify-item-overview/modify-item-overview.component'; import { hasValue } from '../../../shared/empty.util'; -import { rendersAdvancedWorkflowTaskOption } from '../../../shared/mydspace-actions/claimed-task/switcher/claimed-task-actions-decorator'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { AdvancedWorkflowActionComponent } from '../advanced-workflow-action/advanced-workflow-action.component'; +import { ReviewersListComponent } from './reviewers-list/reviewers-list.component'; export const ADVANCED_WORKFLOW_TASK_OPTION_SELECT_REVIEWER = 'submit_select_reviewer'; export const ADVANCED_WORKFLOW_ACTION_SELECT_REVIEWER = 'selectrevieweraction'; @@ -32,11 +39,17 @@ export const ADVANCED_WORKFLOW_ACTION_SELECT_REVIEWER = 'selectrevieweraction'; /** * The page on which Review Managers can assign Reviewers to review an item. */ -@rendersAdvancedWorkflowTaskOption(ADVANCED_WORKFLOW_ACTION_SELECT_REVIEWER) @Component({ selector: 'ds-advanced-workflow-action-select-reviewer', templateUrl: './advanced-workflow-action-select-reviewer.component.html', styleUrls: ['./advanced-workflow-action-select-reviewer.component.scss'], + imports: [ + CommonModule, + ModifyItemOverviewComponent, + TranslateModule, + ReviewersListComponent, + ], + standalone: true, }) export class AdvancedWorkflowActionSelectReviewerComponent extends AdvancedWorkflowActionComponent implements OnInit, OnDestroy { diff --git a/src/app/workflowitems-edit-page/advanced-workflow-action/advanced-workflow-action-select-reviewer/reviewers-list/reviewers-list.component.spec.ts b/src/app/workflowitems-edit-page/advanced-workflow-action/advanced-workflow-action-select-reviewer/reviewers-list/reviewers-list.component.spec.ts index 7608d3751f..e826de1c0e 100644 --- a/src/app/workflowitems-edit-page/advanced-workflow-action/advanced-workflow-action-select-reviewer/reviewers-list/reviewers-list.component.spec.ts +++ b/src/app/workflowitems-edit-page/advanced-workflow-action/advanced-workflow-action-select-reviewer/reviewers-list/reviewers-list.component.spec.ts @@ -19,7 +19,10 @@ import { BrowserModule, By, } from '@angular/platform-browser'; -import { Router } from '@angular/router'; +import { + ActivatedRoute, + Router, +} from '@angular/router'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { TranslateLoader, @@ -43,15 +46,18 @@ import { EPerson } from '../../../../core/eperson/models/eperson.model'; import { Group } from '../../../../core/eperson/models/group.model'; import { PaginationService } from '../../../../core/pagination/pagination.service'; import { PageInfo } from '../../../../core/shared/page-info.model'; +import { ContextHelpDirective } from '../../../../shared/context-help.directive'; import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service'; import { getMockFormBuilderService } from '../../../../shared/mocks/form-builder-service.mock'; import { RouterMock } from '../../../../shared/mocks/router.mock'; import { getMockTranslateService } from '../../../../shared/mocks/translate.service.mock'; import { NotificationsService } from '../../../../shared/notifications/notifications.service'; +import { PaginationComponent } from '../../../../shared/pagination/pagination.component'; import { createNoContentRemoteDataObject$, createSuccessfulRemoteDataObject$, } from '../../../../shared/remote-data.utils'; +import { ActivatedRouteStub } from '../../../../shared/testing/active-router.stub'; import { EPersonMock, EPersonMock2, @@ -168,9 +174,7 @@ describe('ReviewersListComponent', () => { provide: TranslateLoader, useClass: TranslateLoaderMock, }, - }), - ], - declarations: [ReviewersListComponent], + }), ReviewersListComponent], providers: [ReviewersListComponent, { provide: EPersonDataService, useValue: ePersonDataServiceStub }, { provide: GroupDataService, useValue: groupsDataServiceStub }, @@ -178,9 +182,16 @@ describe('ReviewersListComponent', () => { { provide: FormBuilderService, useValue: builderService }, { provide: Router, useValue: new RouterMock() }, { provide: PaginationService, useValue: paginationService }, + { provide: ActivatedRoute, useValue: new ActivatedRouteStub() }, ], schemas: [NO_ERRORS_SCHEMA], - }).compileComponents(); + }) + .overrideComponent(ReviewersListComponent, { + remove: { + imports: [ContextHelpDirective, PaginationComponent], + }, + }) + .compileComponents(); })); beforeEach(() => { @@ -213,6 +224,38 @@ describe('ReviewersListComponent', () => { })).not.toBeTruthy(); }); }); + + it('should replace the value when a new member is added when multipleReviewers is false', () => { + spyOn(component.selectedReviewersUpdated, 'emit'); + component.multipleReviewers = false; + component.selectedReviewers = [EPersonMock]; + + component.addMemberToGroup(EPersonMock2); + + expect(component.selectedReviewers).toEqual([EPersonMock2]); + expect(component.selectedReviewersUpdated.emit).toHaveBeenCalledWith([EPersonMock2]); + }); + + it('should add the value when a new member is added when multipleReviewers is true', () => { + spyOn(component.selectedReviewersUpdated, 'emit'); + component.multipleReviewers = true; + component.selectedReviewers = [EPersonMock]; + + component.addMemberToGroup(EPersonMock2); + + expect(component.selectedReviewers).toEqual([EPersonMock, EPersonMock2]); + expect(component.selectedReviewersUpdated.emit).toHaveBeenCalledWith([EPersonMock, EPersonMock2]); + }); + + it('should delete the member when present', () => { + spyOn(component.selectedReviewersUpdated, 'emit'); + component.selectedReviewers = [EPersonMock]; + + component.deleteMemberFromGroup(EPersonMock); + + expect(component.selectedReviewers).toEqual([]); + expect(component.selectedReviewersUpdated.emit).toHaveBeenCalledWith([]); + }); }); describe('when a group is selected', () => { @@ -234,37 +277,4 @@ describe('ReviewersListComponent', () => { }); }); - - it('should replace the value when a new member is added when multipleReviewers is false', () => { - spyOn(component.selectedReviewersUpdated, 'emit'); - component.multipleReviewers = false; - component.selectedReviewers = [EPersonMock]; - - component.addMemberToGroup(EPersonMock2); - - expect(component.selectedReviewers).toEqual([EPersonMock2]); - expect(component.selectedReviewersUpdated.emit).toHaveBeenCalledWith([EPersonMock2]); - }); - - it('should add the value when a new member is added when multipleReviewers is true', () => { - spyOn(component.selectedReviewersUpdated, 'emit'); - component.multipleReviewers = true; - component.selectedReviewers = [EPersonMock]; - - component.addMemberToGroup(EPersonMock2); - - expect(component.selectedReviewers).toEqual([EPersonMock, EPersonMock2]); - expect(component.selectedReviewersUpdated.emit).toHaveBeenCalledWith([EPersonMock, EPersonMock2]); - }); - - it('should delete the member when present', () => { - spyOn(component.selectedReviewersUpdated, 'emit'); - component.selectedReviewers = [EPersonMock]; - - component.deleteMemberFromGroup(EPersonMock); - - expect(component.selectedReviewers).toEqual([]); - expect(component.selectedReviewersUpdated.emit).toHaveBeenCalledWith([]); - }); - }); diff --git a/src/app/workflowitems-edit-page/advanced-workflow-action/advanced-workflow-action-select-reviewer/reviewers-list/reviewers-list.component.ts b/src/app/workflowitems-edit-page/advanced-workflow-action/advanced-workflow-action-select-reviewer/reviewers-list/reviewers-list.component.ts index 84841f2fc4..5ae3a13f31 100644 --- a/src/app/workflowitems-edit-page/advanced-workflow-action/advanced-workflow-action-select-reviewer/reviewers-list/reviewers-list.component.ts +++ b/src/app/workflowitems-edit-page/advanced-workflow-action/advanced-workflow-action-select-reviewer/reviewers-list/reviewers-list.component.ts @@ -1,3 +1,9 @@ +import { + AsyncPipe, + NgClass, + NgForOf, + NgIf, +} from '@angular/common'; import { Component, EventEmitter, @@ -8,9 +14,22 @@ import { Output, SimpleChanges, } from '@angular/core'; -import { UntypedFormBuilder } from '@angular/forms'; -import { Router } from '@angular/router'; -import { TranslateService } from '@ngx-translate/core'; +import { + ReactiveFormsModule, + UntypedFormBuilder, +} from '@angular/forms'; +import { + Router, + RouterLink, +} from '@angular/router'; +import { + TranslateModule, + TranslateService, +} from '@ngx-translate/core'; +import { + Observable, + of as observableOf, +} from 'rxjs'; import { EPersonListActionConfig, @@ -21,10 +40,14 @@ import { PaginatedList } from '../../../../core/data/paginated-list.model'; import { EPersonDataService } from '../../../../core/eperson/eperson-data.service'; import { GroupDataService } from '../../../../core/eperson/group-data.service'; import { EPerson } from '../../../../core/eperson/models/eperson.model'; +import { EpersonDtoModel } from '../../../../core/eperson/models/eperson-dto.model'; import { Group } from '../../../../core/eperson/models/group.model'; import { PaginationService } from '../../../../core/pagination/pagination.service'; import { getFirstSucceededRemoteDataPayload } from '../../../../core/shared/operators'; +import { ContextHelpDirective } from '../../../../shared/context-help.directive'; +import { hasValue } from '../../../../shared/empty.util'; import { NotificationsService } from '../../../../shared/notifications/notifications.service'; +import { PaginationComponent } from '../../../../shared/pagination/pagination.component'; /** * Keys to keep track of specific subscriptions @@ -42,20 +65,32 @@ enum SubKey { selector: 'ds-reviewers-list', // templateUrl: './reviewers-list.component.html', templateUrl: '../../../../access-control/group-registry/group-form/members-list/members-list.component.html', + standalone: true, + imports: [ + TranslateModule, + ContextHelpDirective, + ReactiveFormsModule, + PaginationComponent, + NgIf, + AsyncPipe, + RouterLink, + NgClass, + NgForOf, + ], }) export class ReviewersListComponent extends MembersListComponent implements OnInit, OnChanges, OnDestroy { @Input() - groupId: string | null; + groupId: string | null; @Input() - actionConfig: EPersonListActionConfig; + actionConfig: EPersonListActionConfig; @Input() - multipleReviewers: boolean; + multipleReviewers: boolean; @Output() - selectedReviewersUpdated: EventEmitter = new EventEmitter(); + selectedReviewersUpdated: EventEmitter = new EventEmitter(); selectedReviewers: EPerson[] = []; @@ -72,7 +107,7 @@ export class ReviewersListComponent extends MembersListComponent implements OnIn super(groupService, ePersonDataService, translateService, notificationsService, formBuilder, paginationService, router, dsoNameService); } - ngOnInit() { + override ngOnInit(): void { this.searchForm = this.formBuilder.group(({ scope: 'metadata', query: '', @@ -85,6 +120,7 @@ export class ReviewersListComponent extends MembersListComponent implements OnIn if (this.groupId === null) { this.retrieveMembers(this.config.currentPage); } else { + this.unsubFrom(SubKey.ActiveGroup); this.subs.set(SubKey.ActiveGroup, this.groupService.findById(this.groupId).pipe( getFirstSucceededRemoteDataPayload(), ).subscribe((activeGroup: Group) => { @@ -107,25 +143,37 @@ export class ReviewersListComponent extends MembersListComponent implements OnIn retrieveMembers(page: number): void { this.config.currentPage = page; if (this.groupId === null) { - this.unsubFrom(SubKey.Members); - const paginatedListOfEPersons: PaginatedList = new PaginatedList(); - paginatedListOfEPersons.page = this.selectedReviewers; + const paginatedListOfEPersons: PaginatedList = new PaginatedList(); + paginatedListOfEPersons.page = this.selectedReviewers.map((ePerson: EPerson) => Object.assign(new EpersonDtoModel(), { + eperson: ePerson, + ableToDelete: this.isMemberOfGroup(ePerson), + })); this.ePeopleMembersOfGroup.next(paginatedListOfEPersons); } else { super.retrieveMembers(page); } } + /** + * Checks whether the given {@link possibleMember} is part of the {@link selectedReviewers}. + * + * @param possibleMember The {@link EPerson} that needs to be checked + */ + isMemberOfGroup(possibleMember: EPerson): Observable { + return observableOf(hasValue(this.selectedReviewers.find((reviewer: EPerson) => reviewer.id === possibleMember.id))); + } + /** * Removes the {@link eperson} from the {@link selectedReviewers} * * @param eperson The {@link EPerson} to remove */ deleteMemberFromGroup(eperson: EPerson) { - const index = this.selectedReviewers.indexOf(eperson); + const index = this.selectedReviewers.findIndex((reviewer: EPerson) => reviewer.id === eperson.id); if (index !== -1) { this.selectedReviewers.splice(index, 1); } + this.retrieveMembers(this.config.currentPage); this.selectedReviewersUpdated.emit(this.selectedReviewers); } @@ -140,6 +188,7 @@ export class ReviewersListComponent extends MembersListComponent implements OnIn this.selectedReviewers = []; } this.selectedReviewers.push(eperson); + this.retrieveMembers(this.config.currentPage); this.selectedReviewersUpdated.emit(this.selectedReviewers); } diff --git a/src/app/workflowitems-edit-page/advanced-workflow-action/advanced-workflow-action/advanced-workflow-action.component.spec.ts b/src/app/workflowitems-edit-page/advanced-workflow-action/advanced-workflow-action/advanced-workflow-action.component.spec.ts index f63fb0e97e..ab1494341e 100644 --- a/src/app/workflowitems-edit-page/advanced-workflow-action/advanced-workflow-action/advanced-workflow-action.component.spec.ts +++ b/src/app/workflowitems-edit-page/advanced-workflow-action/advanced-workflow-action/advanced-workflow-action.component.spec.ts @@ -19,12 +19,12 @@ import { ProcessTaskResponse } from '../../../core/tasks/models/process-task-res import { DSOSelectorComponent } from '../../../shared/dso-selector/dso-selector/dso-selector.component'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { ClaimedTaskDataServiceStub } from '../../../shared/testing/claimed-task-data-service.stub'; -import { LocationStub } from '../../../shared/testing/location.stub'; import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub'; import { RequestServiceStub } from '../../../shared/testing/request-service.stub'; import { routeServiceStub } from '../../../shared/testing/route-service.stub'; import { WorkflowActionDataServiceStub } from '../../../shared/testing/workflow-action-data-service.stub'; import { WorkflowItemDataServiceStub } from '../../../shared/testing/workflow-item-data-service.stub'; +import { WorkflowItemActionPageDirective } from '../../workflow-item-action-page.component'; import { AdvancedWorkflowActionComponent } from './advanced-workflow-action.component'; const workflowId = '1'; @@ -34,25 +34,24 @@ describe('AdvancedWorkflowActionComponent', () => { let fixture: ComponentFixture; let claimedTaskDataService: ClaimedTaskDataServiceStub; - let location: LocationStub; let notificationService: NotificationsServiceStub; let workflowActionDataService: WorkflowActionDataServiceStub; let workflowItemDataService: WorkflowItemDataServiceStub; + let mockLocation; beforeEach(async () => { claimedTaskDataService = new ClaimedTaskDataServiceStub(); - location = new LocationStub(); notificationService = new NotificationsServiceStub(); workflowActionDataService = new WorkflowActionDataServiceStub(); workflowItemDataService = new WorkflowItemDataServiceStub(); + mockLocation = jasmine.createSpyObj(['getState']); await TestBed.configureTestingModule({ imports: [ TranslateModule.forRoot(), RouterTestingModule, - ], - declarations: [ TestComponent, + WorkflowItemActionPageDirective, MockComponent(DSOSelectorComponent), ], providers: [ @@ -70,14 +69,15 @@ describe('AdvancedWorkflowActionComponent', () => { }, }, { provide: ClaimedTaskDataService, useValue: claimedTaskDataService }, - { provide: Location, useValue: location }, + { provide: Location, useValue: mockLocation }, { provide: NotificationsService, useValue: notificationService }, { provide: RouteService, useValue: routeServiceStub }, { provide: WorkflowActionDataService, useValue: workflowActionDataService }, { provide: WorkflowItemDataService, useValue: workflowItemDataService }, { provide: RequestService, useClass: RequestServiceStub }, ], - }).compileComponents(); + }) + .compileComponents(); }); beforeEach(() => { @@ -122,6 +122,8 @@ describe('AdvancedWorkflowActionComponent', () => { // eslint-disable-next-line @angular-eslint/component-selector selector: '', template: '', + standalone: true, + imports: [RouterTestingModule], }) class TestComponent extends AdvancedWorkflowActionComponent { diff --git a/src/app/workflowitems-edit-page/advanced-workflow-action/advanced-workflow-action/advanced-workflow-action.component.ts b/src/app/workflowitems-edit-page/advanced-workflow-action/advanced-workflow-action/advanced-workflow-action.component.ts index 7635020ebf..a12a40b52a 100644 --- a/src/app/workflowitems-edit-page/advanced-workflow-action/advanced-workflow-action/advanced-workflow-action.component.ts +++ b/src/app/workflowitems-edit-page/advanced-workflow-action/advanced-workflow-action/advanced-workflow-action.component.ts @@ -20,7 +20,7 @@ import { ClaimedTaskDataService } from '../../../core/tasks/claimed-task-data.se import { ProcessTaskResponse } from '../../../core/tasks/models/process-task-response'; import { WorkflowAction } from '../../../core/tasks/models/workflow-action-object.model'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; -import { WorkflowItemActionPageComponent } from '../../workflow-item-action-page.component'; +import { WorkflowItemActionPageDirective } from '../../workflow-item-action-page.component'; /** * Abstract component for rendering an advanced claimed task's workflow page @@ -32,7 +32,7 @@ import { WorkflowItemActionPageComponent } from '../../workflow-item-action-page selector: 'ds-advanced-workflow-action', template: '', }) -export abstract class AdvancedWorkflowActionComponent extends WorkflowItemActionPageComponent implements OnInit { +export abstract class AdvancedWorkflowActionComponent extends WorkflowItemActionPageDirective implements OnInit { workflowAction$: Observable; diff --git a/src/app/workflowitems-edit-page/advanced-workflow-action/advanced-workflow-actions-loader/advanced-workflow-actions-loader.component.spec.ts b/src/app/workflowitems-edit-page/advanced-workflow-action/advanced-workflow-actions-loader/advanced-workflow-actions-loader.component.spec.ts index df08d3e8ac..54aae05f76 100644 --- a/src/app/workflowitems-edit-page/advanced-workflow-action/advanced-workflow-actions-loader/advanced-workflow-actions-loader.component.spec.ts +++ b/src/app/workflowitems-edit-page/advanced-workflow-action/advanced-workflow-actions-loader/advanced-workflow-actions-loader.component.spec.ts @@ -1,6 +1,12 @@ +/* eslint-disable max-classes-per-file */ import { ChangeDetectionStrategy, Component, + ComponentFactoryResolver, + Directive, + Injector, + NO_ERRORS_SCHEMA, + ViewContainerRef, } from '@angular/core'; import { ComponentFixture, @@ -8,6 +14,8 @@ import { } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { Router } from '@angular/router'; +import { RouterTestingModule } from '@angular/router/testing'; +import { TranslateModule } from '@ngx-translate/core'; import { getMockThemeService } from 'src/app/shared/mocks/theme-service.mock'; import { ThemeService } from 'src/app/shared/theme-support/theme.service'; @@ -24,25 +32,37 @@ describe('AdvancedWorkflowActionsLoaderComponent', () => { let fixture: ComponentFixture; let router: RouterStub; + let mockComponentFactoryResolver: any; let themeService: ThemeService; beforeEach(async () => { router = new RouterStub(); + mockComponentFactoryResolver = { + resolveComponentFactory: jasmine.createSpy('resolveComponentFactory').and.returnValue( + AdvancedWorkflowActionTestComponent, + ), + }; themeService = getMockThemeService(); - await TestBed.configureTestingModule({ - declarations: [ + TestBed.configureTestingModule({ + imports: [ + TranslateModule.forRoot(), + RouterTestingModule, DynamicComponentLoaderDirective, AdvancedWorkflowActionsLoaderComponent, + AdvancedWorkflowActionTestComponent, ], providers: [ { provide: Router, useValue: router }, + { provide: ComponentFactoryResolver, useValue: mockComponentFactoryResolver }, + { provide: Injector, useValue: {} }, + ViewContainerRef, { provide: ThemeService, useValue: themeService }, ], + schemas: [NO_ERRORS_SCHEMA], }).overrideComponent(AdvancedWorkflowActionsLoaderComponent, { set: { changeDetection: ChangeDetectionStrategy.Default, - entryComponents: [AdvancedWorkflowActionTestComponent], }, }).compileComponents(); }); @@ -88,6 +108,15 @@ describe('AdvancedWorkflowActionsLoaderComponent', () => { // eslint-disable-next-line @angular-eslint/component-selector selector: '', template: '', + standalone: true, }) class AdvancedWorkflowActionTestComponent { } + +@Directive({ + selector: '[dsAdvancedWorkflowActions]', + standalone: true, +}) +export class MockAdvancedWorkflowActionsDirective { + constructor(public viewContainerRef: ViewContainerRef) {} +} diff --git a/src/app/workflowitems-edit-page/advanced-workflow-action/advanced-workflow-actions-loader/advanced-workflow-actions-loader.component.ts b/src/app/workflowitems-edit-page/advanced-workflow-action/advanced-workflow-actions-loader/advanced-workflow-actions-loader.component.ts index e196f1417c..799e06d863 100644 --- a/src/app/workflowitems-edit-page/advanced-workflow-action/advanced-workflow-actions-loader/advanced-workflow-actions-loader.component.ts +++ b/src/app/workflowitems-edit-page/advanced-workflow-action/advanced-workflow-actions-loader/advanced-workflow-actions-loader.component.ts @@ -18,6 +18,7 @@ import { ThemeService } from '../../../shared/theme-support/theme.service'; @Component({ selector: 'ds-advanced-workflow-actions-loader', templateUrl: '../../../shared/abstract-component-loader/abstract-component-loader.component.html', + standalone: true, }) export class AdvancedWorkflowActionsLoaderComponent extends AbstractComponentLoaderComponent implements OnInit { @@ -48,7 +49,7 @@ export class AdvancedWorkflowActionsLoaderComponent extends AbstractComponentLoa } public getComponent(): GenericConstructor { - return getAdvancedComponentByWorkflowTaskOption(this.type); + return getAdvancedComponentByWorkflowTaskOption(this.type) as GenericConstructor; } } diff --git a/src/app/workflowitems-edit-page/item-from-workflow-breadcrumb.resolver.ts b/src/app/workflowitems-edit-page/item-from-workflow-breadcrumb.resolver.ts new file mode 100644 index 0000000000..1c29d6b861 --- /dev/null +++ b/src/app/workflowitems-edit-page/item-from-workflow-breadcrumb.resolver.ts @@ -0,0 +1,25 @@ +import { Injectable } from '@angular/core'; +import { Resolve } from '@angular/router'; + +import { BreadcrumbConfig } from '../breadcrumbs/breadcrumb/breadcrumb-config.model'; +import { SubmissionObject } from '../core/submission/models/submission-object.model'; +import { SubmissionParentBreadcrumbResolver } from '../core/submission/resolver/submission-parent-breadcrumb.resolver'; +import { SubmissionParentBreadcrumbsService } from '../core/submission/submission-parent-breadcrumb.service'; +import { WorkflowItemDataService } from '../core/submission/workflowitem-data.service'; + +/** + * This class represents a resolver that retrieves the breadcrumbs of the workflow item + */ +@Injectable({ + providedIn: 'root', +}) +export class ItemFromWorkflowBreadcrumbResolver extends SubmissionParentBreadcrumbResolver implements Resolve> { + + constructor( + protected dataService: WorkflowItemDataService, + protected breadcrumbService: SubmissionParentBreadcrumbsService, + ) { + super(dataService, breadcrumbService); + } + +} diff --git a/src/app/workflowitems-edit-page/item-from-workflow.resolver.spec.ts b/src/app/workflowitems-edit-page/item-from-workflow.resolver.spec.ts index 5bf6aec2ff..9ee8eaed06 100644 --- a/src/app/workflowitems-edit-page/item-from-workflow.resolver.spec.ts +++ b/src/app/workflowitems-edit-page/item-from-workflow.resolver.spec.ts @@ -2,11 +2,11 @@ import { first } from 'rxjs/operators'; import { WorkflowItemDataService } from '../core/submission/workflowitem-data.service'; import { createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils'; -import { ItemFromWorkflowResolver } from './item-from-workflow.resolver'; +import { itemFromWorkflowResolver } from './item-from-workflow.resolver'; -describe('ItemFromWorkflowResolver', () => { +describe('itemFromWorkflowResolver', () => { describe('resolve', () => { - let resolver: ItemFromWorkflowResolver; + let resolver: any; let wfiService: WorkflowItemDataService; const uuid = '1234-65487-12354-1235'; const itemUuid = '8888-8888-8888-8888'; @@ -20,11 +20,11 @@ describe('ItemFromWorkflowResolver', () => { wfiService = { findById: (id: string) => createSuccessfulRemoteDataObject$(wfi), } as any; - resolver = new ItemFromWorkflowResolver(wfiService, null); + resolver = itemFromWorkflowResolver; }); it('should resolve a an item from from the workflow item with the correct id', (done) => { - resolver.resolve({ params: { id: uuid } } as any, undefined) + resolver({ params: { id: uuid } } as any, undefined, wfiService) .pipe(first()) .subscribe( (resolved) => { diff --git a/src/app/workflowitems-edit-page/item-from-workflow.resolver.ts b/src/app/workflowitems-edit-page/item-from-workflow.resolver.ts index 237a6ad945..e76a147f52 100644 --- a/src/app/workflowitems-edit-page/item-from-workflow.resolver.ts +++ b/src/app/workflowitems-edit-page/item-from-workflow.resolver.ts @@ -1,22 +1,21 @@ -import { Injectable } from '@angular/core'; -import { Resolve } from '@angular/router'; -import { Store } from '@ngrx/store'; +import { inject } from '@angular/core'; +import { + ActivatedRouteSnapshot, + ResolveFn, + RouterStateSnapshot, +} from '@angular/router'; +import { Observable } from 'rxjs'; import { RemoteData } from '../core/data/remote-data'; import { Item } from '../core/shared/item.model'; import { SubmissionObjectResolver } from '../core/submission/resolver/submission-object.resolver'; import { WorkflowItemDataService } from '../core/submission/workflowitem-data.service'; -/** - * This class represents a resolver that requests a specific item before the route is activated - */ -@Injectable() -export class ItemFromWorkflowResolver extends SubmissionObjectResolver implements Resolve> { - constructor( - private workflowItemService: WorkflowItemDataService, - protected store: Store, - ) { - super(workflowItemService, store); - } +export const itemFromWorkflowResolver: ResolveFn> = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + workflowItemService: WorkflowItemDataService = inject(WorkflowItemDataService), +): Observable> => { + return SubmissionObjectResolver(route, state, workflowItemService); +}; -} diff --git a/src/app/workflowitems-edit-page/workflow-item-action-page.component.spec.ts b/src/app/workflowitems-edit-page/workflow-item-action-page.component.spec.ts index d775823746..64fb97147b 100644 --- a/src/app/workflowitems-edit-page/workflow-item-action-page.component.spec.ts +++ b/src/app/workflowitems-edit-page/workflow-item-action-page.component.spec.ts @@ -1,4 +1,7 @@ -import { Location } from '@angular/common'; +import { + CommonModule, + Location, +} from '@angular/common'; import { Component, NO_ERRORS_SCHEMA, @@ -27,6 +30,7 @@ import { RequestService } from '../core/data/request.service'; import { RouteService } from '../core/services/route.service'; import { WorkflowItem } from '../core/submission/models/workflowitem.model'; import { WorkflowItemDataService } from '../core/submission/workflowitem-data.service'; +import { ModifyItemOverviewComponent } from '../item-page/edit-item-page/modify-item-overview/modify-item-overview.component'; import { TranslateLoaderMock } from '../shared/mocks/translate-loader.mock'; import { NotificationsService } from '../shared/notifications/notifications.service'; import { @@ -39,12 +43,12 @@ import { NotificationsServiceStub } from '../shared/testing/notifications-servic import { RequestServiceStub } from '../shared/testing/request-service.stub'; import { RouterStub } from '../shared/testing/router.stub'; import { VarDirective } from '../shared/utils/var.directive'; -import { WorkflowItemActionPageComponent } from './workflow-item-action-page.component'; +import { WorkflowItemActionPageDirective } from './workflow-item-action-page.component'; const type = 'testType'; describe('WorkflowItemActionPageComponent', () => { - let component: WorkflowItemActionPageComponent; - let fixture: ComponentFixture; + let component: WorkflowItemActionPageDirective; + let fixture: ComponentFixture; let wfiService; let wfi; let itemRD$; @@ -68,8 +72,7 @@ describe('WorkflowItemActionPageComponent', () => { provide: TranslateLoader, useClass: TranslateLoaderMock, }, - })], - declarations: [TestComponent, VarDirective], + }), TestComponent, VarDirective], providers: [ { provide: ActivatedRoute, useValue: new ActivatedRouteStub({}, { wfi: createSuccessfulRemoteDataObject(wfi) }) }, { provide: Router, useClass: RouterStub }, @@ -128,9 +131,10 @@ describe('WorkflowItemActionPageComponent', () => { @Component({ selector: 'ds-workflow-item-test-action-page', templateUrl: 'workflow-item-action-page.component.html', -}, -) -class TestComponent extends WorkflowItemActionPageComponent { + imports: [VarDirective, TranslateModule, CommonModule, ModifyItemOverviewComponent], + standalone: true, +}) +class TestComponent extends WorkflowItemActionPageDirective { constructor(protected route: ActivatedRoute, protected workflowItemService: WorkflowItemDataService, protected router: Router, diff --git a/src/app/workflowitems-edit-page/workflow-item-action-page.component.ts b/src/app/workflowitems-edit-page/workflow-item-action-page.component.ts index d245fc3082..f187d26b99 100644 --- a/src/app/workflowitems-edit-page/workflow-item-action-page.component.ts +++ b/src/app/workflowitems-edit-page/workflow-item-action-page.component.ts @@ -1,6 +1,7 @@ import { Location } from '@angular/common'; import { - Component, + Directive, + Input, OnInit, } from '@angular/core'; import { @@ -36,12 +37,15 @@ import { NotificationsService } from '../shared/notifications/notifications.serv /** * Abstract component representing a page to perform an action on a workflow item */ -@Component({ +@Directive({ + // eslint-disable-next-line @angular-eslint/directive-selector selector: 'ds-workflowitem-action-page', - template: '', + standalone: true, }) -export abstract class WorkflowItemActionPageComponent implements OnInit { - public type; +export abstract class WorkflowItemActionPageDirective implements OnInit { + + @Input() type: string; + public wfi$: Observable; public item$: Observable; protected previousQueryParameters?: Params; @@ -64,7 +68,7 @@ export abstract class WorkflowItemActionPageComponent implements OnInit { this.type = this.getType(); this.wfi$ = this.route.data.pipe(map((data: Data) => data.wfi as RemoteData), getRemoteDataPayload()); this.item$ = this.wfi$.pipe(switchMap((wfi: WorkflowItem) => (wfi.item as Observable>).pipe(getAllSucceededRemoteData(), getRemoteDataPayload()))); - this.previousQueryParameters = (this.location.getState() as { [key: string]: any }).previousQueryParams; + this.previousQueryParameters = (this.location.getState() as { [key: string]: any })?.previousQueryParams; } /** diff --git a/src/app/workflowitems-edit-page/workflow-item-delete/themed-workflow-item-delete.component.ts b/src/app/workflowitems-edit-page/workflow-item-delete/themed-workflow-item-delete.component.ts index c2f0abbde4..39180d1c7b 100644 --- a/src/app/workflowitems-edit-page/workflow-item-delete/themed-workflow-item-delete.component.ts +++ b/src/app/workflowitems-edit-page/workflow-item-delete/themed-workflow-item-delete.component.ts @@ -8,9 +8,11 @@ import { WorkflowItemDeleteComponent } from './workflow-item-delete.component'; */ @Component({ - selector: 'ds-themed-workflow-item-delete', + selector: 'ds-workflow-item-delete', styleUrls: [], templateUrl: './../../shared/theme-support/themed.component.html', + standalone: true, + imports: [WorkflowItemDeleteComponent], }) export class ThemedWorkflowItemDeleteComponent extends ThemedComponent { protected getComponentName(): string { diff --git a/src/app/workflowitems-edit-page/workflow-item-delete/workflow-item-delete.component.spec.ts b/src/app/workflowitems-edit-page/workflow-item-delete/workflow-item-delete.component.spec.ts index 0b89452694..801113c4ce 100644 --- a/src/app/workflowitems-edit-page/workflow-item-delete/workflow-item-delete.component.spec.ts +++ b/src/app/workflowitems-edit-page/workflow-item-delete/workflow-item-delete.component.spec.ts @@ -59,8 +59,7 @@ describe('WorkflowItemDeleteComponent', () => { provide: TranslateLoader, useClass: TranslateLoaderMock, }, - })], - declarations: [WorkflowItemDeleteComponent, VarDirective], + }), WorkflowItemDeleteComponent, VarDirective], providers: [ { provide: ActivatedRoute, useValue: new ActivatedRouteStub({}, { wfi: createSuccessfulRemoteDataObject(wfi) }) }, { provide: Router, useClass: RouterStub }, diff --git a/src/app/workflowitems-edit-page/workflow-item-delete/workflow-item-delete.component.ts b/src/app/workflowitems-edit-page/workflow-item-delete/workflow-item-delete.component.ts index 516d57e1a4..0352eba098 100644 --- a/src/app/workflowitems-edit-page/workflow-item-delete/workflow-item-delete.component.ts +++ b/src/app/workflowitems-edit-page/workflow-item-delete/workflow-item-delete.component.ts @@ -1,10 +1,16 @@ -import { Location } from '@angular/common'; +import { + CommonModule, + Location, +} from '@angular/common'; import { Component } from '@angular/core'; import { ActivatedRoute, Router, } from '@angular/router'; -import { TranslateService } from '@ngx-translate/core'; +import { + TranslateModule, + TranslateService, +} from '@ngx-translate/core'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; @@ -14,17 +20,21 @@ import { RouteService } from '../../core/services/route.service'; import { NoContent } from '../../core/shared/NoContent.model'; import { getFirstCompletedRemoteData } from '../../core/shared/operators'; import { WorkflowItemDataService } from '../../core/submission/workflowitem-data.service'; +import { ModifyItemOverviewComponent } from '../../item-page/edit-item-page/modify-item-overview/modify-item-overview.component'; import { NotificationsService } from '../../shared/notifications/notifications.service'; -import { WorkflowItemActionPageComponent } from '../workflow-item-action-page.component'; +import { VarDirective } from '../../shared/utils/var.directive'; +import { WorkflowItemActionPageDirective } from '../workflow-item-action-page.component'; @Component({ - selector: 'ds-workflow-item-delete', + selector: 'ds-base-workflow-item-delete', templateUrl: '../workflow-item-action-page.component.html', + standalone: true, + imports: [VarDirective, TranslateModule, CommonModule, ModifyItemOverviewComponent], }) /** * Component representing a page to delete a workflow item */ -export class WorkflowItemDeleteComponent extends WorkflowItemActionPageComponent { +export class WorkflowItemDeleteComponent extends WorkflowItemActionPageDirective { constructor(protected route: ActivatedRoute, protected workflowItemService: WorkflowItemDataService, protected router: Router, diff --git a/src/app/workflowitems-edit-page/workflow-item-page.resolver.spec.ts b/src/app/workflowitems-edit-page/workflow-item-page.resolver.spec.ts index 21f9e6e00a..02754776a9 100644 --- a/src/app/workflowitems-edit-page/workflow-item-page.resolver.spec.ts +++ b/src/app/workflowitems-edit-page/workflow-item-page.resolver.spec.ts @@ -2,11 +2,11 @@ import { first } from 'rxjs/operators'; import { WorkflowItemDataService } from '../core/submission/workflowitem-data.service'; import { createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils'; -import { WorkflowItemPageResolver } from './workflow-item-page.resolver'; +import { workflowItemPageResolver } from './workflow-item-page.resolver'; -describe('WorkflowItemPageResolver', () => { +describe('workflowItemPageResolver', () => { describe('resolve', () => { - let resolver: WorkflowItemPageResolver; + let resolver: any; let wfiService: WorkflowItemDataService; const uuid = '1234-65487-12354-1235'; @@ -14,11 +14,11 @@ describe('WorkflowItemPageResolver', () => { wfiService = { findById: (id: string) => createSuccessfulRemoteDataObject$({ id }), } as any; - resolver = new WorkflowItemPageResolver(wfiService); + resolver = workflowItemPageResolver; }); it('should resolve a workflow item with the correct id', (done) => { - resolver.resolve({ params: { id: uuid } } as any, undefined) + resolver({ params: { id: uuid } } as any, undefined, wfiService) .pipe(first()) .subscribe( (resolved) => { diff --git a/src/app/workflowitems-edit-page/workflow-item-page.resolver.ts b/src/app/workflowitems-edit-page/workflow-item-page.resolver.ts index c5c2d4d717..d0f49d5700 100644 --- a/src/app/workflowitems-edit-page/workflow-item-page.resolver.ts +++ b/src/app/workflowitems-edit-page/workflow-item-page.resolver.ts @@ -1,7 +1,7 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, - Resolve, + ResolveFn, RouterStateSnapshot, } from '@angular/router'; import { Observable } from 'rxjs'; @@ -12,28 +12,17 @@ import { WorkflowItem } from '../core/submission/models/workflowitem.model'; import { WorkflowItemDataService } from '../core/submission/workflowitem-data.service'; import { followLink } from '../shared/utils/follow-link-config.model'; -/** - * This class represents a resolver that requests a specific workflow item before the route is activated - */ -@Injectable() -export class WorkflowItemPageResolver implements Resolve> { - constructor(private workflowItemService: WorkflowItemDataService) { - } - - /** - * Method for resolving a workflow item based on the parameters in the current route - * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot - * @param {RouterStateSnapshot} state The current RouterStateSnapshot - * @returns Observable<> Emits the found workflow item based on the parameters in the current route, - * or an error if something went wrong - */ - resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable> { - return this.workflowItemService.findById(route.params.id, - true, - false, - followLink('item'), - ).pipe( - getFirstCompletedRemoteData(), - ); - } -} +export const workflowItemPageResolver: ResolveFn> = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + workflowItemService: WorkflowItemDataService = inject(WorkflowItemDataService), +): Observable> => { + return workflowItemService.findById( + route.params.id, + true, + false, + followLink('item'), + ).pipe( + getFirstCompletedRemoteData(), + ); +}; diff --git a/src/app/workflowitems-edit-page/workflow-item-send-back/themed-workflow-item-send-back.component.ts b/src/app/workflowitems-edit-page/workflow-item-send-back/themed-workflow-item-send-back.component.ts index 2a6054f691..72edd8147e 100644 --- a/src/app/workflowitems-edit-page/workflow-item-send-back/themed-workflow-item-send-back.component.ts +++ b/src/app/workflowitems-edit-page/workflow-item-send-back/themed-workflow-item-send-back.component.ts @@ -8,9 +8,11 @@ import { WorkflowItemSendBackComponent } from './workflow-item-send-back.compone */ @Component({ - selector: 'ds-themed-workflow-item-send-back', + selector: 'ds-workflow-item-send-back', styleUrls: [], templateUrl: './../../shared/theme-support/themed.component.html', + standalone: true, + imports: [WorkflowItemSendBackComponent], }) export class ThemedWorkflowItemSendBackComponent extends ThemedComponent { protected getComponentName(): string { diff --git a/src/app/workflowitems-edit-page/workflow-item-send-back/workflow-item-send-back.component.spec.ts b/src/app/workflowitems-edit-page/workflow-item-send-back/workflow-item-send-back.component.spec.ts index 95f90b34dd..2a25bc0cc6 100644 --- a/src/app/workflowitems-edit-page/workflow-item-send-back/workflow-item-send-back.component.spec.ts +++ b/src/app/workflowitems-edit-page/workflow-item-send-back/workflow-item-send-back.component.spec.ts @@ -59,8 +59,7 @@ describe('WorkflowItemSendBackComponent', () => { provide: TranslateLoader, useClass: TranslateLoaderMock, }, - })], - declarations: [WorkflowItemSendBackComponent, VarDirective], + }), WorkflowItemSendBackComponent, VarDirective], providers: [ { provide: ActivatedRoute, useValue: new ActivatedRouteStub({}, { wfi: createSuccessfulRemoteDataObject(wfi) }) }, { provide: Router, useClass: RouterStub }, diff --git a/src/app/workflowitems-edit-page/workflow-item-send-back/workflow-item-send-back.component.ts b/src/app/workflowitems-edit-page/workflow-item-send-back/workflow-item-send-back.component.ts index 651924787f..ad0b1d91a8 100644 --- a/src/app/workflowitems-edit-page/workflow-item-send-back/workflow-item-send-back.component.ts +++ b/src/app/workflowitems-edit-page/workflow-item-send-back/workflow-item-send-back.component.ts @@ -1,26 +1,36 @@ -import { Location } from '@angular/common'; +import { + CommonModule, + Location, +} from '@angular/common'; import { Component } from '@angular/core'; import { ActivatedRoute, Router, } from '@angular/router'; -import { TranslateService } from '@ngx-translate/core'; +import { + TranslateModule, + TranslateService, +} from '@ngx-translate/core'; import { Observable } from 'rxjs'; import { RequestService } from '../../core/data/request.service'; import { RouteService } from '../../core/services/route.service'; import { WorkflowItemDataService } from '../../core/submission/workflowitem-data.service'; +import { ModifyItemOverviewComponent } from '../../item-page/edit-item-page/modify-item-overview/modify-item-overview.component'; import { NotificationsService } from '../../shared/notifications/notifications.service'; -import { WorkflowItemActionPageComponent } from '../workflow-item-action-page.component'; +import { VarDirective } from '../../shared/utils/var.directive'; +import { WorkflowItemActionPageDirective } from '../workflow-item-action-page.component'; @Component({ - selector: 'ds-workflow-item-send-back', + selector: 'ds-base-workflow-item-send-back', templateUrl: '../workflow-item-action-page.component.html', + standalone: true, + imports: [VarDirective, TranslateModule, CommonModule, ModifyItemOverviewComponent], }) /** * Component representing a page to send back a workflow item to the submitter */ -export class WorkflowItemSendBackComponent extends WorkflowItemActionPageComponent { +export class WorkflowItemSendBackComponent extends WorkflowItemActionPageDirective { constructor(protected route: ActivatedRoute, protected workflowItemService: WorkflowItemDataService, protected router: Router, diff --git a/src/app/workflowitems-edit-page/workflowitems-edit-page-routes.ts b/src/app/workflowitems-edit-page/workflowitems-edit-page-routes.ts new file mode 100644 index 0000000000..4bc074c425 --- /dev/null +++ b/src/app/workflowitems-edit-page/workflowitems-edit-page-routes.ts @@ -0,0 +1,81 @@ +import { Routes } from '@angular/router'; + +import { authenticatedGuard } from '../core/auth/authenticated.guard'; +import { i18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver'; +import { ThemedFullItemPageComponent } from '../item-page/full/themed-full-item-page.component'; +import { ThemedSubmissionEditComponent } from '../submission/edit/themed-submission-edit.component'; +import { AdvancedWorkflowActionPageComponent } from './advanced-workflow-action/advanced-workflow-action-page/advanced-workflow-action-page.component'; +import { itemFromWorkflowResolver } from './item-from-workflow.resolver'; +import { ItemFromWorkflowBreadcrumbResolver } from './item-from-workflow-breadcrumb.resolver'; +import { ThemedWorkflowItemDeleteComponent } from './workflow-item-delete/themed-workflow-item-delete.component'; +import { workflowItemPageResolver } from './workflow-item-page.resolver'; +import { ThemedWorkflowItemSendBackComponent } from './workflow-item-send-back/themed-workflow-item-send-back.component'; +import { + ADVANCED_WORKFLOW_PATH, + WORKFLOW_ITEM_DELETE_PATH, + WORKFLOW_ITEM_EDIT_PATH, + WORKFLOW_ITEM_SEND_BACK_PATH, + WORKFLOW_ITEM_VIEW_PATH, +} from './workflowitems-edit-page-routing-paths'; + +export const ROUTES: Routes = [ + { + path: ':id', + resolve: { + breadcrumb: ItemFromWorkflowBreadcrumbResolver, + wfi: workflowItemPageResolver, + }, + children: [ + { + canActivate: [authenticatedGuard], + path: WORKFLOW_ITEM_EDIT_PATH, + component: ThemedSubmissionEditComponent, + resolve: { + breadcrumb: i18nBreadcrumbResolver, + }, + data: { + title: 'workflow-item.edit.title', + breadcrumbKey: 'workflow-item.edit', + collectionModifiable: false, + }, + }, + { + canActivate: [authenticatedGuard], + path: WORKFLOW_ITEM_VIEW_PATH, + component: ThemedFullItemPageComponent, + resolve: { + dso: itemFromWorkflowResolver, + breadcrumb: i18nBreadcrumbResolver, + }, + data: { title: 'workflow-item.view.title', breadcrumbKey: 'workflow-item.view' }, + }, + { + canActivate: [authenticatedGuard], + path: WORKFLOW_ITEM_DELETE_PATH, + component: ThemedWorkflowItemDeleteComponent, + resolve: { + breadcrumb: i18nBreadcrumbResolver, + }, + data: { title: 'workflow-item.delete.title', breadcrumbKey: 'workflow-item.edit' }, + }, + { + canActivate: [authenticatedGuard], + path: WORKFLOW_ITEM_SEND_BACK_PATH, + component: ThemedWorkflowItemSendBackComponent, + resolve: { + breadcrumb: i18nBreadcrumbResolver, + }, + data: { title: 'workflow-item.send-back.title', breadcrumbKey: 'workflow-item.edit' }, + }, + { + canActivate: [authenticatedGuard], + path: ADVANCED_WORKFLOW_PATH, + component: AdvancedWorkflowActionPageComponent, + resolve: { + breadcrumb: i18nBreadcrumbResolver, + }, + data: { title: 'workflow-item.advanced.title', breadcrumbKey: 'workflow-item.edit' }, + }, + ], + }, +]; diff --git a/src/app/workflowitems-edit-page/workflowitems-edit-page-routing.module.ts b/src/app/workflowitems-edit-page/workflowitems-edit-page-routing.module.ts deleted file mode 100644 index a5e0a261b0..0000000000 --- a/src/app/workflowitems-edit-page/workflowitems-edit-page-routing.module.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { NgModule } from '@angular/core'; -import { RouterModule } from '@angular/router'; - -import { AuthenticatedGuard } from '../core/auth/authenticated.guard'; -import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver'; -import { ThemedFullItemPageComponent } from '../item-page/full/themed-full-item-page.component'; -import { ThemedSubmissionEditComponent } from '../submission/edit/themed-submission-edit.component'; -import { AdvancedWorkflowActionPageComponent } from './advanced-workflow-action/advanced-workflow-action-page/advanced-workflow-action-page.component'; -import { ItemFromWorkflowResolver } from './item-from-workflow.resolver'; -import { ThemedWorkflowItemDeleteComponent } from './workflow-item-delete/themed-workflow-item-delete.component'; -import { WorkflowItemPageResolver } from './workflow-item-page.resolver'; -import { ThemedWorkflowItemSendBackComponent } from './workflow-item-send-back/themed-workflow-item-send-back.component'; -import { - ADVANCED_WORKFLOW_PATH, - WORKFLOW_ITEM_DELETE_PATH, - WORKFLOW_ITEM_EDIT_PATH, - WORKFLOW_ITEM_SEND_BACK_PATH, - WORKFLOW_ITEM_VIEW_PATH, -} from './workflowitems-edit-page-routing-paths'; - -@NgModule({ - imports: [ - RouterModule.forChild([ - { - path: ':id', - resolve: { wfi: WorkflowItemPageResolver }, - children: [ - { - canActivate: [AuthenticatedGuard], - path: WORKFLOW_ITEM_EDIT_PATH, - component: ThemedSubmissionEditComponent, - resolve: { - breadcrumb: I18nBreadcrumbResolver, - }, - data: { - title: 'workflow-item.edit.title', - breadcrumbKey: 'workflow-item.edit', - collectionModifiable: false, - }, - }, - { - canActivate: [AuthenticatedGuard], - path: WORKFLOW_ITEM_VIEW_PATH, - component: ThemedFullItemPageComponent, - resolve: { - dso: ItemFromWorkflowResolver, - breadcrumb: I18nBreadcrumbResolver, - }, - data: { title: 'workflow-item.view.title', breadcrumbKey: 'workflow-item.view' }, - }, - { - canActivate: [AuthenticatedGuard], - path: WORKFLOW_ITEM_DELETE_PATH, - component: ThemedWorkflowItemDeleteComponent, - resolve: { - breadcrumb: I18nBreadcrumbResolver, - }, - data: { title: 'workflow-item.delete.title', breadcrumbKey: 'workflow-item.edit' }, - }, - { - canActivate: [AuthenticatedGuard], - path: WORKFLOW_ITEM_SEND_BACK_PATH, - component: ThemedWorkflowItemSendBackComponent, - resolve: { - breadcrumb: I18nBreadcrumbResolver, - }, - data: { title: 'workflow-item.send-back.title', breadcrumbKey: 'workflow-item.edit' }, - }, - { - canActivate: [AuthenticatedGuard], - path: ADVANCED_WORKFLOW_PATH, - component: AdvancedWorkflowActionPageComponent, - resolve: { - breadcrumb: I18nBreadcrumbResolver, - }, - data: { title: 'workflow-item.advanced.title', breadcrumbKey: 'workflow-item.edit' }, - }, - ], - }], - ), - ], - providers: [WorkflowItemPageResolver, ItemFromWorkflowResolver], -}) -/** - * This module defines the default component to load when navigating to the workflowitems edit page path. - */ -export class WorkflowItemsEditPageRoutingModule { -} diff --git a/src/app/workflowitems-edit-page/workflowitems-edit-page.module.ts b/src/app/workflowitems-edit-page/workflowitems-edit-page.module.ts deleted file mode 100644 index 338e6c8a74..0000000000 --- a/src/app/workflowitems-edit-page/workflowitems-edit-page.module.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { CommonModule } from '@angular/common'; -import { NgModule } from '@angular/core'; -import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; - -import { AccessControlModule } from '../access-control/access-control.module'; -import { ItemPageModule } from '../item-page/item-page.module'; -import { FormModule } from '../shared/form/form.module'; -import { SharedModule } from '../shared/shared.module'; -import { StatisticsModule } from '../statistics/statistics.module'; -import { SubmissionModule } from '../submission/submission.module'; -import { AdvancedWorkflowActionPageComponent } from './advanced-workflow-action/advanced-workflow-action-page/advanced-workflow-action-page.component'; -import { AdvancedWorkflowActionRatingComponent } from './advanced-workflow-action/advanced-workflow-action-rating/advanced-workflow-action-rating.component'; -import { AdvancedWorkflowActionSelectReviewerComponent } from './advanced-workflow-action/advanced-workflow-action-select-reviewer/advanced-workflow-action-select-reviewer.component'; -import { ReviewersListComponent } from './advanced-workflow-action/advanced-workflow-action-select-reviewer/reviewers-list/reviewers-list.component'; -import { AdvancedWorkflowActionsLoaderComponent } from './advanced-workflow-action/advanced-workflow-actions-loader/advanced-workflow-actions-loader.component'; -import { ThemedWorkflowItemDeleteComponent } from './workflow-item-delete/themed-workflow-item-delete.component'; -import { WorkflowItemDeleteComponent } from './workflow-item-delete/workflow-item-delete.component'; -import { ThemedWorkflowItemSendBackComponent } from './workflow-item-send-back/themed-workflow-item-send-back.component'; -import { WorkflowItemSendBackComponent } from './workflow-item-send-back/workflow-item-send-back.component'; -import { WorkflowItemsEditPageRoutingModule } from './workflowitems-edit-page-routing.module'; - -@NgModule({ - imports: [ - WorkflowItemsEditPageRoutingModule, - CommonModule, - SharedModule, - SubmissionModule, - StatisticsModule, - ItemPageModule, - AccessControlModule, - FormModule, - NgbModule, - ], - declarations: [ - WorkflowItemDeleteComponent, - ThemedWorkflowItemDeleteComponent, - WorkflowItemSendBackComponent, - ThemedWorkflowItemSendBackComponent, - AdvancedWorkflowActionsLoaderComponent, - AdvancedWorkflowActionRatingComponent, - AdvancedWorkflowActionSelectReviewerComponent, - AdvancedWorkflowActionPageComponent, - ReviewersListComponent, - ], -}) -/** - * This module handles all modules that need to access the workflowitems edit page. - */ -export class WorkflowItemsEditPageModule { - -} diff --git a/src/app/workspaceitems-edit-page/item-from-workspace-breadcrumb.resolver.ts b/src/app/workspaceitems-edit-page/item-from-workspace-breadcrumb.resolver.ts new file mode 100644 index 0000000000..912d578b45 --- /dev/null +++ b/src/app/workspaceitems-edit-page/item-from-workspace-breadcrumb.resolver.ts @@ -0,0 +1,25 @@ +import { Injectable } from '@angular/core'; +import { Resolve } from '@angular/router'; + +import { BreadcrumbConfig } from '../breadcrumbs/breadcrumb/breadcrumb-config.model'; +import { SubmissionObject } from '../core/submission/models/submission-object.model'; +import { SubmissionParentBreadcrumbResolver } from '../core/submission/resolver/submission-parent-breadcrumb.resolver'; +import { SubmissionParentBreadcrumbsService } from '../core/submission/submission-parent-breadcrumb.service'; +import { WorkspaceitemDataService } from '../core/submission/workspaceitem-data.service'; + +/** + * This class represents a resolver that retrieves the breadcrumbs of the workspace item + */ +@Injectable({ + providedIn: 'root', +}) +export class ItemFromWorkspaceBreadcrumbResolver extends SubmissionParentBreadcrumbResolver implements Resolve> { + + constructor( + protected dataService: WorkspaceitemDataService, + protected breadcrumbService: SubmissionParentBreadcrumbsService, + ) { + super(dataService, breadcrumbService); + } + +} diff --git a/src/app/workspaceitems-edit-page/item-from-workspace.resolver.spec.ts b/src/app/workspaceitems-edit-page/item-from-workspace.resolver.spec.ts index 77232762c3..0dc9ad343d 100644 --- a/src/app/workspaceitems-edit-page/item-from-workspace.resolver.spec.ts +++ b/src/app/workspaceitems-edit-page/item-from-workspace.resolver.spec.ts @@ -2,11 +2,11 @@ import { first } from 'rxjs/operators'; import { WorkspaceitemDataService } from '../core/submission/workspaceitem-data.service'; import { createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils'; -import { ItemFromWorkspaceResolver } from './item-from-workspace.resolver'; +import { itemFromWorkspaceResolver } from './item-from-workspace.resolver'; -describe('ItemFromWorkspaceResolver', () => { +describe('itemFromWorkspaceResolver', () => { describe('resolve', () => { - let resolver: ItemFromWorkspaceResolver; + let resolver: any; let wfiService: WorkspaceitemDataService; const uuid = '1234-65487-12354-1235'; const itemUuid = '8888-8888-8888-8888'; @@ -20,11 +20,11 @@ describe('ItemFromWorkspaceResolver', () => { wfiService = { findById: (id: string) => createSuccessfulRemoteDataObject$(wfi), } as any; - resolver = new ItemFromWorkspaceResolver(wfiService, null); + resolver = itemFromWorkspaceResolver; }); it('should resolve a an item from from the workflow item with the correct id', (done) => { - resolver.resolve({ params: { id: uuid } } as any, undefined) + resolver({ params: { id: uuid } } as any, undefined, wfiService) .pipe(first()) .subscribe( (resolved) => { diff --git a/src/app/workspaceitems-edit-page/item-from-workspace.resolver.ts b/src/app/workspaceitems-edit-page/item-from-workspace.resolver.ts index efbf28c61c..6e43fc7bea 100644 --- a/src/app/workspaceitems-edit-page/item-from-workspace.resolver.ts +++ b/src/app/workspaceitems-edit-page/item-from-workspace.resolver.ts @@ -1,6 +1,10 @@ -import { Injectable } from '@angular/core'; -import { Resolve } from '@angular/router'; -import { Store } from '@ngrx/store'; +import { inject } from '@angular/core'; +import { + ActivatedRouteSnapshot, + ResolveFn, + RouterStateSnapshot, +} from '@angular/router'; +import { Observable } from 'rxjs'; import { RemoteData } from '../core/data/remote-data'; import { Item } from '../core/shared/item.model'; @@ -8,15 +12,12 @@ import { SubmissionObjectResolver } from '../core/submission/resolver/submission import { WorkspaceitemDataService } from '../core/submission/workspaceitem-data.service'; /** - * This class represents a resolver that requests a specific item before the route is activated + * This method represents a resolver that requests a specific item before the route is activated */ -@Injectable() -export class ItemFromWorkspaceResolver extends SubmissionObjectResolver implements Resolve> { - constructor( - private workspaceItemService: WorkspaceitemDataService, - protected store: Store, - ) { - super(workspaceItemService, store); - } - -} +export const itemFromWorkspaceResolver: ResolveFn> = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + workspaceItemService: WorkspaceitemDataService = inject(WorkspaceitemDataService), +): Observable> => { + return SubmissionObjectResolver(route, state, workspaceItemService); +}; diff --git a/src/app/workspaceitems-edit-page/workspace-item-page.resolver.spec.ts b/src/app/workspaceitems-edit-page/workspace-item-page.resolver.spec.ts index 0502da186b..4bf44de926 100644 --- a/src/app/workspaceitems-edit-page/workspace-item-page.resolver.spec.ts +++ b/src/app/workspaceitems-edit-page/workspace-item-page.resolver.spec.ts @@ -2,11 +2,11 @@ import { first } from 'rxjs/operators'; import { WorkspaceitemDataService } from '../core/submission/workspaceitem-data.service'; import { createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils'; -import { WorkspaceItemPageResolver } from './workspace-item-page.resolver'; +import { workspaceItemPageResolver } from './workspace-item-page.resolver'; -describe('WorkflowItemPageResolver', () => { +describe('workspaceItemPageResolver', () => { describe('resolve', () => { - let resolver: WorkspaceItemPageResolver; + let resolver: any; let wsiService: WorkspaceitemDataService; const uuid = '1234-65487-12354-1235'; @@ -14,11 +14,11 @@ describe('WorkflowItemPageResolver', () => { wsiService = { findById: (id: string) => createSuccessfulRemoteDataObject$({ id }), } as any; - resolver = new WorkspaceItemPageResolver(wsiService); + resolver = workspaceItemPageResolver; }); it('should resolve a workspace item with the correct id', (done) => { - resolver.resolve({ params: { id: uuid } } as any, undefined) + resolver({ params: { id: uuid } } as any, undefined, wsiService) .pipe(first()) .subscribe( (resolved) => { diff --git a/src/app/workspaceitems-edit-page/workspace-item-page.resolver.ts b/src/app/workspaceitems-edit-page/workspace-item-page.resolver.ts index d10fb8a2b8..1d3b8e946d 100644 --- a/src/app/workspaceitems-edit-page/workspace-item-page.resolver.ts +++ b/src/app/workspaceitems-edit-page/workspace-item-page.resolver.ts @@ -1,39 +1,35 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, - Resolve, + ResolveFn, RouterStateSnapshot, } from '@angular/router'; import { Observable } from 'rxjs'; import { RemoteData } from '../core/data/remote-data'; import { getFirstCompletedRemoteData } from '../core/shared/operators'; -import { WorkflowItem } from '../core/submission/models/workflowitem.model'; +import { WorkspaceItem } from '../core/submission/models/workspaceitem.model'; import { WorkspaceitemDataService } from '../core/submission/workspaceitem-data.service'; import { followLink } from '../shared/utils/follow-link-config.model'; /** - * This class represents a resolver that requests a specific workflow item before the route is activated + * Method for resolving a workflow item based on the parameters in the current route + * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot + * @param {RouterStateSnapshot} state The current RouterStateSnapshot + * @param {WorkspaceitemDataService} workspaceItemService + * @returns Observable<> Emits the found workflow item based on the parameters in the current route, + * or an error if something went wrong */ -@Injectable() -export class WorkspaceItemPageResolver implements Resolve> { - constructor(private workspaceItemService: WorkspaceitemDataService) { - } - - /** - * Method for resolving a workflow item based on the parameters in the current route - * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot - * @param {RouterStateSnapshot} state The current RouterStateSnapshot - * @returns Observable<> Emits the found workflow item based on the parameters in the current route, - * or an error if something went wrong - */ - resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable> { - return this.workspaceItemService.findById(route.params.id, - true, - false, - followLink('item'), - ).pipe( - getFirstCompletedRemoteData(), - ); - } -} +export const workspaceItemPageResolver: ResolveFn> = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + workspaceItemService: WorkspaceitemDataService = inject(WorkspaceitemDataService), +): Observable> => { + return workspaceItemService.findById(route.params.id, + true, + false, + followLink('item'), + ).pipe( + getFirstCompletedRemoteData(), + ); +}; diff --git a/src/app/workspaceitems-edit-page/workspaceitems-delete-page/themed-workspaceitems-delete-page.component.ts b/src/app/workspaceitems-edit-page/workspaceitems-delete-page/themed-workspaceitems-delete-page.component.ts index 5f30cc0d1c..b7e1858b79 100644 --- a/src/app/workspaceitems-edit-page/workspaceitems-delete-page/themed-workspaceitems-delete-page.component.ts +++ b/src/app/workspaceitems-edit-page/workspaceitems-delete-page/themed-workspaceitems-delete-page.component.ts @@ -4,13 +4,14 @@ import { ThemedComponent } from '../../shared/theme-support/themed.component'; import { WorkspaceItemsDeletePageComponent } from './workspaceitems-delete-page.component'; /** - * Themed wrapper for WorkspaceItemsDeletePageComponent + * Themed wrapper for {@link WorkspaceItemsDeletePageComponent} */ - @Component({ - selector: 'ds-themed-workspace-items-delete', + selector: 'ds-workspace-items-delete', styleUrls: [], templateUrl: './../../shared/theme-support/themed.component.html', + standalone: true, + imports: [WorkspaceItemsDeletePageComponent], }) export class ThemedWorkspaceItemsDeletePageComponent extends ThemedComponent { protected getComponentName(): string { @@ -18,10 +19,10 @@ export class ThemedWorkspaceItemsDeletePageComponent extends ThemedComponent { - return import(`../../../themes/${themeName}/app/workflowitems-edit-page/workflow-item-delete/workflow-item-delete.component`); + return import(`../../../themes/${themeName}/app/workspaceitems-edit-page/workspaceitems-delete-page/workspaceitems-delete-page.component`); } protected importUnthemedComponent(): Promise { - return import(`./workspaceitems-delete-page.component`); + return import('./workspaceitems-delete-page.component'); } } diff --git a/src/app/workspaceitems-edit-page/workspaceitems-delete-page/workspaceitems-delete-page.component.html b/src/app/workspaceitems-edit-page/workspaceitems-delete-page/workspaceitems-delete-page.component.html index a0f0a1711e..75c265b420 100644 --- a/src/app/workspaceitems-edit-page/workspaceitems-delete-page/workspaceitems-delete-page.component.html +++ b/src/app/workspaceitems-edit-page/workspaceitems-delete-page/workspaceitems-delete-page.component.html @@ -1,13 +1,13 @@
-

{{ 'workspace-item.delete.header' | translate }}

- +

{{ 'workspace-item.delete.header' | translate }}

+
- +
diff --git a/src/themes/dspace/app/header/header.component.ts b/src/themes/dspace/app/header/header.component.ts index 1e71ba6b20..1931838923 100644 --- a/src/themes/dspace/app/header/header.component.ts +++ b/src/themes/dspace/app/header/header.component.ts @@ -1,18 +1,33 @@ +import { + AsyncPipe, + NgIf, +} from '@angular/common'; import { Component, OnInit, } from '@angular/core'; +import { RouterLink } from '@angular/router'; +import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'; +import { TranslateModule } from '@ngx-translate/core'; import { Observable } from 'rxjs'; +import { ThemedLangSwitchComponent } from 'src/app/shared/lang-switch/themed-lang-switch.component'; +import { ContextHelpToggleComponent } from '../../../../app/header/context-help-toggle/context-help-toggle.component'; import { HeaderComponent as BaseComponent } from '../../../../app/header/header.component'; +import { ThemedNavbarComponent } from '../../../../app/navbar/themed-navbar.component'; +import { ThemedSearchNavbarComponent } from '../../../../app/search-navbar/themed-search-navbar.component'; +import { ThemedAuthNavMenuComponent } from '../../../../app/shared/auth-nav-menu/themed-auth-nav-menu.component'; +import { ImpersonateNavbarComponent } from '../../../../app/shared/impersonate-navbar/impersonate-navbar.component'; /** * Represents the header with the logo and simple navigation */ @Component({ - selector: 'ds-header', + selector: 'ds-themed-header', styleUrls: ['header.component.scss'], templateUrl: 'header.component.html', + standalone: true, + imports: [NgbDropdownModule, ThemedLangSwitchComponent, RouterLink, ThemedSearchNavbarComponent, ContextHelpToggleComponent, ThemedAuthNavMenuComponent, ImpersonateNavbarComponent, ThemedNavbarComponent, TranslateModule, AsyncPipe, NgIf], }) export class HeaderComponent extends BaseComponent implements OnInit { public isNavBarCollapsed$: Observable; diff --git a/src/themes/dspace/app/home-page/home-news/home-news.component.html b/src/themes/dspace/app/home-page/home-news/home-news.component.html index 29711c1bf6..ca053fc662 100644 --- a/src/themes/dspace/app/home-page/home-news/home-news.component.html +++ b/src/themes/dspace/app/home-page/home-news/home-news.component.html @@ -3,7 +3,7 @@
-

DSpace 7

+

DSpace 9

DSpace is the world leading open source repository platform that enables organisations to:

@@ -23,10 +23,10 @@

The test user accounts below have their password set to the name of this software in lowercase.

    -
  • Demo Site Administrator = dspacedemo+admin@gmail.com
  • -
  • Demo Community Administrator = dspacedemo+commadmin@gmail.com
  • -
  • Demo Collection Administrator = dspacedemo+colladmin@gmail.com
  • -
  • Demo Submitter = dspacedemo+submit@gmail.com
  • +
  • Demo Site Administrator = dspacedemo+admin@gmail.com
  • +
  • Demo Community Administrator = dspacedemo+commadmin@gmail.com
  • +
  • Demo Collection Administrator = dspacedemo+colladmin@gmail.com
  • +
  • Demo Submitter = dspacedemo+submit@gmail.com
@@ -35,5 +35,5 @@ - Photo by @inspiredimages + Photo by @inspiredimages
diff --git a/src/themes/dspace/app/home-page/home-news/home-news.component.scss b/src/themes/dspace/app/home-page/home-news/home-news.component.scss index 3c3aa8b445..d00b0ec959 100644 --- a/src/themes/dspace/app/home-page/home-news/home-news.component.scss +++ b/src/themes/dspace/app/home-page/home-news/home-news.component.scss @@ -4,6 +4,7 @@ div.background-image-container { color: white; position: relative; + font-weight: 600; .background-image > img { background-color: var(--bs-info); @@ -68,6 +69,11 @@ color: var(--ds-home-news-link-hover-color); } } + + .lead { + font-size: 1.25rem; + font-weight: 400; + } } diff --git a/src/themes/dspace/app/home-page/home-news/home-news.component.ts b/src/themes/dspace/app/home-page/home-news/home-news.component.ts index bccb4aa9cb..cebea38ee8 100644 --- a/src/themes/dspace/app/home-page/home-news/home-news.component.ts +++ b/src/themes/dspace/app/home-page/home-news/home-news.component.ts @@ -3,9 +3,10 @@ import { Component } from '@angular/core'; import { HomeNewsComponent as BaseComponent } from '../../../../../app/home-page/home-news/home-news.component'; @Component({ - selector: 'ds-home-news', + selector: 'ds-themed-home-news', styleUrls: ['./home-news.component.scss'], templateUrl: './home-news.component.html', + standalone: true, }) /** diff --git a/src/themes/dspace/app/navbar/navbar.component.html b/src/themes/dspace/app/navbar/navbar.component.html index f1c8c66847..d828206e7a 100644 --- a/src/themes/dspace/app/navbar/navbar.component.html +++ b/src/themes/dspace/app/navbar/navbar.component.html @@ -1,5 +1,5 @@ - +