diff --git a/.dockerignore b/.dockerignore index 3b81744d2d8..9ba3d848a33 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 af1b97849b6..5fb4c121717 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -8,7 +8,16 @@ "eslint-plugin-deprecation", "unused-imports", "eslint-plugin-lodash", - "eslint-plugin-jsonc" + "eslint-plugin-jsonc", + "eslint-plugin-rxjs", + "eslint-plugin-simple-import-sort", + "eslint-plugin-import-newlines", + "eslint-plugin-jsonc", + "dspace-angular-ts", + "dspace-angular-html" + ], + "ignorePatterns": [ + "lint/test/fixture" ], "overrides": [ { @@ -18,7 +27,8 @@ "parserOptions": { "project": [ "./tsconfig.json", - "./cypress/tsconfig.json" + "./cypress/tsconfig.json", + "./lint/tsconfig.json" ], "createDefaultProgram": true }, @@ -27,17 +37,32 @@ "plugin:@typescript-eslint/recommended", "plugin:@typescript-eslint/recommended-requiring-type-checking", "plugin:@angular-eslint/recommended", - "plugin:@angular-eslint/template/process-inline-templates" + "plugin:@angular-eslint/template/process-inline-templates", + "plugin:rxjs/recommended" ], "rules": { + "indent": [ + "error", + 2, + { + "SwitchCase": 1, + "ignoredNodes": [ + "ClassBody.body > PropertyDefinition[decorators.length > 0] > .key" + ] + } + ], "max-classes-per-file": [ "error", 1 ], "comma-dangle": [ - "off", + "error", "always-multiline" ], + "object-curly-spacing": [ + "error", + "always" + ], "eol-last": [ "error", "always" @@ -104,15 +129,13 @@ "allowTernary": true } ], - "prefer-const": "off", // todo: re-enable & fix errors (more strict than it used to be in TSLint) + "prefer-const": "error", + "no-case-declarations": "error", + "no-extra-boolean-cast": "error", "prefer-spread": "off", "no-underscore-dangle": "off", - - // todo: disabled rules from eslint:recommended, consider re-enabling & fixing "no-prototype-builtins": "off", "no-useless-escape": "off", - "no-case-declarations": "off", - "no-extra-boolean-cast": "off", "@angular-eslint/directive-selector": [ "error", @@ -139,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", @@ -183,7 +206,7 @@ ], "@typescript-eslint/type-annotation-spacing": "error", "@typescript-eslint/unified-signatures": "error", - "@typescript-eslint/ban-types": "warn", // todo: deal with {} type issues & re-enable + "@typescript-eslint/ban-types": "error", "@typescript-eslint/no-floating-promises": "warn", "@typescript-eslint/no-misused-promises": "warn", "@typescript-eslint/restrict-plus-operands": "warn", @@ -200,17 +223,65 @@ "@typescript-eslint/no-unsafe-return": "off", "@typescript-eslint/restrict-template-expressions": "off", "@typescript-eslint/require-await": "off", + "@typescript-eslint/no-base-to-string": [ + "error", + { + "ignoredTypeNames": [ + "ResourceType", + "Error" + ] + } + ], "deprecation/deprecation": "warn", + "simple-import-sort/imports": "error", + "simple-import-sort/exports": "error", "import/order": "off", + "import/first": "error", + "import/newline-after-import": "error", + "import/no-duplicates": "error", "import/no-deprecated": "warn", "import/no-namespace": "error", + "import-newlines/enforce": [ + "error", + { + "items": 1, + "semi": true, + "forceSingleLine": true + } + ], + "unused-imports/no-unused-imports": "error", "lodash/import-scope": [ "error", "method" - ] + ], + + "rxjs/no-nested-subscribe": "off", // todo: go over _all_ cases + + // Custom DSpace Angular rules + "dspace-angular-ts/themed-component-classes": "error", + "dspace-angular-ts/themed-component-selectors": "error", + "dspace-angular-ts/themed-component-usages": "error" + } + }, + { + "files": [ + "*.spec.ts" + ], + "parserOptions": { + "project": [ + "./tsconfig.json", + "./cypress/tsconfig.json" + ], + "createDefaultProgram": true + }, + "rules": { + "prefer-const": "off", + + // Custom DSpace Angular rules + "dspace-angular-ts/themed-component-usages": "error" } }, { @@ -221,9 +292,8 @@ "plugin:@angular-eslint/template/recommended" ], "rules": { - // todo: re-enable & fix errors - "@angular-eslint/template/no-negated-async": "off", - "@angular-eslint/template/eqeqeq": "off" + // Custom DSpace Angular rules + "dspace-angular-html/themed-component-usages": "error" } }, { diff --git a/.gitattributes b/.gitattributes index 406640bfcc9..b5ad93b1bc6 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 8e4ed0811d5..0cce2848a79 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 34cc2c9e4f3..9eaa4d9f3f8 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 00000000000..b53e501d293 --- /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/disabled-workflows/pull_request_opened.yml b/.github/disabled-workflows/pull_request_opened.yml deleted file mode 100644 index 0dc718c0b9a..00000000000 --- a/.github/disabled-workflows/pull_request_opened.yml +++ /dev/null @@ -1,26 +0,0 @@ -# This workflow runs whenever a new pull request is created -# TEMPORARILY DISABLED. Unfortunately this doesn't work for PRs created from forked repositories (which is how we tend to create PRs). -# There is no known workaround yet. See https://github.community/t/how-to-use-github-token-for-prs-from-forks/16818 -name: Pull Request opened - -# Only run for newly opened PRs against the "main" branch -on: - pull_request: - types: [opened] - branches: - - main - -jobs: - automation: - runs-on: ubuntu-latest - steps: - # Assign the PR to whomever created it. This is useful for visualizing assignments on project boards - # See https://github.com/marketplace/actions/pull-request-assigner - - name: Assign PR to creator - uses: thomaseizinger/assign-pr-creator-action@v1.0.0 - # Note, this authentication token is created automatically - # See: https://docs.github.com/en/actions/configuring-and-managing-workflows/authenticating-with-the-github_token - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - # Ignore errors. It is possible the PR was created by someone who cannot be assigned - continue-on-error: true diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index e50105b8797..14bd09f5e35 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 219074780e3..b26f5a7c068 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -33,21 +33,23 @@ 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 + COMPOSE_PROJECT_NAME: 'ci' 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 steps: # https://github.com/actions/checkout - name: Checkout codebase - uses: actions/checkout@v3 + uses: actions/checkout@v4 # https://github.com/actions/setup-node - name: Install Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} @@ -67,63 +69,69 @@ 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 + # 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 # https://github.com/cypress-io/github-action # (NOTE: to run these e2e tests locally, just use 'ng e2e') - name: Run e2e tests (integration tests) - uses: cypress-io/github-action@v5 + uses: cypress-io/github-action@v6 with: # 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 @@ -133,19 +141,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) @@ -159,7 +167,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 '.' @@ -180,7 +188,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. @@ -191,11 +199,11 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 # 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. @@ -203,10 +211,15 @@ jobs: # Retry action: https://github.com/marketplace/actions/retry-action # Codecov action: https://github.com/codecov/codecov-action - name: Upload coverage to Codecov.io - uses: Wandalen/wretry.action@v1.0.36 + uses: Wandalen/wretry.action@v1.3.0 with: - action: codecov/codecov-action@v3 - # Try upload 5 times max + 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 attempt_delay: 30000 diff --git a/.github/workflows/codescan.yml b/.github/workflows/codescan.yml index 35a2e2d24aa..d96e786cc37 100644 --- a/.github/workflows/codescan.yml +++ b/.github/workflows/codescan.yml @@ -5,12 +5,16 @@ # because CodeQL requires a fresh build with all tests *disabled*. name: "Code Scanning" -# Run this code scan for all pushes / PRs to main branch. Also run once a week. +# Run this code scan for all pushes / PRs to main or maintenance branches. Also run once a week. on: push: - branches: [ main ] + branches: + - main + - 'dspace-**' pull_request: - branches: [ main ] + branches: + - main + - 'dspace-**' # Don't run if PR is only updating static documentation paths-ignore: - '**/*.md' @@ -31,7 +35,7 @@ jobs: steps: # https://github.com/actions/checkout - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 # Initializes the CodeQL tools for scanning. # https://github.com/github/codeql-action diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 9a2c838d83f..d0b4cd0939f 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -3,6 +3,9 @@ name: Docker images # Run this Build for all pushes to 'main' or maintenance branches, or tagged releases. # Also run for PRs to ensure PR doesn't break Docker build process +# NOTE: uses "reusable-docker-build.yml" in DSpace/DSpace to actually build each of the Docker images +# https://github.com/DSpace/DSpace/blob/main/.github/workflows/reusable-docker-build.yml +# on: push: branches: @@ -16,105 +19,41 @@ permissions: contents: read # to fetch code (actions/checkout) jobs: - docker: + ############################################################# + # Build/Push the 'dspace/dspace-angular' image + ############################################################# + dspace-angular: # Ensure this job never runs on forked repos. It's only executed for 'dspace/dspace-angular' if: github.repository == 'dspace/dspace-angular' - runs-on: ubuntu-latest - env: - # Define tags to use for Docker images based on Git tags/branches (for docker/metadata-action) - # For a new commit on default branch (main), use the literal tag 'dspace-7_x' on Docker image. - # For a new commit on other branches, use the branch name as the tag for Docker image. - # For a new tag, copy that tag name as the tag for Docker image. - IMAGE_TAGS: | - type=raw,value=dspace-7_x,enable=${{ endsWith(github.ref, github.event.repository.default_branch) }} - type=ref,event=branch,enable=${{ !endsWith(github.ref, github.event.repository.default_branch) }} - type=ref,event=tag - # Define default tag "flavor" for docker/metadata-action per - # https://github.com/docker/metadata-action#flavor-input - # We turn off 'latest' tag by default. - TAGS_FLAVOR: | - latest=false - # Architectures / Platforms for which we will build Docker images - # If this is a PR, we ONLY build for AMD64. For PRs we only do a sanity check test to ensure Docker builds work. - # If this is NOT a PR (e.g. a tag or merge commit), also build for ARM64. - PLATFORMS: linux/amd64${{ github.event_name != 'pull_request' && ', linux/arm64' || '' }} - - steps: - # https://github.com/actions/checkout - - name: Checkout codebase - uses: actions/checkout@v3 - - # https://github.com/docker/setup-buildx-action - - name: Setup Docker Buildx - uses: docker/setup-buildx-action@v2 - - # https://github.com/docker/setup-qemu-action - - name: Set up QEMU emulation to build for multiple architectures - uses: docker/setup-qemu-action@v2 - - # https://github.com/docker/login-action - - name: Login to DockerHub - # Only login if not a PR, as PRs only trigger a Docker build and not a push - if: github.event_name != 'pull_request' - uses: docker/login-action@v2 - with: - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_ACCESS_TOKEN }} - - ############################################### - # Build/Push the 'dspace/dspace-angular' image - ############################################### - # https://github.com/docker/metadata-action - # Get Metadata for docker_build step below - - name: Sync metadata (tags, labels) from GitHub to Docker for 'dspace-angular' image - id: meta_build - uses: docker/metadata-action@v4 - with: - images: dspace/dspace-angular - tags: ${{ env.IMAGE_TAGS }} - flavor: ${{ env.TAGS_FLAVOR }} - - # https://github.com/docker/build-push-action - - name: Build and push 'dspace-angular' image - id: docker_build - uses: docker/build-push-action@v3 - with: - context: . - file: ./Dockerfile - platforms: ${{ env.PLATFORMS }} - # For pull requests, we run the Docker build (to ensure no PR changes break the build), - # but we ONLY do an image push to DockerHub if it's NOT a PR - push: ${{ github.event_name != 'pull_request' }} - # Use tags / labels provided by 'docker/metadata-action' above - tags: ${{ steps.meta_build.outputs.tags }} - labels: ${{ steps.meta_build.outputs.labels }} - - ##################################################### - # Build/Push the 'dspace/dspace-angular' image ('-dist' tag) - ##################################################### - # https://github.com/docker/metadata-action - # Get Metadata for docker_build_dist step below - - name: Sync metadata (tags, labels) from GitHub to Docker for 'dspace-angular-dist' image - id: meta_build_dist - uses: docker/metadata-action@v4 - with: - images: dspace/dspace-angular - tags: ${{ env.IMAGE_TAGS }} - # As this is a "dist" image, its tags are all suffixed with "-dist". Otherwise, it uses the same - # tagging logic as the primary 'dspace/dspace-angular' image above. - flavor: ${{ env.TAGS_FLAVOR }} - suffix=-dist - - - name: Build and push 'dspace-angular-dist' image - id: docker_build_dist - uses: docker/build-push-action@v3 - with: - context: . - file: ./Dockerfile.dist - platforms: ${{ env.PLATFORMS }} - # For pull requests, we run the Docker build (to ensure no PR changes break the build), - # but we ONLY do an image push to DockerHub if it's NOT a PR - push: ${{ github.event_name != 'pull_request' }} - # Use tags / labels provided by 'docker/metadata-action' above - tags: ${{ steps.meta_build_dist.outputs.tags }} - labels: ${{ steps.meta_build_dist.outputs.labels }} + # 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-dev + image_name: dspace/dspace-angular + dockerfile_path: ./Dockerfile + secrets: + DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} + DOCKER_ACCESS_TOKEN: ${{ secrets.DOCKER_ACCESS_TOKEN }} + + ############################################################# + # Build/Push the 'dspace/dspace-angular' image ('-dist' tag) + ############################################################# + dspace-angular-dist: + # Ensure this job never runs on forked repos. It's only executed for 'dspace/dspace-angular' + if: github.repository == 'dspace/dspace-angular' + # Use the reusable-docker-build.yml script from DSpace/DSpace repo to build our Docker image + uses: DSpace/DSpace/.github/workflows/reusable-docker-build.yml@main + with: + build_id: dspace-angular-dist + image_name: dspace/dspace-angular + dockerfile_path: ./Dockerfile.dist + # As this is a "dist" image, its tags are all suffixed with "-dist". Otherwise, it uses the same + # tagging logic as the primary 'dspace/dspace-angular' image above. + tags_flavor: suffix=-dist + secrets: + DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} + DOCKER_ACCESS_TOKEN: ${{ secrets.DOCKER_ACCESS_TOKEN }} + # Enable redeploy of sandbox & demo if the branch for this image matches the deployment branch of + # these sites as specified in reusable-docker-build.xml + REDEPLOY_SANDBOX_URL: ${{ secrets.REDEPLOY_SANDBOX_URL }} + REDEPLOY_DEMO_URL: ${{ secrets.REDEPLOY_DEMO_URL }} \ No newline at end of file diff --git a/.github/workflows/issue_opened.yml b/.github/workflows/issue_opened.yml index b4436dca3aa..0a35a6a9504 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/label_merge_conflicts.yml b/.github/workflows/label_merge_conflicts.yml index c1396b6f45c..ccc6c401c0b 100644 --- a/.github/workflows/label_merge_conflicts.yml +++ b/.github/workflows/label_merge_conflicts.yml @@ -1,11 +1,12 @@ # This workflow checks open PRs for merge conflicts and labels them when conflicts are found name: Check for merge conflicts -# Run whenever the "main" branch is updated -# NOTE: This means merge conflicts are only checked for when a PR is merged to main. +# Run this for all pushes (i.e. merges) to 'main' or maintenance branches on: push: - branches: [ main ] + branches: + - main + - 'dspace-**' # So that the `conflict_label_name` is removed if conflicts are resolved, # we allow this to run for `pull_request_target` so that github secrets are available. pull_request_target: @@ -24,6 +25,8 @@ jobs: # See: https://github.com/prince-chrismc/label-merge-conflicts-action - name: Auto-label PRs with merge conflicts uses: prince-chrismc/label-merge-conflicts-action@v3 + # Ignore any failures -- may occur (randomly?) for older, outdated PRs. + continue-on-error: true # Add "merge conflict" label if a merge conflict is detected. Remove it when resolved. # Note, the authentication token is created automatically # See: https://docs.github.com/en/actions/configuring-and-managing-workflows/authenticating-with-the-github_token diff --git a/.github/workflows/port_merged_pull_request.yml b/.github/workflows/port_merged_pull_request.yml new file mode 100644 index 00000000000..857f22755e4 --- /dev/null +++ b/.github/workflows/port_merged_pull_request.yml @@ -0,0 +1,46 @@ +# This workflow will attempt to port a merged pull request to +# the branch specified in a "port to" label (if exists) +name: Port merged Pull Request + +# Only run for merged PRs against the "main" or maintenance branches +# We allow this to run for `pull_request_target` so that github secrets are available +# (This is required when the PR comes from a forked repo) +on: + pull_request_target: + types: [ closed ] + branches: + - main + - 'dspace-**' + +permissions: + contents: write # so action can add comments + pull-requests: write # so action can create pull requests + +jobs: + port_pr: + runs-on: ubuntu-latest + # Don't run on closed *unmerged* pull requests + if: github.event.pull_request.merged + steps: + # Checkout code + - uses: actions/checkout@v4 + # Port PR to other branch (ONLY if labeled with "port to") + # See https://github.com/korthout/backport-action + - name: Create backport pull requests + uses: korthout/backport-action@v2 + with: + # Trigger based on a "port to [branch]" label on PR + # (This label must specify the branch name to port to) + label_pattern: '^port to ([^ ]+)$' + # Title to add to the (newly created) port PR + pull_title: '[Port ${target_branch}] ${pull_title}' + # Description to add to the (newly created) port PR + pull_description: 'Port of #${pull_number} by @${pull_author} to `${target_branch}`.' + # Copy all labels from original PR to (newly created) port PR + # NOTE: The labels matching 'label_pattern' are automatically excluded + copy_labels_pattern: '.*' + # Skip any merge commits in the ported PR. This means only non-merge commits are cherry-picked to the new PR + merge_commits: 'skip' + # Use a personal access token (PAT) to create PR as 'dspace-bot' user. + # A PAT is required in order for the new PR to trigger its own actions (for CI checks) + github_token: ${{ secrets.PR_PORT_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/pull_request_opened.yml b/.github/workflows/pull_request_opened.yml new file mode 100644 index 00000000000..bbac52af243 --- /dev/null +++ b/.github/workflows/pull_request_opened.yml @@ -0,0 +1,24 @@ +# This workflow runs whenever a new pull request is created +name: Pull Request opened + +# Only run for newly opened PRs against the "main" or maintenance branches +# We allow this to run for `pull_request_target` so that github secrets are available +# (This is required to assign a PR back to the creator when the PR comes from a forked repo) +on: + pull_request_target: + types: [ opened ] + branches: + - main + - 'dspace-**' + +permissions: + pull-requests: write + +jobs: + automation: + runs-on: ubuntu-latest + steps: + # Assign the PR to whomever created it. This is useful for visualizing assignments on project boards + # See https://github.com/toshimaru/auto-author-assign + - name: Assign PR to creator + uses: toshimaru/auto-author-assign@v2.1.0 diff --git a/.gitignore b/.gitignore index 7d065aca061..7af424c50f3 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 4e732302f4a..6525750e4eb 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 8fac7495e1f..cd29d762f80 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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. @@ -25,4 +23,4 @@ ENV NODE_OPTIONS="--max_old_space_size=4096" # NOTE: At this time it is only possible to run Docker container in Production mode # if you have a public URL. See https://github.com/DSpace/dspace-angular/issues/1485 ENV NODE_ENV development -CMD yarn serve --host 0.0.0.0 +CMD npm run serve -- --host 0.0.0.0 diff --git a/Dockerfile.dist b/Dockerfile.dist index 2a6a66fc063..cb6e1bbd305 100644 --- a/Dockerfile.dist +++ b/Dockerfile.dist @@ -2,20 +2,20 @@ # See https://github.com/DSpace/dspace-angular/tree/main/docker for usage details # Test build: -# docker build -f Dockerfile.dist -t dspace/dspace-angular:dspace-7_x-dist . +# docker build -f Dockerfile.dist -t dspace/dspace-angular:latest-dist . -FROM node:18-alpine as build +FROM 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 diff --git a/README.md b/README.md index 689c64a2925..783e60efba0 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) `v16.x` or `v18.x`, [npm](https://www.npmjs.com/) >= `v5.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 `v16.x` or `v18.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 @@ -157,8 +157,8 @@ DSPACE_UI_SSL => DSPACE_SSL The same settings can also be overwritten by setting system environment variables instead, E.g.: ```bash -export DSPACE_HOST=api7.dspace.org -export DSPACE_UI_PORT=4200 +export DSPACE_HOST=demo.dspace.org +export DSPACE_UI_PORT=4000 ``` The priority works as follows: **environment variable** overrides **variable in `.env` file** overrides external config set by `DSPACE_APP_CONFIG_PATH` overrides **`config.(prod or dev).yml`** @@ -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 @@ -288,7 +288,7 @@ E2E tests (aka integration tests) use [Cypress.io](https://www.cypress.io/). Con The test files can be found in the `./cypress/integration/` folder. Before you can run e2e tests, two things are REQUIRED: -1. You MUST be running the DSpace backend (i.e. REST API) locally. The e2e tests will *NOT* succeed if run against our demo REST API (https://api7.dspace.org/server/), as that server is uncontrolled and may have content added/removed at any time. +1. You MUST be running the DSpace backend (i.e. REST API) locally. The e2e tests will *NOT* succeed if run against our demo/sandbox REST API (https://demo.dspace.org/server/ or https://sandbox.dspace.org/server/), as those sites may have content added/removed at any time. * After starting up your backend on localhost, make sure either your `config.prod.yml` or `config.dev.yml` has its `rest` settings defined to use that localhost backend. * If you'd prefer, you may instead use environment variables as described at [Configuring](#configuring). For example: ``` @@ -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 5e597d4d307..02fd69b1e1b 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 ea38303fa36..3b46b8403f6 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,12 +17,19 @@ 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 + # The REST API server settings # NOTE: these settings define which (publicly available) REST API to use. They are usually # 'synced' with the 'dspace.server.url' setting in your backend's local.cfg. rest: ssl: true - host: api7.dspace.org + host: sandbox.dspace.org port: 443 # NOTE: Space is capitalized because 'namespace' is a reserved string in TypeScript nameSpace: /server @@ -52,7 +59,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. @@ -75,7 +82,7 @@ cache: anonymousCache: # Maximum number of pages to cache. Default is zero (0) which means anonymous user cache is disabled. # As all pages are cached in server memory, increasing this value will increase memory needs. - # Individual cached pages are usually small (<100KB), so a value of max=1000 would only require ~100MB of memory. + # Individual cached pages are usually small (<100KB), so a value of max=1000 would only require ~100MB of memory. max: 0 # Amount of time after which cached pages are considered stale (in ms). After becoming stale, the cached # copy is automatically refreshed on the next request. @@ -131,12 +138,16 @@ submission: # NOTE: after how many time (milliseconds) submission is saved automatically # eg. timer: 5 * (1000 * 60); // 5 minutes timer: 0 + # Always show the duplicate detection section if enabled, even if there are no potential duplicates detected + # (a message will be displayed to indicate no matches were found) + duplicateDetection: + alwaysShowSection: false icons: metadata: # NOTE: example of configuration # # NOTE: metadata name # - name: dc.author - # # NOTE: fontawesome (v5.x) icon classes and bootstrap utility classes can be used + # # NOTE: fontawesome (v6.x) icon classes and bootstrap utility classes can be used # style: fas fa-user - name: dc.author style: fas fa-user @@ -147,18 +158,40 @@ submission: confidence: # NOTE: example of configuration # # NOTE: confidence value - # - name: dc.author - # # NOTE: fontawesome (v5.x) icon classes and bootstrap utility classes can be used - # style: fa-user + # - value: 600 + # # NOTE: fontawesome (v6.x) icon classes and bootstrap utility classes can be used + # style: text-success + # icon: fa-circle-check + # # NOTE: the class configured in property style is used by default, the icon property could be used in component + # configured to use a 'icon mode' display (mainly in edit-item page) - value: 600 style: text-success + icon: fa-circle-check - value: 500 style: text-info + icon: fa-gear - value: 400 style: text-warning + icon: fa-circle-question + - value: 300 + style: text-muted + icon: fa-thumbs-down + - value: 200 + style: text-muted + icon: fa-circle-exclamation + - value: 100 + style: text-muted + icon: fa-circle-stop + - value: 0 + style: text-muted + icon: fa-ban + - value: -1 + style: text-muted + icon: fa-circle-xmark # default configuration - value: default style: text-muted + icon: fa-circle-xmark # Default Language in which the UI will be rendered if the user's browser language is not an active language defaultLanguage: en @@ -169,6 +202,12 @@ languages: - code: en label: English active: true + - code: ar + label: العربية + active: true + - code: bn + label: বাংলা + active: true - code: ca label: Català active: true @@ -178,24 +217,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: 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: hu - label: Magyar - active: true - code: nl label: Nederlands active: true @@ -208,8 +259,11 @@ languages: - code: pt-BR label: Português do Brasil active: true - - code: fi - label: Suomi + - code: sr-lat + label: Srpski (lat) + active: true + - code: sr-cyr + label: Српски active: true - code: sv label: Svenska @@ -217,24 +271,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: uk label: Yкраї́нська active: true + - code: vi + label: Tiếng Việt + active: true # Browse-By Pages @@ -266,6 +308,8 @@ homePage: # No. of communities to list per page on the home page # This will always round to the nearest number from the list of page sizes. e.g. if you set it to 7 it'll use 10 pageSize: 5 + # Enable or disable the Discover filters on the homepage + showDiscoverFilters: false # Item Config item: @@ -279,8 +323,17 @@ item: # settings menu. See pageSizeOptions in 'pagination-component-options.model.ts'. pageSize: 5 +# Community Page Config +community: + # Search tab config + searchSection: + showSidebar: true + # Collection Page Config collection: + # Search tab config + searchSection: + showSidebar: true edit: undoTimeout: 10000 # 10 seconds @@ -292,33 +345,33 @@ themes: # # # A theme with a handle property will match the community, collection or item with the given # # handle, and all collections and/or items within it - # - name: 'custom', - # handle: '10673/1233' + # - name: custom + # handle: 10673/1233 # # # A theme with a regex property will match the route using a regular expression. If it # # matches the route for a community or collection it will also apply to all collections # # and/or items within it - # - name: 'custom', - # regex: 'collections\/e8043bc2.*' + # - name: custom + # regex: collections\/e8043bc2.* # # # A theme with a uuid property will match the community, collection or item with the given # # ID, and all collections and/or items within it - # - name: 'custom', - # uuid: '0958c910-2037-42a9-81c7-dca80e3892b4' + # - name: custom + # uuid: 0958c910-2037-42a9-81c7-dca80e3892b4 # # # The extends property specifies an ancestor theme (by name). Whenever a themed component is not found # # in the current theme, its ancestor theme(s) will be checked recursively before falling back to default. - # - name: 'custom-A', - # extends: 'custom-B', + # - name: custom-A + # extends: custom-B # # Any of the matching properties above can be used - # handle: '10673/34' + # handle: 10673/34 # - # - name: 'custom-B', - # extends: 'custom', - # handle: '10673/12' + # - name: custom-B + # extends: custom + # handle: 10673/12 # # # A theme with only a name will match every route - # name: 'custom' + # name: custom # # # This theme will use the default bootstrap styling for DSpace components # - name: BASE_THEME_NAME @@ -357,10 +410,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. @@ -376,7 +430,89 @@ vocabularies: vocabulary: 'srsc' enabled: true -# Default collection/community sorting order at Advanced search, Create/update community and collection when there are not a query. +# Default collection/community sorting order at Advanced search, Create/update community and collection when there are not a query. comcolSelectionSort: sortField: 'dc.title' - sortDirection: 'ASC' \ No newline at end of file + sortDirection: 'ASC' + +# Example of fallback collection for suggestions import +# suggestion: + # - collectionId: 8f7df5ca-f9c2-47a4-81ec-8a6393d6e5af + # source: "openaire" + + +# Search settings +search: + # Settings to enable/disable or configure advanced search filters. + advancedFilters: + enabled: false + # List of filters to enable in "Advanced Search" dropdown + filter: [ 'title', 'author', 'subject', 'entityType' ] + + +# Notify metrics +# Configuration for Notify Admin Dashboard for metrics visualization +notifyMetrics: + # Configuration for received messages +- title: 'admin-notify-dashboard.received-ldn' + boxes: + - color: '#B8DAFF' + title: 'admin-notify-dashboard.NOTIFY.incoming.accepted' + config: 'NOTIFY.incoming.accepted' + description: 'admin-notify-dashboard.NOTIFY.incoming.accepted.description' + - color: '#D4EDDA' + title: 'admin-notify-dashboard.NOTIFY.incoming.processed' + config: 'NOTIFY.incoming.processed' + description: 'admin-notify-dashboard.NOTIFY.incoming.processed.description' + - color: '#FDBBC7' + title: 'admin-notify-dashboard.NOTIFY.incoming.failure' + config: 'NOTIFY.incoming.failure' + description: 'admin-notify-dashboard.NOTIFY.incoming.failure.description' + - color: '#FDBBC7' + title: 'admin-notify-dashboard.NOTIFY.incoming.untrusted' + config: 'NOTIFY.incoming.untrusted' + description: 'admin-notify-dashboard.NOTIFY.incoming.untrusted.description' + - color: '#43515F' + title: 'admin-notify-dashboard.NOTIFY.incoming.involvedItems' + textColor: '#fff' + config: 'NOTIFY.incoming.involvedItems' + description: 'admin-notify-dashboard.NOTIFY.incoming.involvedItems.description' +# Configuration for outgoing messages +- title: 'admin-notify-dashboard.generated-ldn' + boxes: + - color: '#B8DAFF' + title: 'admin-notify-dashboard.NOTIFY.outgoing.queued' + config: 'NOTIFY.outgoing.queued' + description: 'admin-notify-dashboard.NOTIFY.outgoing.queued.description' + - color: '#FDEEBB' + title: 'admin-notify-dashboard.NOTIFY.outgoing.queued_for_retry' + config: 'NOTIFY.outgoing.queued_for_retry' + description: 'admin-notify-dashboard.NOTIFY.outgoing.queued_for_retry.description' + - color: '#FDBBC7' + title: 'admin-notify-dashboard.NOTIFY.outgoing.failure' + config: 'NOTIFY.outgoing.failure' + description: 'admin-notify-dashboard.NOTIFY.outgoing.failure.description' + - color: '#43515F' + title: 'admin-notify-dashboard.NOTIFY.outgoing.involvedItems' + textColor: '#fff' + config: 'NOTIFY.outgoing.involvedItems' + description: 'admin-notify-dashboard.NOTIFY.outgoing.involvedItems.description' + - color: '#D4EDDA' + title: 'admin-notify-dashboard.NOTIFY.outgoing.delivered' + config: 'NOTIFY.outgoing.delivered' + description: 'admin-notify-dashboard.NOTIFY.outgoing.delivered.description' + + +# Live Region configuration +# Live Region as defined by w3c, https://www.w3.org/TR/wai-aria-1.1/#terms: +# Live regions are perceivable regions of a web page that are typically updated as a +# 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/config/config.yml b/config/config.yml index b5eecd112f0..109db60ca92 100644 --- a/config/config.yml +++ b/config/config.yml @@ -1,5 +1,5 @@ rest: ssl: true - host: api7.dspace.org + host: sandbox.dspace.org port: 443 nameSpace: /server diff --git a/cypress.config.ts b/cypress.config.ts index 91eeb9838b3..36d8120342a 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', @@ -9,27 +10,33 @@ export default defineConfig({ openMode: 0, }, env: { - // Global constants used in DSpace e2e tests (see also ./cypress/support/e2e.ts) - // May be overridden in our cypress.json config file using specified environment variables. + // Global DSpace environment variables used in all our Cypress e2e tests + // May be modified in this config, or overridden in a variety of ways. + // See Cypress environment variable docs: https://docs.cypress.io/guides/guides/environment-variables // Default values listed here are all valid for the Demo Entities Data set available at // https://github.com/DSpace-Labs/AIP-Files/releases/tag/demo-entities-data // (This is the data set used in our CI environment) // 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', DSPACE_TEST_COLLECTION: '282164f5-d325-4740-8dd1-fa4d6d3e7200', - DSPACE_TEST_ENTITY_PUBLICATION: 'e98b0f27-5c19-49a0-960d-eb6ad5287067', + DSPACE_TEST_ENTITY_PUBLICATION: '6160810f-1e53-40db-81ef-f6621a727398', // Search term (should return results) used in search tests DSPACE_TEST_SEARCH_TERM: 'test', - // Collection used for submission tests + // Main Collection used for submission tests. Should be able to accept normal Item objects DSPACE_TEST_SUBMIT_COLLECTION_NAME: 'Sample Collection', DSPACE_TEST_SUBMIT_COLLECTION_UUID: '9d8334e9-25d3-4a67-9cea-3dffdef80144', + // Collection used for Person entity submission tests. MUST be configured with EntityType=Person. + DSPACE_TEST_SUBMIT_PERSON_COLLECTION_NAME: 'People', // Account used to test basic submission process DSPACE_TEST_SUBMIT_USER: 'dspacedemo+submit@gmail.com', DSPACE_TEST_SUBMIT_USER_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 00000000000..c783972b757 --- /dev/null +++ b/cypress/e2e/admin-add-new-modals.cy.ts @@ -0,0 +1,48 @@ +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"]').click(); + + // Click on entry of menu + 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"]').click(); + + // Click on entry of menu + 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"]').click(); + + // Click on entry of menu + 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 00000000000..e66f0ccaad8 --- /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 00000000000..f2f18de8773 --- /dev/null +++ b/cypress/e2e/admin-edit-modals.cy.ts @@ -0,0 +1,48 @@ +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"]').click(); + + // Click on entry of menu + cy.get('#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"]').click(); + + // Click on entry of menu + cy.get('#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"]').click(); + + // Click on entry of menu + cy.get('#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 00000000000..0efe040fc84 --- /dev/null +++ b/cypress/e2e/admin-export-modals.cy.ts @@ -0,0 +1,35 @@ +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"]').click(); + + // Click on entry of menu + cy.get('#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"]').click(); + + // Click on entry of menu + cy.get('#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 00000000000..877a0542e25 --- /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 00000000000..4fbf8939fe4 --- /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 new file mode 100644 index 00000000000..318bf2b27e2 --- /dev/null +++ b/cypress/e2e/admin-sidebar.cy.ts @@ -0,0 +1,28 @@ +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')); + }); + + 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 }); + + // 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 }, + }, + } 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 00000000000..c3c235e346d --- /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 00000000000..871b8644ce1 --- /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 00000000000..f113d45ebce --- /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 ea6acdafcde..f660f47a540 100644 --- a/cypress/e2e/breadcrumbs.cy.ts +++ b/cypress/e2e/breadcrumbs.cy.ts @@ -1,15 +1,14 @@ -import { TEST_ENTITY_PUBLICATION } from 'cypress/support/e2e'; 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(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 07c20ad7c91..3e914a2f8c0 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-page').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-page'); - }); + // 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 4d22420227c..5fe05433153 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-page').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-page'); - }); + // 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 89b791f03c4..0937a2542bb 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-page').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-page'); - }); + // 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 e4e027586a8..71a7356ce32 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-page').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-page'); - }); + // 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 00000000000..87033e13e4f --- /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 00000000000..29f7dd5cacb --- /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 new file mode 100644 index 00000000000..e1ba1c5eed8 --- /dev/null +++ b/cypress/e2e/collection-edit.cy.ts @@ -0,0 +1,128 @@ +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); + + // 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'); + + // 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(); + + // tag must be loaded + cy.get('ds-collection-roles').should('be.visible'); + + // 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(); + + // 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(); + + // 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'); + }); +}); + +describe('Edit Collection > Curate tab', () => { + + it('should pass accessibility tests', () => { + cy.get('a[data-test="curate"]').click(); + + // tag must be loaded + cy.get('ds-collection-curate').should('be.visible'); + + // 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(); + + // tag must be loaded + cy.get('ds-collection-access-control').should('be.visible'); + + // 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(); + + // tag must be loaded + cy.get('ds-collection-authorizations').should('be.visible'); + + // 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(); + + // tag must be loaded + cy.get('ds-collection-item-mapper').should('be.visible'); + + // 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(); + + // 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'); + }); +}); + + +describe('Edit Collection > Delete page', () => { + + 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'); + + // 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 a034b4361d6..d12536d332a 100644 --- a/cypress/e2e/collection-page.cy.ts +++ b/cypress/e2e/collection-page.cy.ts @@ -1,15 +1,14 @@ -import { TEST_COLLECTION } from 'cypress/support/e2e'; import { testA11y } from 'cypress/support/utils'; describe('Collection Page', () => { - it('should pass accessibility tests', () => { - cy.visit('/collections/'.concat(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 6df4e9a4542..3e5a465e398 100644 --- a/cypress/e2e/collection-statistics.cy.ts +++ b/cypress/e2e/collection-statistics.cy.ts @@ -1,37 +1,37 @@ -import { REGEX_MATCH_NON_EMPTY_TEXT, TEST_COLLECTION } from 'cypress/support/e2e'; +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(TEST_COLLECTION); - - it('should load if you click on "Statistics" from a Collection page', () => { - cy.visit('/collections/'.concat(TEST_COLLECTION)); - cy.get('ds-navbar ds-link-menu-item a[title="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 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(TEST_COLLECTION).concat('_TotalVisitsPerMonth')).should('exist'); - }); - - it('should pass accessibility tests', () => { - cy.visit(COLLECTIONSTATISTICSPAGE); - - // 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); - - // Analyze for accessibility issues - testA11y('ds-collection-statistics-page'); - }); + 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 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 pass accessibility tests', () => { + cy.visit(COLLECTIONSTATISTICSPAGE); + + // 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); + + // 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 00000000000..96bc003ba2a --- /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 new file mode 100644 index 00000000000..77e260feec0 --- /dev/null +++ b/cypress/e2e/community-edit.cy.ts @@ -0,0 +1,86 @@ +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); + + // 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'); + + // 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(); + + // tag must be loaded + cy.get('ds-community-roles').should('be.visible'); + + // 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(); + + // tag must be loaded + cy.get('ds-community-curate').should('be.visible'); + + // 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(); + + // tag must be loaded + cy.get('ds-community-access-control').should('be.visible'); + + // 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(); + + // tag must be loaded + cy.get('ds-community-authorizations').should('be.visible'); + + // 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(); + + // tag must be loaded + cy.get('ds-delete-community').should('be.visible'); + + // 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 7b60b59dbc9..9b9c87b112d 100644 --- a/cypress/e2e/community-list.cy.ts +++ b/cypress/e2e/community-list.cy.ts @@ -1,25 +1,17 @@ -import { Options } from 'cypress-axe'; 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 - // Disable heading-order checks until it is fixed - testA11y('ds-community-list-page', - { - rules: { - 'heading-order': { enabled: false } - } - } as Options - ); - }); + // 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 6c628e21ce1..5a4441dbae8 100644 --- a/cypress/e2e/community-page.cy.ts +++ b/cypress/e2e/community-page.cy.ts @@ -1,15 +1,14 @@ -import { TEST_COMMUNITY } from 'cypress/support/e2e'; import { testA11y } from 'cypress/support/utils'; describe('Community Page', () => { - it('should pass accessibility tests', () => { - cy.visit('/communities/'.concat(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 710450e7972..00e23a90b37 100644 --- a/cypress/e2e/community-statistics.cy.ts +++ b/cypress/e2e/community-statistics.cy.ts @@ -1,37 +1,37 @@ -import { REGEX_MATCH_NON_EMPTY_TEXT, TEST_COMMUNITY } from 'cypress/support/e2e'; +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(TEST_COMMUNITY); - - it('should load if you click on "Statistics" from a Community page', () => { - cy.visit('/communities/'.concat(TEST_COMMUNITY)); - cy.get('ds-navbar ds-link-menu-item a[title="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 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(TEST_COMMUNITY).concat('_TotalVisitsPerMonth')).should('exist'); - }); - - it('should pass accessibility tests', () => { - cy.visit(COMMUNITYSTATISTICSPAGE); - - // 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); - - // Analyze for accessibility issues - testA11y('ds-community-statistics-page'); - }); + 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 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 pass accessibility tests', () => { + cy.visit(COMMUNITYSTATISTICSPAGE); + + // 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); + + // 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 00000000000..d23986ba29d --- /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 00000000000..135c041a8d5 --- /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 00000000000..166c913b8c8 --- /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 00000000000..e43ede978ad --- /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 00000000000..989d21ce60f --- /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 00000000000..a6192f13d95 --- /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 00000000000..75fe1097c63 --- /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 656e9d47012..4ee1d6669ae 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 00000000000..5c0099c2f1f --- /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 236208db686..1471e5ae6c5 100644 --- a/cypress/e2e/header.cy.ts +++ b/cypress/e2e/header.cy.ts @@ -1,19 +1,38 @@ import { testA11y } from 'cypress/support/utils'; describe('Header', () => { - it('should pass accessibility tests', () => { - cy.visit('/'); - - // Header must first be visible - cy.get('ds-header').should('be.visible'); - - // Analyze for accessibility - testA11y({ - include: ['ds-header'], - exclude: [ - ['#search-navbar-container'], // search in navbar has duplicative ID. Will be fixed in #1174 - ['.dropdownLogin'] // "Log in" link has color contrast issues. Will be fixed in #1149 - ], - }); - }); + it('should pass accessibility tests', () => { + cy.visit('/'); + + // Header must first be visible + cy.get('ds-header').should('be.visible'); + + // 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 00000000000..c702fa72d79 --- /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 2a1ab9785ab..0e0fca3c5bb 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, TEST_ENTITY_PUBLICATION } 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[title="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(TEST_ENTITY_PUBLICATION, 'item'); - cy.generateViewEvent(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 new file mode 100644 index 00000000000..45131baace2 --- /dev/null +++ b/cypress/e2e/item-edit.cy.ts @@ -0,0 +1,169 @@ +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); + + // 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')); + + // We need to wait for the correction types allowed for the item to be loaded to be sure that each tab is fully loaded. + // This because the edit item page causes often tests to fails due to timeout. + cy.intercept('GET', 'server/api/config/correctiontypes/search/findByItem*').as('correctionTypes'); + cy.wait('@correctionTypes'); +}); + +describe('Edit Item > Edit Metadata tab', () => { + it('should pass accessibility tests', () => { + cy.get('a[data-test="metadata"]').click(); + + // Our selected tab should be active + cy.get('a[data-test="metadata"]').should('have.class', 'active'); + + // 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(); + + // Our selected tab should be active + cy.get('a[data-test="status"]').should('have.class', 'active'); + + // 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(); + + // Our selected tab should be active + cy.get('a[data-test="bitstreams"]').should('have.class', 'active'); + + // tag must be loaded + cy.get('ds-item-bitstreams').should('be.visible'); + + // 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, + ); + }); +}); + +describe('Edit Item > Curate tab', () => { + + it('should pass accessibility tests', () => { + cy.get('a[data-test="curate"]').click(); + + // Our selected tab should be active + cy.get('a[data-test="curate"]').should('have.class', 'active'); + + // 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(); + + // Our selected tab should be active + cy.get('a[data-test="relationships"]').should('have.class', 'active'); + + // 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(); + + // Our selected tab should be active + cy.get('a[data-test="versionhistory"]').should('have.class', 'active'); + + // 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(); + + // Our selected tab should be active + cy.get('a[data-test="access-control"]').should('have.class', 'active'); + + // 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(); + + // Our selected tab should be active + cy.get('a[data-test="mapper"]').should('have.class', 'active'); + + // tag must be loaded + cy.get('ds-item-collection-mapper').should('be.visible'); + + // Analyze entire page for accessibility issues + testA11y('ds-item-collection-mapper'); + + // Click on the "Map new collections" tab + cy.get('li[data-test="mapTab"] a').click(); + + // 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 9eed711776e..b79b6ac31d1 100644 --- a/cypress/e2e/item-page.cy.ts +++ b/cypress/e2e/item-page.cy.ts @@ -1,31 +1,32 @@ -import { Options } from 'cypress-axe'; -import { TEST_ENTITY_PUBLICATION } from 'cypress/support/e2e'; import { testA11y } from 'cypress/support/utils'; describe('Item Page', () => { - const ITEMPAGE = '/items/'.concat(TEST_ENTITY_PUBLICATION); - const ENTITYPAGE = '/entities/publication/'.concat(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); - }); - - it('should pass accessibility tests', () => { - cy.visit(ENTITYPAGE); - - // tag must be loaded - cy.get('ds-item-page').should('be.visible'); - - // Analyze for accessibility issues - // Disable heading-order checks until it is fixed - testA11y('ds-item-page', - { - rules: { - 'heading-order': { enabled: false } - } - } as Options - ); - }); + 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); + }); + + it('should pass accessibility tests', () => { + cy.visit(ENTITYPAGE); + + // tag must be loaded + cy.get('ds-item-page').should('be.visible'); + + // Analyze for accessibility issues + testA11y('ds-item-page'); + }); + + 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'); + + // 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 9b90cb24afc..6518f595a90 100644 --- a/cypress/e2e/item-statistics.cy.ts +++ b/cypress/e2e/item-statistics.cy.ts @@ -1,43 +1,43 @@ -import { REGEX_MATCH_NON_EMPTY_TEXT, TEST_ENTITY_PUBLICATION } from 'cypress/support/e2e'; +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(TEST_ENTITY_PUBLICATION); - - it('should load if you click on "Statistics" from an Item/Entity page', () => { - cy.visit('/entities/publication/'.concat(TEST_ENTITY_PUBLICATION)); - cy.get('ds-navbar ds-link-menu-item a[title="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 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(TEST_ENTITY_PUBLICATION).concat('_TotalVisitsPerMonth')).should('exist'); - }); - - it('should pass accessibility tests', () => { - cy.visit(ITEMSTATISTICSPAGE); - - // 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); - - // Analyze for accessibility issues - testA11y('ds-item-statistics-page'); - }); + 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 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 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); + + // 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); + + // 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 00000000000..5f5b21a16ab --- /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 b169634cfa0..80d36a03099 100644 --- a/cypress/e2e/login-modal.cy.ts +++ b/cypress/e2e/login-modal.cy.ts @@ -1,126 +1,150 @@ -import { TEST_ADMIN_PASSWORD, TEST_ADMIN_USER, TEST_ENTITY_PUBLICATION } from 'cypress/support/e2e'; +import { testA11y } from 'cypress/support/utils'; const page = { - openLoginMenu() { - // Click the "Log In" dropdown menu in header - cy.get('ds-themed-navbar [data-test="login-menu"]').click(); - }, - openUserMenu() { - // Once logged in, click the User menu in header - cy.get('ds-themed-navbar [data-test="user-menu"]').click(); - }, - submitLoginAndPasswordByPressingButton(email, password) { - // Enter email - cy.get('ds-themed-navbar [data-test="email"]').type(email); - // Enter password - cy.get('ds-themed-navbar [data-test="password"]').type(password); - // Click login button - cy.get('ds-themed-navbar [data-test="login-button"]').click(); - }, - submitLoginAndPasswordByPressingEnter(email, password) { - // In opened Login modal, fill out email & password, then click Enter - cy.get('ds-themed-navbar [data-test="email"]').type(email); - cy.get('ds-themed-navbar [data-test="password"]').type(password); - cy.get('ds-themed-navbar [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-navbar [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(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(TEST_ADMIN_USER, 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(TEST_ADMIN_USER, 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(TEST_ADMIN_USER, 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-navbar [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-navbar [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'); - it('should allow forgot password', () => { - cy.visit('/'); + // Test accessibility of this page + testA11y('ds-register-email'); + }); - page.openLoginMenu(); + it('should allow forgot password', () => { + cy.visit('/'); - // Forgot password link should be visible - cy.get('ds-themed-navbar [data-test="forgot"]').should('be.visible'); + page.openLoginMenu(); - // Click link & you should go to Forgot Password page - cy.get('ds-themed-navbar [data-test="forgot"]').click(); - cy.location('pathname').should('eq', '/forgot'); - cy.get('ds-forgot-email').should('exist'); - }); + // 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-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'); + }); + + 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'); + + // Now login + page.submitLoginAndPasswordByPressingButton(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD')); + cy.get('ds-log-in').should('not.exist'); + + // Open user menu, verify user menu accessibility + page.openUserMenu(); + cy.get('ds-user-menu').should('be.visible'); + testA11y('ds-user-menu'); + }); }); diff --git a/cypress/e2e/metadata-import-page.cy.ts b/cypress/e2e/metadata-import-page.cy.ts new file mode 100644 index 00000000000..a31c18e4ebb --- /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 00000000000..0402d33153e --- /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 00000000000..9ff0db0714b --- /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 79786c298a3..159bb4f5e65 100644 --- a/cypress/e2e/my-dspace.cy.ts +++ b/cypress/e2e/my-dspace.cy.ts @@ -1,155 +1,134 @@ -import { Options } from 'cypress-axe'; -import { TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD, TEST_SUBMIT_COLLECTION_NAME } from 'cypress/support/e2e'; import { testA11y } from 'cypress/support/utils'; describe('My DSpace page', () => { - 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(TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD); - - 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'); - - // 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( - { - include: ['ds-my-dspace-page'], - exclude: [ - ['nouislider'] // Date filter slider is missing ARIA labels. Will be fixed by #1175 - ], - }, - { - rules: { - // Search filters fail these two "moderate" impact rules - 'heading-order': { enabled: false }, - 'landmark-unique': { enabled: false } - } - } as Options - ); - }); + it('should display recent submissions and pass accessibility tests', () => { + cy.visit('/mydspace'); - it('should have a working detailed view that passes accessibility tests', () => { - cy.visit('/mydspace'); + // This page is restricted, so we will be shown the login form. Fill it out & submit. + cy.loginViaForm(Cypress.env('DSPACE_TEST_SUBMIT_USER'), Cypress.env('DSPACE_TEST_SUBMIT_USER_PASSWORD')); - // This page is restricted, so we will be shown the login form. Fill it out & submit. - cy.loginViaForm(TEST_SUBMIT_USER, 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'); - // Click button in sidebar to display detailed view - cy.get('ds-search-sidebar [data-test="detail-view"]').click(); + // 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 }); - cy.get('ds-object-detail').should('be.visible'); + // Analyze for accessibility issues + testA11y('ds-my-dspace-page'); + }); - // Analyze for accessibility issues - testA11y('ds-my-dspace-page', - { - rules: { - // Search filters fail these two "moderate" impact rules - 'heading-order': { enabled: false }, - 'landmark-unique': { enabled: false } - } - } as Options - ); - }); + it('should have a working detailed view that passes accessibility tests', () => { + cy.visit('/mydspace'); - // 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')); - // This page is restricted, so we will be shown the login form. Fill it out & submit. - cy.loginViaForm(TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD); + cy.get('ds-my-dspace-page').should('be.visible'); - // 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(); + // Click button in sidebar to display detailed view + cy.get('ds-search-sidebar [data-test="detail-view"]').click(); - // This should display the (popup window) - cy.get('ds-create-item-parent-selector').should('be.visible'); + cy.get('ds-object-detail').should('be.visible'); - // Type in a known Collection name in the search box - cy.get('ds-authorized-collection-selector input[type="search"]').type(TEST_SUBMIT_COLLECTION_NAME); + // Analyze for accessibility issues + testA11y('ds-my-dspace-page'); + }); - // Click on the button matching that known Collection name - cy.get('ds-authorized-collection-selector button[title="'.concat(TEST_SUBMIT_COLLECTION_NAME).concat('"]')).click(); + // 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'); - // New URL should include /workspaceitems, as we've started a new submission - cy.url().should('include', '/workspaceitems'); + // 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')); - // The Submission edit form tag should be visible - cy.get('ds-submission-edit').should('be.visible'); + // 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(); - // A Collection menu button should exist & its value should be the selected collection - cy.get('#collectionControlsMenuButton span').should('have.text', TEST_SUBMIT_COLLECTION_NAME); + // This should display the (popup window) + cy.get('ds-create-item-parent-selector').should('be.visible'); - // 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]; + // 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 the "Save for Later" button to save this submission - cy.get('ds-submission-form-footer [data-test="save-for-later"]').click(); + // 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(); - // "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'); - // 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}); + // 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 created 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_COLLECTION_NAME')); - // Wait for search results to come back from the above GET command - cy.wait('@search-results'); + // 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 Edit button for this in-progress submission - cy.get('#edit_' + id).click(); + // Click the "Save for Later" button to save this submission + cy.get('ds-submission-form-footer [data-test="save-for-later"]').click(); - // Should send us back to the submission form - cy.url().should('include', '/workspaceitems/' + id + '/edit'); + // "Save for Later" should send us to MyDSpace + cy.url().should('include', '/mydspace'); - // 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(); + // 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 }); - // Discarding should send us back to MyDSpace - cy.url().should('include', '/mydspace'); - }); - }); + // 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(); - it('should let you import from external sources', () => { - cy.visit('/mydspace'); + // Wait for search results to come back from the above GET command + cy.wait('@search-results'); - // This page is restricted, so we will be shown the login form. Fill it out & submit. - cy.loginViaForm(TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD); + // Click the Edit button for this in-progress submission + cy.get('#edit_' + id).click(); - // 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(); + // Should send us back to the submission form + cy.url().should('include', '/workspaceitems/' + id + '/edit'); - // New URL should include /import-external, as we've moved to the import page - cy.url().should('include', '/import-external'); + // 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(); - // The external import searchbox should be visible - cy.get('ds-submission-import-external-searchbar').should('be.visible'); + // 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'); + }); }); diff --git a/cypress/e2e/new-process.cy.ts b/cypress/e2e/new-process.cy.ts new file mode 100644 index 00000000000..d26da7cc4df --- /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 43e3c3af242..5f84a22df9c 100644 --- a/cypress/e2e/pagenotfound.cy.ts +++ b/cypress/e2e/pagenotfound.cy.ts @@ -1,13 +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 not contain element ds-pagenotfound when navigating to existing page', () => { - cy.visit('/home'); - cy.get('ds-pagenotfound').should('not.exist'); - }); + 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'); + }); + + 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 00000000000..16e049f701e --- /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 00000000000..2be3bd4c181 --- /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 00000000000..911ef33ba58 --- /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 00000000000..722917ef16b --- /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 648db17fe65..0613e5e7124 100644 --- a/cypress/e2e/search-navbar.cy.ts +++ b/cypress/e2e/search-navbar.cy.ts @@ -1,66 +1,64 @@ -import { TEST_SEARCH_TERM } from 'cypress/support/e2e'; - const page = { - fillOutQueryInNavBar(query) { - // Click the magnifying glass - cy.get('ds-themed-navbar [data-test="header-search-icon"]').click(); - // Fill out a query in input that appears - cy.get('ds-themed-navbar [data-test="header-search-box"]').type(query); - }, - submitQueryByPressingEnter() { - cy.get('ds-themed-navbar [data-test="header-search-box"]').type('{enter}'); - }, - submitQueryByPressingIcon() { - cy.get('ds-themed-navbar [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 = 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 24519cc236a..62e73c38772 100644 --- a/cypress/e2e/search-page.cy.ts +++ b/cypress/e2e/search-page.cy.ts @@ -1,70 +1,57 @@ -import { Options } from 'cypress-axe'; -import { TEST_SEARCH_TERM } from 'cypress/support/e2e'; import { testA11y } from 'cypress/support/utils'; +import { Options } from 'cypress-axe'; describe('Search Page', () => { - 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)); - }); + // NOTE: these tests currently assume this query will return results! + const query = Cypress.env('DSPACE_TEST_SEARCH_TERM'); - it('should load results and pass accessibility tests', () => { - cy.visit('/search?query='.concat(TEST_SEARCH_TERM)); - cy.get('[data-test="search-box"]').should('have.value', 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)); + }); - // tag must be loaded - cy.get('ds-search-page').should('be.visible'); + it('should load results and pass accessibility tests', () => { + cy.visit('/search?query='.concat(query)); + cy.get('[data-test="search-box"]').should('have.value', query); - // At least one search result should be displayed - cy.get('[data-test="list-object"]').should('be.visible'); + // tag must be loaded + cy.get('ds-search-page').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 }); + // At least one search result should be displayed + cy.get('[data-test="list-object"]').should('be.visible'); - // Analyze for accessibility issues - testA11y( - { - include: ['ds-search-page'], - exclude: [ - ['nouislider'] // Date filter slider is missing ARIA labels. Will be fixed by #1175 - ], - }, - { - rules: { - // Search filters fail these two "moderate" impact rules - 'heading-order': { enabled: false }, - 'landmark-unique': { enabled: false } - } - } as Options - ); - }); + // 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'); + }); - it('should have a working grid view that passes accessibility tests', () => { - cy.visit('/search?query='.concat(TEST_SEARCH_TERM)); + 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: { - // Search filters fail these two "moderate" impact rules - 'heading-order': { enabled: false }, - 'landmark-unique': { 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 ed10b2d13aa..ebdabbde2e0 100644 --- a/cypress/e2e/submission.cy.ts +++ b/cypress/e2e/submission.cy.ts @@ -1,134 +1,227 @@ -import { TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD, TEST_SUBMIT_COLLECTION_NAME, TEST_SUBMIT_COLLECTION_UUID } from 'cypress/support/e2e'; +import { testA11y } from 'cypress/support/utils'; +//import { TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD, TEST_SUBMIT_COLLECTION_NAME, TEST_SUBMIT_COLLECTION_UUID, TEST_ADMIN_USER, TEST_ADMIN_PASSWORD } from 'cypress/support/e2e'; +import { Options } from 'cypress-axe'; describe('New Submission page', () => { - // NOTE: We already test that new 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(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(TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD); - - // 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'); - - // A Collection menu button should exist & it's value should be the selected collection - cy.get('#collectionControlsMenuButton span').should('have.text', 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'); - - // 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(); + // 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')); + + // 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'); + + // 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'); + + // Test entire page for accessibility + testA11y('ds-submission-edit', + { + rules: { + // Author & Subject fields have invalid "aria-multiline" attrs. + // See https://github.com/DSpace/dspace-angular/issues/1272 + 'aria-allowed-attr': { enabled: false }, + // All panels are accordions & fail "aria-required-children" and "nested-interactive". + // Seem to require updating ng-bootstrap and https://github.com/DSpace/dspace-angular/issues/2216 + 'aria-required-children': { enabled: false }, + 'nested-interactive': { enabled: false }, + // All select boxes fail to have a name / aria-label. + // This is a bug in ng-dynamic-forms and may require https://github.com/DSpace/dspace-angular/issues/2216 + 'select-name': { enabled: false }, + }, + + } as Options, + ); + + // Discard button should work + // Clicking it will display a confirmation, which we will confirm with another click + cy.get('button#discard').click(); + cy.get('button#discard_submit').click(); + }); + + it('should block submission & show errors if required fields are missing', () => { + // Create a new submission + cy.visit('/submit?collection='.concat(Cypress.env('DSPACE_TEST_SUBMIT_COLLECTION_UUID')).concat('&entityType=none')); + + // This page is restricted, so we will be shown the login form. Fill it out & submit. + cy.loginViaForm(Cypress.env('DSPACE_TEST_SUBMIT_USER'), Cypress.env('DSPACE_TEST_SUBMIT_USER_PASSWORD')); + + // Attempt an immediate deposit without filling out any fields + cy.get('button#deposit').click(); + + // A warning alert should display. + cy.get('ds-notification div.alert-success').should('not.exist'); + cy.get('ds-notification div.alert-warning').should('be.visible'); + + // First section should have an exclamation error in the header + // (as it has required fields) + cy.get('div#traditionalpageone-header i.fa-exclamation-circle').should('be.visible'); + + // Title field should have class "is-invalid" applied, as it's required + cy.get('input#dc_title').should('have.class', 'is-invalid'); + + // Date Year field should also have "is-valid" class + cy.get('input#dc_date_issued_year').should('have.class', 'is-invalid'); + + // FINALLY, cleanup after ourselves. This also exercises the MyDSpace delete button. + // Get our Submission URL, to parse out the ID of this submission + cy.location().then(fullUrl => { + // This will be the full path (/workspaceitems/[id]/edit) + const path = fullUrl.pathname; + // Split on the slashes + const subpaths = path.split('/'); + // Part 2 will be the [id] of the submission + const id = subpaths[2]; + + // Even though form is incomplete, the "Save for Later" button should still work + cy.get('button#saveForLater').click(); + + // "Save for Later" should send us to MyDSpace + cy.url().should('include', '/mydspace'); + + // A success alert should be visible + cy.get('ds-notification div.alert-success').should('be.visible'); + // Now, dismiss any open alert boxes (may be multiple, as tests run quickly) + cy.get('[data-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 block submission & show errors if required fields are missing', () => { - // Create a new submission - cy.visit('/submit?collection='.concat(TEST_SUBMIT_COLLECTION_UUID).concat('&entityType=none')); + 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(TEST_SUBMIT_USER, 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')); - // Attempt an immediate deposit without filling out any fields - cy.get('button#deposit').click(); + // 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'); - // 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'); + // 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 } ); - // 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'); + // 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'); - // Title field should have class "is-invalid" applied, as it's required - cy.get('input#dc_title').should('have.class', 'is-invalid'); + // This is the POST command that will upload the file + cy.intercept('POST', '/server/api/submission/workspaceitems/*').as('upload'); - // Date Year field should also have "is-valid" class - cy.get('input#dc_date_issued_year').should('have.class', 'is-invalid'); + // 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', + }); - // 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]; + // Wait for upload to complete before proceeding + cy.wait('@upload'); - // Even though form is incomplete, the "Save for Later" button should still work - cy.get('button#saveForLater').click(); + // Wait for deposit button to not be disabled & click it. + cy.get('button#deposit').should('not.be.disabled').click(); - // "Save for Later" should send us to MyDSpace - cy.url().should('include', '/mydspace'); + // 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 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}); + 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 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(); + // 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')); - // Wait for search results to come back from the above GET command - cy.wait('@search-results'); + // 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(); - // Delete our created submission & confirm deletion - cy.get('button#delete_' + id).click(); - cy.get('button#delete_confirm').click(); - }); - }); + // This should display the (popup window) + cy.get('ds-create-item-parent-selector').should('be.visible'); - it('should allow for deposit if all required fields completed & file uploaded', () => { - // Create a new submission - cy.visit('/submit?collection='.concat(TEST_SUBMIT_COLLECTION_UUID).concat('&entityType=none')); + // 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')); - // This page is restricted, so we will be shown the login form. Fill it out & submit. - cy.loginViaForm(TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD); + // 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(); - // 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'); + // New URL should include /workspaceitems, as we've started a new submission + cy.url().should('include', '/workspaceitems'); - // 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} ); + // The Submission edit form tag should be visible + cy.get('ds-submission-edit').should('be.visible'); - // 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'); + // A Collection menu button should exist & its value should be the selected collection + cy.get('#collectionControlsMenuButton span').should('have.text', Cypress.env('DSPACE_TEST_SUBMIT_PERSON_COLLECTION_NAME')); - // This is the POST command that will upload the file - cy.intercept('POST', '/server/api/submission/workspaceitems/*').as('upload'); + // 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'); - // 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' - }); + // Test entire page for accessibility + testA11y('ds-submission-edit', + { + rules: { + // All panels are accordions & fail "aria-required-children" and "nested-interactive". + // Seem to require updating ng-bootstrap and https://github.com/DSpace/dspace-angular/issues/2216 + 'aria-required-children': { enabled: false }, + 'nested-interactive': { enabled: false }, + }, - // Wait for upload to complete before proceeding - cy.wait('@upload'); + } as Options, + ); - // Wait for deposit button to not be disabled & click it. - cy.get('button#deposit').should('not.be.disabled').click(); + // Click the lookup button next to "Publication" field + cy.get('button[data-test="lookup-button"]').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 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(); + }); }); diff --git a/cypress/e2e/system-wide-alert.cy.ts b/cypress/e2e/system-wide-alert.cy.ts new file mode 100644 index 00000000000..046bfe619fe --- /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 ead38afb921..091f11d0f7e 100644 --- a/cypress/plugins/index.ts +++ b/cypress/plugins/index.ts @@ -1,35 +1,59 @@ const fs = require('fs'); +// These two global variables are used to store information about the REST API used +// by these e2e tests. They are filled out prior to running any tests in the before() +// method of e2e.ts. They can then be accessed by any tests via the getters below. +let REST_BASE_URL: string; +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; - } - }); + 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 c70c4e37e12..8cc2c5c721b 100644 --- a/cypress/support/commands.ts +++ b/cypress/support/commands.ts @@ -3,13 +3,15 @@ // 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'; - -// NOTE: FALLBACK_TEST_REST_BASE_URL is only used if Cypress cannot read the REST API BaseURL -// from the Angular UI's config.json. See 'login()'. -export const FALLBACK_TEST_REST_BASE_URL = 'http://localhost:8080/server'; -export const FALLBACK_TEST_REST_DOMAIN = 'localhost'; +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 // ALL custom commands MUST be listed here for code completion to work @@ -41,6 +43,13 @@ declare global { * @param dsoType type of DSpace Object (e.g. "item", "collection", "community") */ generateViewEvent(uuid: string, dsoType: string): typeof generateViewEvent; + + /** + * Create a new CSRF token and add to required Cookie. CSRF Token is returned + * in chainable in order to allow it to be sent also in required CSRF header. + * @returns Chainable reference to allow CSRF token to also be sent in header. + */ + createCSRFCookie(): Chainable; } } } @@ -54,60 +63,33 @@ declare global { * @param password password to login as */ function login(email: string, password: string): void { - // 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. - cy.task('readUIConfig').then((str: string) => { - // Parse config into a JSON object - const config = JSON.parse(str); - - // Find the URL of our REST API. Have a fallback ready, just in case 'rest.baseUrl' cannot be found. - 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); - } else { - //console.log("Found 'rest.baseUrl' in config.json. Using this REST API for login: ".concat(config.rest.baseUrl)); - baseRestUrl = config.rest.baseUrl; - } - - // Now find domain of our REST API, again with a fallback. - 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); - } else { - baseDomain = config.rest.host; - } - - // Create a fake CSRF Token. Set it in the required server-side cookie - const csrfToken = 'fakeLoginCSRFToken'; - cy.setCookie(DSPACE_XSRF_COOKIE, csrfToken, { 'domain': baseDomain }); - - // 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); - - // 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)); - }); - - // Remove cookie with fake CSRF token, as it's no longer needed - cy.clearCookie(DSPACE_XSRF_COOKIE); + // 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); + + // 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); @@ -118,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); @@ -141,54 +123,53 @@ Cypress.Commands.add('loginViaForm', loginViaForm); * @param dsoType type of DSpace Object (e.g. "item", "collection", "community") */ function generateViewEvent(uuid: string, dsoType: string): void { - // 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. - cy.task('readUIConfig').then((str: string) => { - // Parse config into a JSON object - const config = JSON.parse(str); - - // Find the URL of our REST API. Have a fallback ready, just in case 'rest.baseUrl' cannot be found. - 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); - } else { - baseRestUrl = config.rest.baseUrl; - } - - // Now find domain of our REST API, again with a fallback. - 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); - } else { - baseDomain = config.rest.host; - } - - // Create a fake CSRF Token. Set it in the required server-side cookie - const csrfToken = 'fakeGenerateViewEventCSRFToken'; - cy.setCookie(DSPACE_XSRF_COOKIE, csrfToken, { 'domain': baseDomain }); - - // 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', - }, - //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); - }); - - // Remove cookie with fake CSRF token, as it's no longer needed - cy.clearCookie(DSPACE_XSRF_COOKIE); + // 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); + +/** + * Can be used by tests to generate a random XSRF/CSRF token and save it to + * the required XSRF/CSRF cookie for usage when sending POST requests or similar. + * The generated CSRF token is returned in a Chainable to allow it to be also sent + * in the CSRF HTTP Header. + * @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(); + + // 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); +} +// 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 dd7ee1824c4..48985e79115 100644 --- a/cypress/support/e2e.ts +++ b/cypress/support/e2e.ts @@ -15,49 +15,57 @@ // 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'; -// 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}'); -}); +import { DSPACE_XSRF_COOKIE } from 'src/app/core/xsrf/xsrf.constants'; + +// Runs once before all tests +before(() => { + // Cypress doesn't have access to the running application in Node.js. + // 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. + cy.task('readUIConfig').then((str: string) => { + // Parse config into a JSON object + const config = JSON.parse(str); -// For better stability between tests, we visit "about:blank" (i.e. blank page) after each test. -// This ensures any remaining/outstanding XHR requests are killed, so they don't affect the next test. -// Borrowed from: https://glebbahmutov.com/blog/visit-blank-page-between-tests/ -/*afterEach(() => { - cy.window().then((win) => { - win.location.href = 'about:blank'; - }); -});*/ + // 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); + } else { + 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); + } else { + baseDomain = config.rest.host; + } + cy.task('saveRestBaseDomain', baseDomain); -// Global constants used in tests -// May be overridden in our cypress.json config file using specified environment variables. -// Default values listed here are all valid for the Demo Entities Data set available at -// https://github.com/DSpace-Labs/AIP-Files/releases/tag/demo-entities-data -// (This is the data set used in our CI environment) + }); +}); -// Admin account used for administrative tests -export const TEST_ADMIN_USER = Cypress.env('DSPACE_TEST_ADMIN_USER') || 'dspacedemo+admin@gmail.com'; -export const TEST_ADMIN_PASSWORD = Cypress.env('DSPACE_TEST_ADMIN_PASSWORD') || 'dspace'; -// Community/collection/publication used for view/edit tests -export const TEST_COLLECTION = Cypress.env('DSPACE_TEST_COLLECTION') || '282164f5-d325-4740-8dd1-fa4d6d3e7200'; -export const TEST_COMMUNITY = Cypress.env('DSPACE_TEST_COMMUNITY') || '0958c910-2037-42a9-81c7-dca80e3892b4'; -export const TEST_ENTITY_PUBLICATION = Cypress.env('DSPACE_TEST_ENTITY_PUBLICATION') || 'e98b0f27-5c19-49a0-960d-eb6ad5287067'; -// Search term (should return results) used in search tests -export const TEST_SEARCH_TERM = Cypress.env('DSPACE_TEST_SEARCH_TERM') || 'test'; -// Collection used for submission tests -export const TEST_SUBMIT_COLLECTION_NAME = Cypress.env('DSPACE_TEST_SUBMIT_COLLECTION_NAME') || 'Sample Collection'; -export const TEST_SUBMIT_COLLECTION_UUID = Cypress.env('DSPACE_TEST_SUBMIT_COLLECTION_UUID') || '9d8334e9-25d3-4a67-9cea-3dffdef80144'; -export const TEST_SUBMIT_USER = Cypress.env('DSPACE_TEST_SUBMIT_USER') || 'dspacedemo+submit@gmail.com'; -export const TEST_SUBMIT_USER_PASSWORD = Cypress.env('DSPACE_TEST_SUBMIT_USER_PASSWORD') || 'dspace'; +// Runs once before the first test in each "block" +beforeEach(() => { + // Pre-agree to all Orejime cookies by setting the orejime-anonymous cookie + // This just ensures it doesn't get in the way of matching other objects in the page. + cy.setCookie('orejime-anonymous', '{"authentication":true,"preferences":true,"acknowledgement":true,"google-analytics":true}'); + + // 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 +// from the Angular UI's config.json. See 'before()' above. +const FALLBACK_TEST_REST_BASE_URL = 'http://localhost:8080/server'; +const FALLBACK_TEST_REST_DOMAIN = 'localhost'; // USEFUL REGEX for testing diff --git a/cypress/support/utils.ts b/cypress/support/utils.ts index 96575969e85..9a9ea1121ba 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 58083003cda..51237b5e954 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 42deb793f91..3dc5fd50550 100644 --- a/docker/README.md +++ b/docker/README.md @@ -20,17 +20,17 @@ 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:dspace-7_x . +docker build -t dspace/dspace-angular:latest . ``` This image is built *automatically* after each commit is made to the `main` branch. Admins to our DockerHub repo can manually publish with the following command. ``` -docker push dspace/dspace-angular:dspace-7_x +docker push dspace/dspace-angular:latest ``` ### Dockerfile.dist @@ -39,18 +39,18 @@ The `Dockerfile.dist` is used to generate a *production* build and runtime envir ```bash # build the latest image -docker build -f Dockerfile.dist -t dspace/dspace-angular:dspace-7_x-dist . +docker build -f Dockerfile.dist -t dspace/dspace-angular:latest-dist . ``` A default/demo version of this image is built *automatically*. ## 'docker' directory - docker-compose.yml - - Starts DSpace Angular with Docker Compose from the current branch. This file assumes that a DSpace 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, @@ -101,25 +101,25 @@ and the backend at http://localhost:8080/server/ ## Run DSpace Angular dist build with DSpace Demo site backend -This allows you to run the Angular UI in *production* mode, pointing it at the demo backend -(https://api7.dspace.org/server/). +This allows you to run the Angular UI in *production* mode, pointing it at the demo or sandbox backend +(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 40e4974c7c7..98f74148610 100644 --- a/docker/cli.assetstore.yml +++ b/docker/cli.assetstore.yml @@ -12,15 +12,8 @@ # 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" - -networks: - dspacenet: - services: dspace-cli: - networks: - dspacenet: {} environment: # This assetstore zip is available from https://github.com/DSpace-Labs/AIP-Files/releases/tag/demo-entities-data - LOADASSETS=https://github.com/DSpace-Labs/AIP-Files/releases/download/demo-entities-data/assetstore.tar.gz diff --git a/docker/cli.ingest.yml b/docker/cli.ingest.yml index 1db241af3bf..31563ccc083 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 54b83d45036..9b1973426f3 100644 --- a/docker/cli.yml +++ b/docker/cli.yml @@ -12,11 +12,16 @@ # 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") + # If COMPOSITE_PROJECT_NAME is missing, default value will be "docker" (name of folder this file is in) + default: + name: ${COMPOSE_PROJECT_NAME:-docker}_dspacenet + external: true services: dspace-cli: - image: "${DOCKER_OWNER:-dspace}/dspace-cli:${DSPACE_VER:-dspace-7_x}" + image: "${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. @@ -30,16 +35,12 @@ services: # solr.server: Ensure we are using the 'dspacesolr' image for Solr solr__P__server: http://dspacesolr:8983/solr volumes: - - "assetstore:/dspace/assetstore" + # Keep DSpace assetstore directory between reboots + - assetstore:/dspace/assetstore entrypoint: /dspace/bin/dspace command: help - networks: - - dspacenet tty: true stdin_open: true volumes: assetstore: - -networks: - dspacenet: diff --git a/docker/db.entities.yml b/docker/db.entities.yml index 6473bf2e385..b3cf5bd86f4 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_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 9ec8fe664a3..c5c419a4a74 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,11 +32,12 @@ 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' + LOGGING_CONFIG: /dspace/config/log4j2-container.xml + image: "${DOCKER_OWNER:-dspace}/dspace:${DSPACE_VER:-latest-test}" depends_on: - dspacedb - image: dspace/dspace:dspace-7_x-test networks: - dspacenet: + - dspacenet ports: - published: 8080 target: 8080 @@ -45,46 +45,45 @@ services: tty: true volumes: - assetstore:/dspace/assetstore - # Mount DSpace's solr configs to a volume, so that we can share to 'dspacesolr' container (see below) - - solr_configs:/dspace/solr # 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_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: + - dspacenet + ports: + - published: 5432 + target: 5432 stdin_open: true tty: true volumes: + # Keep Postgres data directory between reboots - pgdata:/pgdata # DSpace Solr container dspacesolr: container_name: dspacesolr - # Uses official Solr image at https://hub.docker.com/_/solr/ - image: solr:8.11-slim - # Needs main 'dspace' container to start first to guarantee access to solr_configs - depends_on: - - dspace + image: "${DOCKER_OWNER:-dspace}/dspace-solr:${DSPACE_VER:-latest}" networks: - dspacenet: + - dspacenet ports: - published: 8983 target: 8983 @@ -92,9 +91,6 @@ services: tty: true working_dir: /var/solr/data volumes: - # Mount our "solr_configs" volume available under the Solr's configsets folder (in a 'dspace' subfolder) - # This copies the Solr configs from main 'dspace' container into 'dspacesolr' via that volume - - solr_configs:/opt/solr/server/solr/configsets/dspace # Keep Solr data directory between reboots - solr_data:/var/solr/data # Initialize all DSpace Solr cores using the mounted configsets (see above), then start Solr @@ -103,14 +99,20 @@ services: - '-c' - | init-var-solr - precreate-core authority /opt/solr/server/solr/configsets/dspace/authority - precreate-core oai /opt/solr/server/solr/configsets/dspace/oai - precreate-core search /opt/solr/server/solr/configsets/dspace/search - precreate-core statistics /opt/solr/server/solr/configsets/dspace/statistics + precreate-core authority /opt/solr/server/solr/configsets/authority + cp -r /opt/solr/server/solr/configsets/authority/* authority + precreate-core oai /opt/solr/server/solr/configsets/oai + cp -r /opt/solr/server/solr/configsets/oai/* oai + precreate-core search /opt/solr/server/solr/configsets/search + cp -r /opt/solr/server/solr/configsets/search/* search + precreate-core statistics /opt/solr/server/solr/configsets/statistics + cp -r /opt/solr/server/solr/configsets/statistics/* statistics + precreate-core qaevent /opt/solr/server/solr/configsets/qaevent + cp -r /opt/solr/server/solr/configsets/qaevent/* qaevent + precreate-core suggestion /opt/solr/server/solr/configsets/suggestion + cp -r /opt/solr/server/solr/configsets/suggestion/* suggestion exec solr -f volumes: assetstore: pgdata: solr_data: - # Special volume used to share Solr configs from 'dspace' to 'dspacesolr' container (see above) - solr_configs: \ No newline at end of file diff --git a/docker/docker-compose-dist.yml b/docker/docker-compose-dist.yml index 1c75539da97..67eba167852 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: @@ -24,10 +23,10 @@ services: # This is because Server Side Rendering (SSR) currently requires a public URL, # see this bug: https://github.com/DSpace/dspace-angular/issues/1485 DSPACE_REST_SSL: 'true' - DSPACE_REST_HOST: api7.dspace.org + DSPACE_REST_HOST: sandbox.dspace.org DSPACE_REST_PORT: 443 DSPACE_REST_NAMESPACE: /server - image: dspace/dspace-angular:dspace-7_x-dist + image: 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 e5f62600e70..09dfcf2a5f7 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,55 +39,55 @@ 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:-dspace-7_x-test}" + LOGGING_CONFIG: /dspace/config/log4j2-container.xml + image: "${DOCKER_OWNER:-dspace}/dspace:${DSPACE_VER:-latest-test}" depends_on: - dspacedb networks: - dspacenet: + - dspacenet ports: - published: 8080 target: 8080 stdin_open: true tty: true volumes: + # Keep DSpace assetstore directory between reboots - assetstore:/dspace/assetstore - # Mount DSpace's solr configs to a volume, so that we can share to 'dspacesolr' container (see below) - - solr_configs:/dspace/solr # 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_OWNER:-dspace}/dspace-postgres-pgcrypto:${DSPACE_VER:-latest}" environment: PGDATA: /pgdata - image: dspace/dspace-postgres-pgcrypto + POSTGRES_PASSWORD: dspace networks: - dspacenet: + - dspacenet ports: - published: 5432 target: 5432 stdin_open: true tty: true volumes: + # Keep Postgres data directory between reboots - pgdata:/pgdata # DSpace Solr container dspacesolr: container_name: dspacesolr - image: "${DOCKER_OWNER:-dspace}/dspace-solr:${DSPACE_VER:-dspace-7_x}" - # Needs main 'dspace' container to start first to guarantee access to solr_configs - depends_on: - - dspace + image: "${DOCKER_OWNER:-dspace}/dspace-solr:${DSPACE_VER:-latest}" networks: - dspacenet: + - dspacenet ports: - published: 8983 target: 8983 @@ -101,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' @@ -115,10 +115,12 @@ services: cp -r /opt/solr/server/solr/configsets/search/* search precreate-core statistics /opt/solr/server/solr/configsets/statistics cp -r /opt/solr/server/solr/configsets/statistics/* statistics + precreate-core qaevent /opt/solr/server/solr/configsets/qaevent + cp -r /opt/solr/server/solr/configsets/qaevent/* qaevent + precreate-core suggestion /opt/solr/server/solr/configsets/suggestion + cp -r /opt/solr/server/solr/configsets/suggestion/* suggestion exec solr -f volumes: assetstore: pgdata: solr_data: - # Special volume used to share Solr configs from 'dspace' to 'dspacesolr' container (see above) - solr_configs: diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 1387b1de396..1c268b84b7b 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-7_x + image: dspace/dspace-angular:${DSPACE_VER:-latest} build: context: .. dockerfile: Dockerfile diff --git a/docs/Configuration.md b/docs/Configuration.md index 62fa444cc0f..aa6be0f6822 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): @@ -48,7 +48,7 @@ dspace-angular connects to your DSpace installation by using its REST endpoint. ```yaml rest: ssl: true - host: api7.dspace.org + host: demo.dspace.org port: 443 nameSpace: /server } @@ -57,7 +57,7 @@ rest: Alternately you can set the following environment variables. If any of these are set, it will override all configuration files: ``` DSPACE_REST_SSL=true - DSPACE_REST_HOST=api7.dspace.org + DSPACE_REST_HOST=demo.dspace.org DSPACE_REST_PORT=443 DSPACE_REST_NAMESPACE=/server ``` diff --git a/docs/lint/html/index.md b/docs/lint/html/index.md new file mode 100644 index 00000000000..15d693843c0 --- /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 00000000000..a04fe1c770a --- /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 00000000000..ed060c946e8 --- /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 00000000000..1f4ec72801c --- /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 00000000000..f4d0ea177c9 --- /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 00000000000..16ccb701c20 --- /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/karma.conf.js b/karma.conf.js index 8418312b1ab..f96558bfaff 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -15,7 +15,10 @@ module.exports = function (config) { ], client: { clearContext: false, // leave Jasmine Spec Runner output visible in browser - captureConsole: false + captureConsole: false, + jasmine: { + failSpecWithNoExpectations: true + } }, coverageIstanbulReporter: { dir: require('path').join(__dirname, './coverage/dspace-angular'), diff --git a/lint/.gitignore b/lint/.gitignore new file mode 100644 index 00000000000..0d22081b3bc --- /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 00000000000..7251a35c06a --- /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 00000000000..d3f310d23b9 --- /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 00000000000..f19e18756ac --- /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 00000000000..fb2bf53fb58 --- /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 00000000000..dfacd41a96c --- /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 00000000000..7c1370ae2d4 --- /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 00000000000..e907285dbca --- /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 00000000000..a7fdfe41efe --- /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 00000000000..527655adfa4 --- /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 00000000000..c27fd66d662 --- /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 00000000000..83fe6f8ea89 --- /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 00000000000..70ee903fb81 --- /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 00000000000..10408cc316c --- /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 00000000000..49cb60124e8 --- /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 00000000000..2e3aebd9ab4 --- /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 00000000000..d959f292910 --- /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 00000000000..b39d193cc18 --- /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 00000000000..64644145fae --- /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 00000000000..0d04ef1a3d9 --- /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 00000000000..b19ae11b558 --- /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 00000000000..1d4f33f7e28 --- /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 00000000000..1ccbccc5994 --- /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 00000000000..b445040539c --- /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 00000000000..2300ac4a56f --- /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 00000000000..2300ac4a56f --- /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 00000000000..c01f104c989 --- /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 00000000000..a37396ef459 --- /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 00000000000..2697a8c598e --- /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/browse-by/browse-by-metadata-page/browse-by-metadata-page.component.scss b/lint/test/fixture/src/test.ts similarity index 100% rename from src/app/browse-by/browse-by-metadata-page/browse-by-metadata-page.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 00000000000..f72161b2bfc --- /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 00000000000..d2b02ca9f1f --- /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 00000000000..ff6ec3b2c0e --- /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 00000000000..0fd1141ae0e --- /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 00000000000..bd648d007f5 --- /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 00000000000..11c9bec46cf --- /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 00000000000..24e69e42d9a --- /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 00000000000..53faf320699 --- /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 00000000000..2edf9594b62 --- /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 00000000000..d3537a73762 --- /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 00000000000..9bd081ddf83 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,23361 @@ +{ + "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.4", + "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.38.1", + "date-fns": "^2.29.3", + "date-fns-tz": "^1.3.7", + "deepmerge": "^4.3.1", + "ejs": "^3.1.10", + "express": "^4.21.1", + "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.17", + "js-cookie": "2.2.1", + "js-yaml": "^4.1.0", + "json5": "^2.2.3", + "jsonschema": "1.4.1", + "jwt-decode": "^3.1.2", + "lodash": "^4.17.21", + "lru-cache": "^7.14.1", + "markdown-it": "^13.0.1", + "mirador": "^3.3.0", + "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-ui-switch": "^14.1.0", + "nouislider": "^15.7.1", + "orejime": "^2.3.0", + "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.6.0", + "@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.13", + "@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.15.1", + "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.6.0", + "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.1.8", + "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.1", + "ngx-mask": "14.2.4", + "nodemon": "^2.0.22", + "postcss": "^8.4", + "postcss-import": "^14.0.0", + "postcss-loader": "^4.0.3", + "postcss-preset-env": "^7.4.2", + "rimraf": "^3.0.2", + "sass": "~1.80.6", + "sass-loader": "^12.6.0", + "sass-resources-loader": "^2.2.5", + "ts-node": "^8.10.2", + "typescript": "~5.4.5", + "webpack": "5.96.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.4", + "resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.4.tgz", + "integrity": "sha512-eqNHMsxEXuit0sRvvWoGG3/4+Q5qwqjKARWXKM/KoSsKvTNBwWt8pwspg5+TniP3POAZcPPx0O8CiEIQ4e6NWg==", + "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": "~2.5.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": "^4.1.3", + "tunnel-agent": "^0.6.0", + "uuid": "^8.3.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@cypress/request/node_modules/form-data": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz", + "integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==", + "dev": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 0.12" + } + }, + "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.6.0", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.6.0.tgz", + "integrity": "sha512-60G28ke/sXdtS9KZCpZSHHkCbdsOGEhIUGlwq6yhY74UpTiToIh8np7A8yphhM4BWsvNFtIvLpi4co+h9Mr9Ow==", + "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.13", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.13.tgz", + "integrity": "sha512-lfx+dftrEZcdBPczf9d0Qv0x+j/rfNCMuC6OcfXmO8gkfeNAY88PgKUbvG56whcN23gc27yenwF6oJZXGFpYxg==", + "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.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.12.1.tgz", + "integrity": "sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg==", + "dev": true, + "dependencies": { + "@webassemblyjs/helper-numbers": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz", + "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz", + "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.12.1.tgz", + "integrity": "sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz", + "integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==", + "dev": true, + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.11.6", + "@webassemblyjs/helper-api-error": "1.11.6", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz", + "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.12.1.tgz", + "integrity": "sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-buffer": "1.12.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/wasm-gen": "1.12.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz", + "integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==", + "dev": true, + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz", + "integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==", + "dev": true, + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz", + "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==", + "dev": true + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.12.1.tgz", + "integrity": "sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-buffer": "1.12.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/helper-wasm-section": "1.12.1", + "@webassemblyjs/wasm-gen": "1.12.1", + "@webassemblyjs/wasm-opt": "1.12.1", + "@webassemblyjs/wasm-parser": "1.12.1", + "@webassemblyjs/wast-printer": "1.12.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.12.1.tgz", + "integrity": "sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/ieee754": "1.11.6", + "@webassemblyjs/leb128": "1.11.6", + "@webassemblyjs/utf8": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.12.1.tgz", + "integrity": "sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-buffer": "1.12.1", + "@webassemblyjs/wasm-gen": "1.12.1", + "@webassemblyjs/wasm-parser": "1.12.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz", + "integrity": "sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.12.1", + "@webassemblyjs/helper-api-error": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/ieee754": "1.11.6", + "@webassemblyjs/leb128": "1.11.6", + "@webassemblyjs/utf8": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.12.1.tgz", + "integrity": "sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.12.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.7", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", + "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", + "license": "MIT", + "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": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "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.38.1", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.38.1.tgz", + "integrity": "sha512-OP35aUorbU3Zvlx7pjsFdu1rGNnD4pgw/CWoYzRY3t2EzoVT7shKHY1dlAy3f41cGIO7ZDPQimhGFTlEYkG/Hw==", + "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.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "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.15.1", + "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.15.1.tgz", + "integrity": "sha512-DwUFiKXo4lef9kA0M4iEhixFqoqp2hw8igr0lTqafRb9qtU3X0XGxKbkSYsUFdkrAkphc7MPDxoNPhk5pj9PVg==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "@cypress/request": "^3.0.4", + "@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", + "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-ci": "^3.0.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.5.1", + "resolved": "https://registry.npmjs.org/eslint-compat-utils/-/eslint-compat-utils-0.5.1.tgz", + "integrity": "sha512-3z3vFexKIEnjHE3zCMRo6fn/e44U7T1khUjg+Hp0ZQMCigh28rALD0nPFBcGZuiLC5rLZa2ubQHDRln09JfU2Q==", + "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-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.16.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsonc/-/eslint-plugin-jsonc-2.16.0.tgz", + "integrity": "sha512-Af/ZL5mgfb8FFNleH6KlO4/VdmDuTqmM+SPnWcdoWywTetv7kq+vQe99UyQb9XO3b0OWLVuTH7H0d/PXYCMdSg==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "eslint-compat-utils": "^0.5.0", + "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.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "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.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", + "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", + "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.10", + "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" + } + }, + "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.1.8", + "resolved": "https://registry.npmjs.org/express-static-gzip/-/express-static-gzip-2.1.8.tgz", + "integrity": "sha512-g8tiJuI9Y9Ffy59ehVXvqb0hhP83JwZiLxzanobPaMbkB5qBWA8nuVgd+rcd5qzH3GkgogTALlc0BaADYwnMbQ==", + "dev": true, + "dependencies": { + "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-ci": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz", + "integrity": "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==", + "dev": true, + "dependencies": { + "ci-info": "^3.2.0" + }, + "bin": { + "is-ci": "bin.js" + } + }, + "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.17", + "resolved": "https://registry.npmjs.org/isbot/-/isbot-5.1.17.tgz", + "integrity": "sha512-/wch8pRKZE+aoVhRX/hYPY1C7dMCeeMyhkQLNLNlYAbGQn9bkvMB8fOUXNnk5I0m4vDYbBJ9ciVtkr9zfBJ7qA==", + "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.4.1", + "resolved": "https://registry.npmjs.org/jsonschema/-/jsonschema-1.4.1.tgz", + "integrity": "sha512-S6cATIPVv1z0IlxdN+zUk5EPjkGCdnhN4wVSBlvoUO1tOLJootbo9CquNJmbIh4yikWHiUedhRYrNPn1arpEmQ==", + "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.3.0", + "resolved": "https://registry.npmjs.org/mirador/-/mirador-3.3.0.tgz", + "integrity": "sha512-BmGfRnWJ45B+vtiAwcFT7n9nKialfejE9UvuUK0NorO37ShArpsKr3yVSD4jQASwSR4DRRpPEG21jOk4WN7H3w==", + "dependencies": { + "@material-ui/core": "^4.11.0", + "@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", + "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.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "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.1", + "resolved": "https://registry.npmjs.org/ng-mocks/-/ng-mocks-14.13.1.tgz", + "integrity": "sha512-eyfnjXeC108SqVD09i/cBwCpKkK0JjBoAg8jp7oQS2HS081K3WJTttFpgLGeLDYKmZsZ6nYpI+HHNQ3OksaJ7A==", + "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", + "@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", + "@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", + "@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" + } + }, + "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-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.0", + "resolved": "https://registry.npmjs.org/orejime/-/orejime-2.3.0.tgz", + "integrity": "sha512-eDUgd5yEF5pCIUHPNXVDKmgH+4k4GEwxU7eexBFeVESba3ZYDHSDq4ULXiF2LnZwxg+H6Qi28o8rGfGxNeOriA==", + "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.10", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", + "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==" + }, + "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.4.47", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", + "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", + "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.1.0", + "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/psl": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", + "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", + "dev": 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/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "dev": true + }, + "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.80.6", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.80.6.tgz", + "integrity": "sha512-ccZgdHNiBF1NHBsWvacvT5rju3y1d/Eu+8Ex6c21nHp2lZGLBEtuwc415QfiI1PJa1TpCo3iXwwSRjRpn2Ckjg==", + "dev": true, + "dependencies": { + "chokidar": "^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" + }, + "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": "4.3.6", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.6.tgz", + "integrity": "sha512-Ju0+lEMyzMVZarkTn/gqRpdqd5dOPaz1mCZ0SH3JV6iFw81PldE/PEB1hWVEA288HPt4WXW8O7AWxB10M+03QQ==", + "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/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": "4.1.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", + "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", + "dev": true, + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tough-cookie/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/tough-cookie/node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "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/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dev": true, + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "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.96.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.96.1.tgz", + "integrity": "sha512-l2LlBSvVZGhL4ZrPwyr8+37AunkcYj5qh8o6u2/2rzoPc8gxFJkLj1WxNgooi9pnoc06jh0BjuXnamM4qlujZA==", + "dev": true, + "dependencies": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.6", + "@webassemblyjs/ast": "^1.12.1", + "@webassemblyjs/wasm-edit": "^1.12.1", + "@webassemblyjs/wasm-parser": "^1.12.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 719b13b23b6..dec54f98ba1 100644 --- a/package.json +++ b/package.json @@ -1,28 +1,33 @@ { "name": "dspace-angular", - "version": "7.6.0", + "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": "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", - "e2e": "ng e2e", + "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", "clean:dist": "rimraf dist", @@ -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,159 +55,187 @@ "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": "^0.27.2", + "axios": "^1.7.4", "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.38.1", "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.1", "express-rate-limit": "^5.1.3", "fast-json-patch": "^3.1.1", "filesize": "^6.1.0", - "http-proxy-middleware": "^1.0.5", - "isbot": "^3.6.10", + "http-proxy-middleware": "^2.0.7", + "http-terminator": "^3.2.0", + "isbot": "^5.1.17", "js-cookie": "2.2.1", "js-yaml": "^4.1.0", "json5": "^2.2.3", "jsonschema": "1.4.1", "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-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-nouislider": "^1.8.3", - "ngx-infinite-scroll": "^15.0.0", + "ng2-file-upload": "5.0.0", + "ng2-nouislider": "^2.0.0", + "ngx-infinite-scroll": "^16.0.0", "ngx-pagination": "6.0.3", - "ngx-sortablejs": "^11.1.0", - "ngx-ui-switch": "^14.0.3", - "nouislider": "^14.6.3", - "pem": "1.14.7", - "prop-types": "^15.8.1", - "react-copy-to-clipboard": "^5.1.0", - "reflect-metadata": "^0.1.13", + "ngx-ui-switch": "^14.1.0", + "nouislider": "^15.7.1", + "orejime": "^2.3.0", + "pem": "1.14.8", + "reflect-metadata": "^0.2.2", "rxjs": "^7.8.0", - "sanitize-html": "^2.10.0", - "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": "^15.2.6", - "@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.6.0", + "@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.13", "@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.0", + "@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.10.0", - "cypress-axe": "^1.4.0", + "cypress": "^13.15.1", + "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-jsdoc": "^39.6.4", + "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-lodash": "^7.4.0", - "eslint-plugin-unused-imports": "^2.0.0", - "express-static-gzip": "^2.1.7", + "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.1.8", + "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.1", + "ngx-mask": "14.2.4", "nodemon": "^2.0.22", "postcss": "^8.4", - "postcss-apply": "0.12.0", "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.80.6", "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.96.1", + "webpack-cli": "^5.1.4", + "webpack-dev-server": "^4.15.1" } } diff --git a/postcss.config.js b/postcss.config.js index df092d1d39f..f8b9666b312 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 e790828c0db..64442f57884 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 96ba0d40105..170266b6a28 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 23327c2058e..07a05656d87 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,33 +27,37 @@ 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'; +import { createHttpTerminator } from 'http-terminator'; import { readFileSync } from 'fs'; 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 @@ -96,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(); @@ -126,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); /* @@ -161,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, }); }); @@ -176,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, })); /** @@ -185,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, })); /** @@ -196,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); } @@ -235,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) { // 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)'); @@ -251,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); - } - } 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); + 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); + } } - if (sendToUser) { - console.warn('Falling back to serving direct client-side rendering (CSR).'); - clientSideRender(req, res); + }) + .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); + } } - } - }); + next(err); + }); } /** @@ -320,22 +323,23 @@ function initCache() { if (botCacheEnabled()) { // Initialize a new "least-recently-used" item cache (where least recently used pages are removed first) // See https://www.npmjs.com/package/lru-cache - // When enabled, each page defaults to expiring after 1 day + // When enabled, each page defaults to expiring after 1 day (defined in default-app-config.ts) botCache = new LRU( { max: environment.cache.serverSide.botCache.max, - ttl: environment.cache.serverSide.botCache.timeToLive || 24 * 60 * 60 * 1000, // 1 day - allowStale: environment.cache.serverSide.botCache.allowStale ?? true // if object is stale, return stale value before deleting + ttl: environment.cache.serverSide.botCache.timeToLive, + allowStale: environment.cache.serverSide.botCache.allowStale, }); } if (anonymousCacheEnabled()) { // NOTE: While caches may share SSR pages, this cache must be kept separately because the timeToLive // may expire pages more frequently. - // When enabled, each page defaults to expiring after 10 seconds (to minimize anonymous users seeing out-of-date content) + // When enabled, each page defaults to expiring after 10 seconds (defined in default-app-config.ts) + // to minimize anonymous users seeing out-of-date content anonymousCache = new LRU( { max: environment.cache.serverSide.anonymousCache.max, - ttl: environment.cache.serverSide.anonymousCache.timeToLive || 10 * 1000, // 10 seconds - allowStale: environment.cache.serverSide.anonymousCache.allowStale ?? true // if object is stale, return stale value before deleting + ttl: environment.cache.serverSide.anonymousCache.timeToLive, + allowStale: environment.cache.serverSide.anonymousCache.allowStale, }); } } @@ -346,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); } /** @@ -355,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); } /** @@ -368,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. @@ -406,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`); } @@ -423,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.`); } @@ -487,7 +492,7 @@ function saveToCache(req, page: any) { */ function hasNotSucceeded(statusCode) { const rgx = new RegExp(/^20+/); - return !rgx.test(statusCode) + return !rgx.test(statusCode); } function retrieveHeaders(response) { @@ -525,23 +530,46 @@ function serverStarted() { * @param keys SSL credentials */ function createHttpsServer(keys) { - createServer({ + 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 }); + 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'); + })(); + }); } +/** + * Create an HTTP server with the configured port and host. + */ function run() { const port = environment.ui.port || 4000; const host = environment.ui.host || '/'; // Start up the Node server const server = app(); - server.listen(port, host, () => { + const listener = server.listen(port, host, () => { serverStarted(); }); + + // Graceful shutdown when signalled + 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; + })(); + }); } function start() { @@ -572,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.'); @@ -581,7 +609,7 @@ function start() { createCertificate({ days: 1, - selfSigned: true + selfSigned: true, }, (error, keys) => { createHttpsServer(keys); }); @@ -602,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 00000000000..07b6f6c4ff4 --- /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-paths.ts b/src/app/access-control/access-control-routing-paths.ts index 259aa311e74..06ae0321945 100644 --- a/src/app/access-control/access-control-routing-paths.ts +++ b/src/app/access-control/access-control-routing-paths.ts @@ -1,12 +1,22 @@ -import { URLCombiner } from '../core/url-combiner/url-combiner'; import { getAccessControlModuleRoute } from '../app-routing-paths'; +import { URLCombiner } from '../core/url-combiner/url-combiner'; + +export const EPERSON_PATH = 'epeople'; + +export function getEPersonsRoute(): string { + return new URLCombiner(getAccessControlModuleRoute(), EPERSON_PATH).toString(); +} + +export function getEPersonEditRoute(id: string): string { + return new URLCombiner(getEPersonsRoute(), id, 'edit').toString(); +} -export const GROUP_EDIT_PATH = 'groups'; +export const GROUP_PATH = 'groups'; export function getGroupsRoute() { - return new URLCombiner(getAccessControlModuleRoute(), GROUP_EDIT_PATH).toString(); + return new URLCombiner(getAccessControlModuleRoute(), GROUP_PATH).toString(); } export function getGroupEditRoute(id: string) { - return new URLCombiner(getAccessControlModuleRoute(), GROUP_EDIT_PATH, id).toString(); + return new URLCombiner(getGroupsRoute(), id, 'edit').toString(); } 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 6f6de6cb263..00000000000 --- a/src/app/access-control/access-control-routing.module.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { NgModule } from '@angular/core'; -import { RouterModule } from '@angular/router'; -import { EPeopleRegistryComponent } from './epeople-registry/epeople-registry.component'; -import { GroupFormComponent } from './group-registry/group-form/group-form.component'; -import { GroupsRegistryComponent } from './group-registry/groups-registry.component'; -import { GROUP_EDIT_PATH } from './access-control-routing-paths'; -import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver'; -import { GroupPageGuard } from './group-registry/group-page.guard'; -import { - GroupAdministratorGuard -} from '../core/data/feature-authorization/feature-authorization-guard/group-administrator.guard'; -import { - SiteAdministratorGuard -} from '../core/data/feature-authorization/feature-authorization-guard/site-administrator.guard'; -import { BulkAccessComponent } from './bulk-access/bulk-access.component'; - -@NgModule({ - imports: [ - RouterModule.forChild([ - { - path: 'epeople', - component: EPeopleRegistryComponent, - resolve: { - breadcrumb: I18nBreadcrumbResolver - }, - data: { title: 'admin.access-control.epeople.title', breadcrumbKey: 'admin.access-control.epeople' }, - canActivate: [SiteAdministratorGuard] - }, - { - path: GROUP_EDIT_PATH, - component: GroupsRegistryComponent, - resolve: { - breadcrumb: I18nBreadcrumbResolver - }, - data: { title: 'admin.access-control.groups.title', breadcrumbKey: 'admin.access-control.groups' }, - canActivate: [GroupAdministratorGuard] - }, - { - path: `${GROUP_EDIT_PATH}/newGroup`, - component: GroupFormComponent, - resolve: { - breadcrumb: I18nBreadcrumbResolver - }, - data: { title: 'admin.access-control.groups.title.addGroup', breadcrumbKey: 'admin.access-control.groups.addGroup' }, - canActivate: [GroupAdministratorGuard] - }, - { - path: `${GROUP_EDIT_PATH}/:groupId`, - 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 3dc4b6cedc7..00000000000 --- a/src/app/access-control/access-control.module.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { CommonModule } from '@angular/common'; -import { NgModule } from '@angular/core'; -import { RouterModule } from '@angular/router'; -import { SharedModule } from '../shared/shared.module'; -import { AccessControlRoutingModule } from './access-control-routing.module'; -import { EPeopleRegistryComponent } from './epeople-registry/epeople-registry.component'; -import { EPersonFormComponent } from './epeople-registry/eperson-form/eperson-form.component'; -import { GroupFormComponent } from './group-registry/group-form/group-form.component'; -import { MembersListComponent } from './group-registry/group-form/members-list/members-list.component'; -import { SubgroupsListComponent } from './group-registry/group-form/subgroup-list/subgroups-list.component'; -import { GroupsRegistryComponent } from './group-registry/groups-registry.component'; -import { FormModule } from '../shared/form/form.module'; -import { DYNAMIC_ERROR_MESSAGES_MATCHER, DynamicErrorMessagesMatcher } from '@ng-dynamic-forms/core'; -import { AbstractControl } from '@angular/forms'; -import { BulkAccessComponent } from './bulk-access/bulk-access.component'; -import { NgbAccordionModule } from '@ng-bootstrap/ng-bootstrap'; -import { BulkAccessBrowseComponent } from './bulk-access/browse/bulk-access-browse.component'; -import { BulkAccessSettingsComponent } from './bulk-access/settings/bulk-access-settings.component'; -import { SearchModule } from '../shared/search/search.module'; -import { AccessControlFormModule } from '../shared/access-control-form-container/access-control-form.module'; - -/** - * Condition for displaying error messages on email form field - */ -export const ValidateEmailErrorStateMatcher: DynamicErrorMessagesMatcher = - (control: AbstractControl, model: any, hasFocus: boolean) => { - return (control.touched && !hasFocus) || (control.errors?.emailTaken && hasFocus); - }; - -@NgModule({ - imports: [ - CommonModule, - SharedModule, - RouterModule, - AccessControlRoutingModule, - FormModule, - NgbAccordionModule, - SearchModule, - AccessControlFormModule, - ], - exports: [ - MembersListComponent, - ], - declarations: [ - EPeopleRegistryComponent, - EPersonFormComponent, - GroupsRegistryComponent, - GroupFormComponent, - SubgroupsListComponent, - MembersListComponent, - BulkAccessComponent, - BulkAccessBrowseComponent, - BulkAccessSettingsComponent, - ], - providers: [ - { - provide: DYNAMIC_ERROR_MESSAGES_MATCHER, - useValue: ValidateEmailErrorStateMatcher - }, - ] -}) -/** - * This module handles all components related to the access control pages - */ -export class AccessControlModule { - -} 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 c716aedb8b3..f96ddf4a235 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 @@ -1,15 +1,15 @@ -
-
-
+
+
@@ -17,51 +17,52 @@
- -
+
diff --git a/src/app/access-control/bulk-access/browse/bulk-access-browse.component.spec.ts b/src/app/access-control/bulk-access/browse/bulk-access-browse.component.spec.ts index 87b2a8d5684..f9eb487d73a 100644 --- a/src/app/access-control/bulk-access/browse/bulk-access-browse.component.spec.ts +++ b/src/app/access-control/bulk-access/browse/bulk-access-browse.component.spec.ts @@ -1,16 +1,28 @@ -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { NO_ERRORS_SCHEMA } from '@angular/core'; - -import { of } from 'rxjs'; -import { NgbAccordionModule, NgbNavModule } from '@ng-bootstrap/ng-bootstrap'; +import { + ComponentFixture, + TestBed, + waitForAsync, +} from '@angular/core/testing'; +import { + NgbAccordionModule, + NgbNavModule, +} from '@ng-bootstrap/ng-bootstrap'; import { TranslateModule } from '@ngx-translate/core'; +import { of } from 'rxjs'; -import { BulkAccessBrowseComponent } from './bulk-access-browse.component'; +import { buildPaginatedList } from '../../../core/data/paginated-list.model'; +import { PageInfo } from '../../../core/shared/page-info.model'; +import { getMockThemeService } from '../../../shared/mocks/theme-service.mock'; +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 { SelectableListService } from '../../../shared/object-list/selectable-list/selectable-list.service'; import { SelectableObject } from '../../../shared/object-list/selectable-list/selectable-list.service.spec'; -import { PageInfo } from '../../../core/shared/page-info.model'; -import { buildPaginatedList } from '../../../core/data/paginated-list.model'; +import { PaginationComponent } from '../../../shared/pagination/pagination.component'; import { createSuccessfulRemoteDataObject } from '../../../shared/remote-data.utils'; +import { ThemedSearchComponent } from '../../../shared/search/themed-search.component'; +import { ThemeService } from '../../../shared/theme-support/theme.service'; +import { BulkAccessBrowseComponent } from './bulk-access-browse.component'; describe('BulkAccessBrowseComponent', () => { let component: BulkAccessBrowseComponent; @@ -23,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(() => { @@ -31,14 +43,28 @@ describe('BulkAccessBrowseComponent', () => { imports: [ NgbAccordionModule, NgbNavModule, - TranslateModule.forRoot() + TranslateModule.forRoot(), + BulkAccessBrowseComponent, + ], + providers: [ + { provide: SelectableListService, useValue: selectableListService }, + { provide: ThemeService, useValue: getMockThemeService() }, ], - declarations: [BulkAccessBrowseComponent], - providers: [ { provide: SelectableListService, useValue: selectableListService }, ], schemas: [ - NO_ERRORS_SCHEMA - ] - }).compileComponents(); + NO_ERRORS_SCHEMA, + ], + }) + .overrideComponent(BulkAccessBrowseComponent, { + remove: { + imports: [ + PaginationComponent, + ThemedSearchComponent, + SelectableListItemControlComponent, + ListableObjectComponentLoaderComponent, + ], + }, + }) + .compileComponents(); })); beforeEach(() => { @@ -72,8 +98,8 @@ describe('BulkAccessBrowseComponent', () => { 'elementsPerPage': 5, 'totalElements': 2, 'totalPages': 1, - 'currentPage': 1 - }), [selected1, selected2]) ; + 'currentPage': 1, + }), [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 e806e729c8e..a400742f017 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,19 +1,48 @@ -import { Component, Input, OnDestroy, OnInit } from '@angular/core'; - -import { BehaviorSubject, Subscription } from 'rxjs'; -import { distinctUntilChanged, map } from 'rxjs/operators'; - -import { SEARCH_CONFIG_SERVICE } from '../../../my-dspace-page/my-dspace-page.component'; -import { SearchConfigurationService } from '../../../core/shared/search/search-configuration.service'; -import { SelectableListService } from '../../../shared/object-list/selectable-list/selectable-list.service'; -import { SelectableListState } from '../../../shared/object-list/selectable-list/selectable-list.reducer'; +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, +} from 'rxjs'; +import { + distinctUntilChanged, + map, +} from 'rxjs/operators'; + +import { + buildPaginatedList, + PaginatedList, +} from '../../../core/data/paginated-list.model'; import { RemoteData } from '../../../core/data/remote-data'; -import { buildPaginatedList, PaginatedList } from '../../../core/data/paginated-list.model'; -import { ListableObject } from '../../../shared/object-collection/shared/listable-object.model'; -import { createSuccessfulRemoteDataObject } from '../../../shared/remote-data.utils'; import { PageInfo } from '../../../core/shared/page-info.model'; -import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model'; +import { SearchConfigurationService } from '../../../core/shared/search/search-configuration.service'; +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', @@ -22,9 +51,24 @@ import { hasValue } from '../../../shared/empty.util'; providers: [ { provide: SEARCH_CONFIG_SERVICE, - useClass: SearchConfigurationService - } - ] + useClass: SearchConfigurationService, + }, + ], + imports: [ + PaginationComponent, + AsyncPipe, + NgbAccordionModule, + TranslateModule, + NgIf, + NgbNavModule, + ThemedSearchComponent, + BrowserOnlyPipe, + NgForOf, + NgxPaginationModule, + SelectableListItemControlComponent, + ListableObjectComponentLoaderComponent, + ], + standalone: true, }) export class BulkAccessBrowseComponent implements OnInit, OnDestroy { @@ -49,7 +93,7 @@ export class BulkAccessBrowseComponent implements OnInit, OnDestroy { paginationOptions$: BehaviorSubject = new BehaviorSubject(Object.assign(new PaginationComponentOptions(), { id: 'bas', pageSize: 5, - currentPage: 1 + currentPage: 1, })); /** @@ -67,20 +111,20 @@ export class BulkAccessBrowseComponent implements OnInit, OnDestroy { this.subs.push( this.selectableListService.getSelectableList(this.listId).pipe( distinctUntilChanged(), - map((list: SelectableListState) => this.generatePaginatedListBySelectedElements(list)) - ).subscribe(this.objectsSelected$) + map((list: SelectableListState) => this.generatePaginatedListBySelectedElements(list)), + ).subscribe(this.objectsSelected$), ); } pageNext() { this.paginationOptions$.next(Object.assign(new PaginationComponentOptions(), this.paginationOptions$.value, { - currentPage: this.paginationOptions$.value.currentPage + 1 + currentPage: this.paginationOptions$.value.currentPage + 1, })); } pagePrev() { this.paginationOptions$.next(Object.assign(new PaginationComponentOptions(), this.paginationOptions$.value, { - currentPage: this.paginationOptions$.value.currentPage - 1 + currentPage: this.paginationOptions$.value.currentPage - 1, })); } @@ -99,12 +143,12 @@ export class BulkAccessBrowseComponent implements OnInit, OnDestroy { elementsPerPage: this.paginationOptions$.value.pageSize, totalElements: list?.selection.length, totalPages: this.calculatePageCount(this.paginationOptions$.value.pageSize, list?.selection.length), - currentPage: this.paginationOptions$.value.currentPage + currentPage: this.paginationOptions$.value.currentPage, }); if (pageInfo.currentPage > pageInfo.totalPages) { pageInfo.currentPage = pageInfo.totalPages; this.paginationOptions$.next(Object.assign(new PaginationComponentOptions(), this.paginationOptions$.value, { - currentPage: pageInfo.currentPage + currentPage: pageInfo.currentPage, })); } return createSuccessfulRemoteDataObject(buildPaginatedList(pageInfo, list?.selection || [])); diff --git a/src/app/access-control/bulk-access/bulk-access.component.html b/src/app/access-control/bulk-access/bulk-access.component.html index 382caf85f46..c164cc5c319 100644 --- a/src/app/access-control/bulk-access/bulk-access.component.html +++ b/src/app/access-control/bulk-access/bulk-access.component.html @@ -1,4 +1,5 @@
+

{{ 'admin.access-control.bulk-access.title' | translate }}

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 e9b253147dc..8bfbe1fe5d1 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 @@ -1,18 +1,23 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; import { NO_ERRORS_SCHEMA } from '@angular/core'; - +import { + ComponentFixture, + TestBed, +} from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; import { TranslateModule } from '@ngx-translate/core'; import { of } from 'rxjs'; -import { BulkAccessComponent } from './bulk-access.component'; +import { Process } from '../../process-page/processes/process.model'; import { BulkAccessControlService } from '../../shared/access-control-form-container/bulk-access-control.service'; -import { SelectableListService } from '../../shared/object-list/selectable-list/selectable-list.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 { Process } from '../../process-page/processes/process.model'; -import { RouterTestingModule } from '@angular/router/testing'; -import { NotificationsService } from '../../shared/notifications/notifications.service'; 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; @@ -31,35 +36,35 @@ describe('BulkAccessComponent', () => { 'startDate': { 'year': 2026, 'month': 5, - 'day': 31 + 'day': 31, }, - 'endDate': null - } + 'endDate': null, + }, ], 'state': { 'item': { 'toggleStatus': true, - 'accessMode': 'replace' + 'accessMode': 'replace', }, 'bitstream': { 'toggleStatus': false, 'accessMode': '', 'changesLimit': '', - 'selectedBitstreams': [] - } - } + 'selectedBitstreams': [], + }, + }, }; const mockFile = { 'uuids': [ - '1234', '5678' + '1234', '5678', ], - 'file': { } + 'file': { }, }; const mockSettings: any = jasmine.createSpyObj('AccessControlFormContainerComponent', { getValue: jasmine.createSpy('getValue'), - reset: jasmine.createSpy('reset') + reset: jasmine.createSpy('reset'), }); const selection: any[] = [{ indexableObject: { uuid: '1234' } }, { indexableObject: { uuid: '5678' } }]; const selectableListState: SelectableListState = { id: 'test', selection }; @@ -71,16 +76,24 @@ describe('BulkAccessComponent', () => { await TestBed.configureTestingModule({ imports: [ RouterTestingModule, - TranslateModule.forRoot() + TranslateModule.forRoot(), + BulkAccessComponent, ], - declarations: [ BulkAccessComponent ], providers: [ { provide: BulkAccessControlService, useValue: bulkAccessControlServiceMock }, { provide: NotificationsService, useValue: NotificationsServiceStub }, - { provide: SelectableListService, useValue: selectableListServiceMock } + { provide: SelectableListService, useValue: selectableListServiceMock }, + { provide: ThemeService, useValue: getMockThemeService() }, ], - schemas: [NO_ERRORS_SCHEMA] + 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 04724614cb6..bd8e893b599 100644 --- a/src/app/access-control/bulk-access/bulk-access.component.ts +++ b/src/app/access-control/bulk-access/bulk-access.component.ts @@ -1,17 +1,34 @@ -import { Component, OnInit, ViewChild } from '@angular/core'; +import { + Component, + OnInit, + ViewChild, +} from '@angular/core'; +import { TranslateModule } from '@ngx-translate/core'; +import { + BehaviorSubject, + Subscription, +} from 'rxjs'; +import { + distinctUntilChanged, + map, +} from 'rxjs/operators'; -import { BehaviorSubject, Subscription } from 'rxjs'; -import { distinctUntilChanged, map } from 'rxjs/operators'; - -import { BulkAccessSettingsComponent } from './settings/bulk-access-settings.component'; 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'] + styleUrls: ['./bulk-access.component.scss'], + imports: [ + TranslateModule, + BulkAccessSettingsComponent, + BulkAccessBrowseComponent, + ], + standalone: true, }) export class BulkAccessComponent implements OnInit { @@ -37,7 +54,7 @@ export class BulkAccessComponent implements OnInit { constructor( private bulkAccessControlService: BulkAccessControlService, - private selectableListService: SelectableListService + private selectableListService: SelectableListService, ) { } @@ -45,8 +62,8 @@ export class BulkAccessComponent implements OnInit { this.subs.push( this.selectableListService.getSelectableList(this.listId).pipe( distinctUntilChanged(), - map((list: SelectableListState) => this.generateIdListBySelectedElements(list)) - ).subscribe(this.objectsSelected$) + map((list: SelectableListState) => this.generateIdListBySelectedElements(list)), + ).subscribe(this.objectsSelected$), ); } @@ -74,12 +91,12 @@ export class BulkAccessComponent implements OnInit { const { file } = this.bulkAccessControlService.createPayloadFile({ bitstreamAccess, itemAccess, - state: settings.state + state: settings.state, }); this.bulkAccessControlService.executeScript( this.objectsSelected$.value || [], - file + file, ).subscribe(); } diff --git a/src/app/access-control/bulk-access/settings/bulk-access-settings.component.html b/src/app/access-control/bulk-access/settings/bulk-access-settings.component.html index 01f36ef03f4..c41053874e7 100644 --- a/src/app/access-control/bulk-access/settings/bulk-access-settings.component.html +++ b/src/app/access-control/bulk-access/settings/bulk-access-settings.component.html @@ -1,13 +1,13 @@ -
- -
-
+
+
@@ -15,7 +15,7 @@
- + 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 14e0fdefb21..880e1f2472c 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 @@ -1,8 +1,13 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { + ComponentFixture, + TestBed, +} from '@angular/core/testing'; 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'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; describe('BulkAccessSettingsComponent', () => { let component: BulkAccessSettingsComponent; @@ -15,36 +20,39 @@ describe('BulkAccessSettingsComponent', () => { 'startDate': { 'year': 2026, 'month': 5, - 'day': 31 + 'day': 31, }, - 'endDate': null - } + 'endDate': null, + }, ], 'state': { 'item': { 'toggleStatus': true, - 'accessMode': 'replace' + 'accessMode': 'replace', }, 'bitstream': { 'toggleStatus': false, 'accessMode': '', 'changesLimit': '', - 'selectedBitstreams': [] - } - } + 'selectedBitstreams': [], + }, + }, }; const mockControl: any = jasmine.createSpyObj('AccessControlFormContainerComponent', { getFormValue: jasmine.createSpy('getFormValue'), - reset: jasmine.createSpy('reset') + reset: jasmine.createSpy('reset'), }); beforeEach(async () => { await TestBed.configureTestingModule({ - imports: [NgbAccordionModule, TranslateModule.forRoot()], - declarations: [BulkAccessSettingsComponent], - schemas: [NO_ERRORS_SCHEMA] - }).compileComponents(); + imports: [NgbAccordionModule, TranslateModule.forRoot(), BulkAccessSettingsComponent], + schemas: [NO_ERRORS_SCHEMA], + }) + .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 eecc0162451..264cefc7084 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,13 +1,25 @@ -import { Component, ViewChild } from '@angular/core'; +import { NgIf } from '@angular/common'; import { - AccessControlFormContainerComponent -} from '../../../shared/access-control-form-container/access-control-form-container.component'; + 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'; @Component({ selector: 'ds-bulk-access-settings', templateUrl: 'bulk-access-settings.component.html', styleUrls: ['./bulk-access-settings.component.scss'], - exportAs: 'dsBulkSettings' + exportAs: 'dsBulkSettings', + imports: [ + NgbAccordionModule, + TranslateModule, + NgIf, + AccessControlFormContainerComponent, + ], + standalone: true, }) export class BulkAccessSettingsComponent { diff --git a/src/app/access-control/epeople-registry/epeople-registry.actions.ts b/src/app/access-control/epeople-registry/epeople-registry.actions.ts index a07ea37df29..e6e7608ba3f 100644 --- a/src/app/access-control/epeople-registry/epeople-registry.actions.ts +++ b/src/app/access-control/epeople-registry/epeople-registry.actions.ts @@ -1,5 +1,6 @@ /* eslint-disable max-classes-per-file */ import { Action } from '@ngrx/store'; + import { EPerson } from '../../core/eperson/models/eperson.model'; import { type } from '../../shared/ngrx/type'; 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 e3a8e2c590f..b5a26533cfa 100644 --- a/src/app/access-control/epeople-registry/epeople-registry.component.html +++ b/src/app/access-control/epeople-registry/epeople-registry.component.html @@ -2,98 +2,92 @@
- +

{{labelPrefix + 'head' | translate}}

-
+
- - -
- -
-
- -
-
-
- - + + +
+ +
+
+
+ + -
-
-
-
- +
+
+ +
+ - - + + -
- - - - - - - - - - - - - - - - - -
{{labelPrefix + 'table.id' | translate}}{{labelPrefix + 'table.name' | translate}}{{labelPrefix + 'table.email' | translate}}{{labelPrefix + 'table.edit' | translate}}
{{epersonDto.eperson.id}}{{ dsoNameService.getName(epersonDto.eperson) }}{{epersonDto.eperson.email}} -
- - -
-
-
+
+ + + + + + + + + + + + + + + + + +
{{labelPrefix + 'table.id' | translate}}{{labelPrefix + 'table.name' | translate}}{{labelPrefix + 'table.email' | translate}}{{labelPrefix + 'table.edit' | translate}}
{{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 4a09913862f..c636b72d561 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 @@ -1,47 +1,75 @@ -import { Router } from '@angular/router'; -import { Observable, of as observableOf } from 'rxjs'; import { CommonModule } from '@angular/common'; -import { DebugElement, NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { FormsModule, ReactiveFormsModule } from '@angular/forms'; -import { BrowserModule, By } from '@angular/platform-browser'; -import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; -import { TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core'; -import { buildPaginatedList, PaginatedList } from '../../core/data/paginated-list.model'; +import { + DebugElement, + NO_ERRORS_SCHEMA, +} from '@angular/core'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import { + FormsModule, + ReactiveFormsModule, +} from '@angular/forms'; +import { + BrowserModule, + By, +} from '@angular/platform-browser'; +import { Router } from '@angular/router'; +import { RouterTestingModule } from '@angular/router/testing'; +import { + NgbModal, + NgbModule, +} from '@ng-bootstrap/ng-bootstrap'; +import { TranslateModule } from '@ngx-translate/core'; +import { + Observable, + of as observableOf, +} from 'rxjs'; + +import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; +import { FindListOptions } from '../../core/data/find-list-options.model'; +import { + buildPaginatedList, + 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 { 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 { EPeopleRegistryComponent } from './epeople-registry.component'; -import { EPersonMock, EPersonMock2 } from '../../shared/testing/eperson.mock'; +import { PaginationComponent } from '../../shared/pagination/pagination.component'; import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; -import { getMockFormBuilderService } from '../../shared/mocks/form-builder-service.mock'; -import { getMockTranslateService } from '../../shared/mocks/translate.service.mock'; -import { TranslateLoaderMock } from '../../shared/mocks/translate-loader.mock'; +import { + EPersonMock, + EPersonMock2, +} from '../../shared/testing/eperson.mock'; import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub'; -import { RouterStub } from '../../shared/testing/router.stub'; -import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; -import { RequestService } from '../../core/data/request.service'; -import { PaginationService } from '../../core/pagination/pagination.service'; import { PaginationServiceStub } from '../../shared/testing/pagination-service.stub'; -import { FindListOptions } from '../../core/data/find-list-options.model'; +import { EPeopleRegistryComponent } from './epeople-registry.component'; +import { EPersonFormComponent } from './eperson-form/eperson-form.component'; describe('EPeopleRegistryComponent', () => { let component: EPeopleRegistryComponent; let fixture: ComponentFixture; - let translateService: TranslateService; let builderService: FormBuilderService; - let mockEPeople; + let mockEPeople: EPerson[]; let ePersonDataServiceStub: any; let authorizationService: AuthorizationDataService; - let modalService; - - let paginationService; + let modalService: NgbModal; + let paginationService: PaginationServiceStub; - beforeEach(waitForAsync(() => { + beforeEach(waitForAsync(async () => { jasmine.getEnv().allowRespy(true); mockEPeople = [EPersonMock, EPersonMock2]; ePersonDataServiceStub = { @@ -52,7 +80,7 @@ describe('EPeopleRegistryComponent', () => { elementsPerPage: this.allEpeople.length, totalElements: this.allEpeople.length, totalPages: 1, - currentPage: 1 + currentPage: 1, }), this.allEpeople)); }, getActiveEPerson(): Observable { @@ -67,7 +95,7 @@ describe('EPeopleRegistryComponent', () => { elementsPerPage: [result].length, totalElements: [result].length, totalPages: 1, - currentPage: 1 + currentPage: 1, }), [result])); } if (scope === 'metadata') { @@ -76,7 +104,7 @@ describe('EPeopleRegistryComponent', () => { elementsPerPage: this.allEpeople.length, totalElements: this.allEpeople.length, totalPages: 1, - currentPage: 1 + currentPage: 1, }), this.allEpeople)); } const result = this.allEpeople.find((ePerson: EPerson) => { @@ -86,20 +114,20 @@ describe('EPeopleRegistryComponent', () => { elementsPerPage: [result].length, totalElements: [result].length, totalPages: 1, - currentPage: 1 + currentPage: 1, }), [result])); } return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo({ elementsPerPage: this.allEpeople.length, totalElements: this.allEpeople.length, totalPages: 1, - currentPage: 1 + currentPage: 1, }), this.allEpeople)); }, deleteEPerson(ePerson: EPerson): Observable { this.allEpeople = this.allEpeople.filter((ePerson2: EPerson) => { return (ePerson2.uuid !== ePerson.uuid); - }); + }); return observableOf(true); }, editEPerson(ePerson: EPerson) { @@ -113,42 +141,44 @@ describe('EPeopleRegistryComponent', () => { }, getEPeoplePageRouterLink(): string { return '/access-control/epeople'; - } + }, }; authorizationService = jasmine.createSpyObj('authorizationService', { - isAuthorized: observableOf(true) + isAuthorized: observableOf(true), }); builderService = getMockFormBuilderService(); - translateService = getMockTranslateService(); paginationService = new PaginationServiceStub(); TestBed.configureTestingModule({ - imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule, - TranslateModule.forRoot({ - loader: { - provide: TranslateLoader, - useClass: TranslateLoaderMock - } - }), - ], - declarations: [EPeopleRegistryComponent], + 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 } + { provide: PaginationService, useValue: paginationService }, ], - schemas: [NO_ERRORS_SCHEMA] - }).compileComponents(); + schemas: [NO_ERRORS_SCHEMA], + }) + .overrideComponent(EPeopleRegistryComponent, { + remove: { + imports: [ + EPersonFormComponent, + ThemedLoadingComponent, + PaginationComponent, + ], + }, + }) + .compileComponents(); })); beforeEach(() => { fixture = TestBed.createComponent(EPeopleRegistryComponent); component = fixture.componentInstance; - modalService = (component as any).modalService; + modalService = TestBed.inject(NgbModal); spyOn(modalService, 'open').and.returnValue(Object.assign({ componentInstance: Object.assign({ response: observableOf(true) }) })); fixture.detectChanges(); }); @@ -158,10 +188,10 @@ describe('EPeopleRegistryComponent', () => { }); it('should display list of ePeople', () => { - const ePeopleIdsFound = fixture.debugElement.queryAll(By.css('#epeople tr td:first-child')); + const ePeopleIdsFound: DebugElement[] = fixture.debugElement.queryAll(By.css('#epeople tr td:first-child')); expect(ePeopleIdsFound.length).toEqual(2); mockEPeople.map((ePerson: EPerson) => { - expect(ePeopleIdsFound.find((foundEl) => { + expect(ePeopleIdsFound.find((foundEl: DebugElement) => { return (foundEl.nativeElement.textContent.trim() === ePerson.uuid); })).toBeTruthy(); }); @@ -169,7 +199,7 @@ describe('EPeopleRegistryComponent', () => { describe('search', () => { describe('when searching with scope/query (scope metadata)', () => { - let ePeopleIdsFound; + let ePeopleIdsFound: DebugElement[]; beforeEach(fakeAsync(() => { component.search({ scope: 'metadata', query: EPersonMock2.name }); tick(); @@ -179,14 +209,14 @@ describe('EPeopleRegistryComponent', () => { it('should display search result', () => { expect(ePeopleIdsFound.length).toEqual(1); - expect(ePeopleIdsFound.find((foundEl) => { + expect(ePeopleIdsFound.find((foundEl: DebugElement) => { return (foundEl.nativeElement.textContent.trim() === EPersonMock2.uuid); })).toBeTruthy(); }); }); describe('when searching with scope/query (scope email)', () => { - let ePeopleIdsFound; + let ePeopleIdsFound: DebugElement[]; beforeEach(fakeAsync(() => { component.search({ scope: 'email', query: EPersonMock.email }); tick(); @@ -196,43 +226,13 @@ describe('EPeopleRegistryComponent', () => { it('should display search result', () => { expect(ePeopleIdsFound.length).toEqual(1); - expect(ePeopleIdsFound.find((foundEl) => { + expect(ePeopleIdsFound.find((foundEl: DebugElement) => { return (foundEl.nativeElement.textContent.trim() === EPersonMock.uuid); })).toBeTruthy(); }); }); }); - describe('toggleEditEPerson', () => { - describe('when you click on first edit eperson button', () => { - beforeEach(fakeAsync(() => { - const editButtons = fixture.debugElement.queryAll(By.css('.access-control-editEPersonButton')); - editButtons[0].triggerEventHandler('click', { - preventDefault: () => {/**/ - } - }); - tick(); - fixture.detectChanges(); - })); - - it('editEPerson form is toggled', () => { - const ePeopleIds = fixture.debugElement.queryAll(By.css('#epeople tr td:first-child')); - ePersonDataServiceStub.getActiveEPerson().subscribe((activeEPerson: EPerson) => { - if (ePeopleIds[0] && activeEPerson === ePeopleIds[0].nativeElement.textContent) { - expect(component.isEPersonFormShown).toEqual(false); - } else { - expect(component.isEPersonFormShown).toEqual(true); - } - - }); - }); - - it('EPerson search section is hidden', () => { - expect(fixture.debugElement.query(By.css('#search'))).toBeNull(); - }); - }); - }); - describe('deleteEPerson', () => { describe('when you click on first delete eperson button', () => { let ePeopleIdsFoundBeforeDelete; @@ -242,7 +242,7 @@ describe('EPeopleRegistryComponent', () => { const deleteButtons = fixture.debugElement.queryAll(By.css('.access-control-deleteEPersonButton')); deleteButtons[0].triggerEventHandler('click', { preventDefault: () => {/**/ - } + }, }); tick(); fixture.detectChanges(); @@ -258,19 +258,12 @@ describe('EPeopleRegistryComponent', () => { }); }); - describe('delete EPerson button when the isAuthorized returns false', () => { - let ePeopleDeleteButton; - beforeEach(() => { - spyOn(authorizationService, 'isAuthorized').and.returnValue(observableOf(false)); - component.initialisePage(); - fixture.detectChanges(); - }); - it('should be disabled', () => { - ePeopleDeleteButton = fixture.debugElement.queryAll(By.css('#epeople tr td div button.delete-button')); - ePeopleDeleteButton.forEach((deleteButton: DebugElement) => { - expect(deleteButton.nativeElement.disabled).toBe(true); - }); - }); + it('should hide delete EPerson button when the isAuthorized returns false', () => { + spyOn(authorizationService, 'isAuthorized').and.returnValue(observableOf(false)); + component.initialisePage(); + fixture.detectChanges(); + + expect(fixture.debugElement.query(By.css('#epeople tr td div button.delete-button'))).toBeNull(); }); }); 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 fb045ebb883..6b62a13ecf1 100644 --- a/src/app/access-control/epeople-registry/epeople-registry.component.ts +++ b/src/app/access-control/epeople-registry/epeople-registry.component.ts @@ -1,31 +1,86 @@ -import { Component, OnDestroy, OnInit } from '@angular/core'; -import { UntypedFormBuilder } from '@angular/forms'; -import { Router } from '@angular/router'; -import { TranslateService } from '@ngx-translate/core'; -import { BehaviorSubject, combineLatest, Observable, Subscription } from 'rxjs'; -import { map, switchMap, take } from 'rxjs/operators'; -import { buildPaginatedList, PaginatedList } from '../../core/data/paginated-list.model'; +import { + AsyncPipe, + NgClass, + NgForOf, + NgIf, +} from '@angular/common'; +import { + Component, + OnDestroy, + OnInit, +} from '@angular/core'; +import { + ReactiveFormsModule, + UntypedFormBuilder, +} from '@angular/forms'; +import { + Router, + RouterModule, +} from '@angular/router'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import { + TranslateModule, + TranslateService, +} from '@ngx-translate/core'; +import { + BehaviorSubject, + combineLatest, + Observable, + Subscription, +} from 'rxjs'; +import { + map, + switchMap, + take, +} from 'rxjs/operators'; + +import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; +import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; +import { FeatureID } from '../../core/data/feature-authorization/feature-id'; +import { + buildPaginatedList, + 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 { EPerson } from '../../core/eperson/models/eperson.model'; +import { EpersonDtoModel } from '../../core/eperson/models/eperson-dto.model'; +import { PaginationService } from '../../core/pagination/pagination.service'; +import { NoContent } from '../../core/shared/NoContent.model'; +import { + getAllSucceededRemoteData, + getFirstCompletedRemoteData, +} from '../../core/shared/operators'; +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 { EpersonDtoModel } from '../../core/eperson/models/eperson-dto.model'; -import { FeatureID } from '../../core/data/feature-authorization/feature-id'; -import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; -import { getAllSucceededRemoteData, getFirstCompletedRemoteData } from '../../core/shared/operators'; -import { ConfirmationModalComponent } from '../../shared/confirmation-modal/confirmation-modal.component'; -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { RequestService } from '../../core/data/request.service'; -import { PageInfo } from '../../core/shared/page-info.model'; -import { NoContent } from '../../core/shared/NoContent.model'; -import { PaginationService } from '../../core/pagination/pagination.service'; -import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; +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. @@ -45,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 */ @@ -61,14 +118,9 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy { config: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), { id: 'elp', pageSize: 5, - currentPage: 1 + currentPage: 1, }); - /** - * Whether or not to show the EPerson form - */ - isEPersonFormShown: boolean; - // The search form searchForm; @@ -114,26 +166,21 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy { */ initialisePage() { this.searching$.next(true); - this.isEPersonFormShown = false; - this.search({scope: this.currentSearchScope, query: this.currentSearchQuery}); - this.subs.push(this.epersonService.getActiveEPerson().subscribe((eperson: EPerson) => { - if (eperson != null && eperson.id) { - this.isEPersonFormShown = 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) { - return combineLatest([...epeople.page.map((eperson: EPerson) => { + return combineLatest(epeople.page.map((eperson: EPerson) => { return this.authorizationService.isAuthorized(FeatureID.CanDelete, hasValue(eperson) ? eperson.self : undefined).pipe( map((authorized) => { const epersonDtoModel: EpersonDtoModel = new EpersonDtoModel(); epersonDtoModel.ableToDelete = authorized; epersonDtoModel.eperson = eperson; return epersonDtoModel; - }) + }), ); - })]).pipe(map((dtos: EpersonDtoModel[]) => { + })).pipe(map((dtos: EpersonDtoModel[]) => { return buildPaginatedList(epeople.pageInfo, dtos); })); } else { @@ -157,78 +204,44 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy { } this.findListOptionsSub = this.paginationService.getCurrentPagination(this.config.id, this.config).pipe( switchMap((findListOptions) => { - const query: string = data.query; - const scope: string = data.scope; - if (query != null && this.currentSearchQuery !== query) { - this.router.navigate([this.epersonService.getEPeoplePageRouterLink()], { - queryParamsHandling: 'merge' - }); - this.currentSearchQuery = query; - this.paginationService.resetPage(this.config.id); - } - if (scope != null && this.currentSearchScope !== scope) { - this.router.navigate([this.epersonService.getEPeoplePageRouterLink()], { - queryParamsHandling: 'merge' - }); - this.currentSearchScope = scope; - this.paginationService.resetPage(this.config.id); - - } - return this.epersonService.searchByScope(this.currentSearchScope, this.currentSearchQuery, { - currentPage: findListOptions.currentPage, - elementsPerPage: findListOptions.pageSize + const query: string = data.query; + const scope: string = data.scope; + if (query != null && this.currentSearchQuery !== query) { + void this.router.navigate([getEPersonsRoute()], { + queryParamsHandling: 'merge', }); + this.currentSearchQuery = query; + this.paginationService.resetPage(this.config.id); + } + if (scope != null && this.currentSearchScope !== scope) { + void this.router.navigate([getEPersonsRoute()], { + queryParamsHandling: 'merge', + }); + this.currentSearchScope = scope; + this.paginationService.resetPage(this.config.id); + } + return this.epersonService.searchByScope(this.currentSearchScope, this.currentSearchQuery, { + currentPage: findListOptions.currentPage, + elementsPerPage: findListOptions.pageSize, + }); + }, ), getAllSucceededRemoteData(), ).subscribe((peopleRD) => { - this.ePeople$.next(peopleRD.payload); - this.pageInfoState$.next(peopleRD.payload.pageInfo); - } + this.ePeople$.next(peopleRD.payload); + this.pageInfoState$.next(peopleRD.payload.pageInfo); + }, ); } - /** - * 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(); - } - - /** - * Start editing the selected EPerson - * @param ePerson - */ - toggleEditEPerson(ePerson: EPerson) { - this.getActiveEPerson().pipe(take(1)).subscribe((activeEPerson: EPerson) => { - if (ePerson === activeEPerson) { - this.epersonService.cancelEditEPerson(); - this.isEPersonFormShown = false; - } else { - this.epersonService.editEPerson(ePerson); - this.isEPersonFormShown = true; - } - }); - this.scrollToTop(); - } - /** * Deletes EPerson, show notification on success/failure & updates EPeople list */ deleteEPerson(ePerson: EPerson) { if (hasValue(ePerson.id)) { const modalRef = this.modalService.open(ConfirmationModalComponent); - modalRef.componentInstance.dso = ePerson; + modalRef.componentInstance.name = this.dsoNameService.getName(ePerson); modalRef.componentInstance.headerLabel = 'confirmation-modal.delete-eperson.header'; modalRef.componentInstance.infoLabel = 'confirmation-modal.delete-eperson.info'; modalRef.componentInstance.cancelLabel = 'confirmation-modal.delete-eperson.cancel'; @@ -240,9 +253,9 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy { if (hasValue(ePerson.id)) { this.epersonService.deleteEPerson(ePerson).pipe(getFirstCompletedRemoteData()).subscribe((restResponse: RemoteData) => { if (restResponse.hasSucceeded) { - this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.deleted.success', {name: this.dsoNameService.getName(ePerson)})); + this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.deleted.success', { name: this.dsoNameService.getName(ePerson) })); } else { - this.notificationsService.error('Error occured when trying to delete EPerson with id: ' + ePerson.id + ' with code: ' + restResponse.statusCode + ' and message: ' + restResponse.errorMessage); + this.notificationsService.error(this.translateService.get(this.labelPrefix + 'notification.deleted.success', { id: ePerson.id, statusCode: restResponse.statusCode, errorMessage: restResponse.errorMessage })); } }); } @@ -264,16 +277,6 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy { this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe()); } - scrollToTop() { - (function smoothscroll() { - const currentScroll = document.documentElement.scrollTop || document.body.scrollTop; - if (currentScroll > 0) { - window.requestAnimationFrame(smoothscroll); - window.scrollTo(0, currentScroll - (currentScroll / 8)); - } - })(); - } - /** * Reset all input-fields to be empty and search all search */ @@ -281,23 +284,10 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy { this.searchForm.patchValue({ query: '', }); - this.search({query: ''}); + this.search({ query: '' }); } - /** - * This method will set everything to stale, which will cause the lists on this page to update. - */ - reset(): void { - this.epersonService.getBrowseEndpoint().pipe( - take(1), - switchMap((href: string) => { - return this.requestService.setStaleByHrefSubstring(href).pipe( - take(1), - ); - }) - ).subscribe(()=>{ - this.epersonService.cancelEditEPerson(); - this.isEPersonFormShown = false; - }); + getEditEPeoplePage(id: string): string { + return getEPersonEditRoute(id); } } diff --git a/src/app/access-control/epeople-registry/epeople-registry.reducers.spec.ts b/src/app/access-control/epeople-registry/epeople-registry.reducers.spec.ts index 7158acc79b4..6bee3f84e21 100644 --- a/src/app/access-control/epeople-registry/epeople-registry.reducers.spec.ts +++ b/src/app/access-control/epeople-registry/epeople-registry.reducers.spec.ts @@ -1,6 +1,12 @@ -import { EPeopleRegistryCancelEPersonAction, EPeopleRegistryEditEPersonAction } from './epeople-registry.actions'; -import { ePeopleRegistryReducer, EPeopleRegistryState } from './epeople-registry.reducers'; import { EPersonMock } from '../../shared/testing/eperson.mock'; +import { + EPeopleRegistryCancelEPersonAction, + EPeopleRegistryEditEPersonAction, +} from './epeople-registry.actions'; +import { + ePeopleRegistryReducer, + EPeopleRegistryState, +} from './epeople-registry.reducers'; const initialState: EPeopleRegistryState = { editEPerson: null, diff --git a/src/app/access-control/epeople-registry/epeople-registry.reducers.ts b/src/app/access-control/epeople-registry/epeople-registry.reducers.ts index 1e0319f3ba4..3bab6769e12 100644 --- a/src/app/access-control/epeople-registry/epeople-registry.reducers.ts +++ b/src/app/access-control/epeople-registry/epeople-registry.reducers.ts @@ -2,7 +2,7 @@ import { EPerson } from '../../core/eperson/models/eperson.model'; import { EPeopleRegistryAction, EPeopleRegistryActionTypes, - EPeopleRegistryEditEPersonAction + EPeopleRegistryEditEPersonAction, } from './epeople-registry.actions'; /** @@ -30,13 +30,13 @@ export function ePeopleRegistryReducer(state = initialState, action: EPeopleRegi case EPeopleRegistryActionTypes.EDIT_EPERSON: { return Object.assign({}, state, { - editEPerson: (action as EPeopleRegistryEditEPersonAction).eperson + editEPerson: (action as EPeopleRegistryEditEPersonAction).eperson, }); } case EPeopleRegistryActionTypes.CANCEL_EDIT_EPERSON: { return Object.assign({}, state, { - editEPerson: null + editEPerson: null, }); } 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 228449a8a57..ae1046be452 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 @@ -1,89 +1,98 @@ -
+
+
+
- -

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

-
+
- -

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

-
+ +

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

+
- -
- -
-
- -
-
- - -
- -
+ +

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

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

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

- + -
- - - - - - - - - - - - - - - -
{{messagePrefix + '.table.id' | translate}}{{messagePrefix + '.table.name' | translate}}{{messagePrefix + '.table.collectionOrCommunity' | translate}}
{{group.id}} - - {{ dsoNameService.getName(group) }} - - {{ dsoNameService.getName(undefined) }}
-
+ + +
+ + + + + + + + + + + + + + + +
{{messagePrefix + '.table.id' | translate}}{{messagePrefix + '.table.name' | translate}}{{messagePrefix + '.table.collectionOrCommunity' | translate}}
{{group.id}} + + {{ dsoNameService.getName(group) }} + + + {{ 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 fb911e709c4..e61f95d6e5c 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,36 +1,70 @@ -import { Observable, of as observableOf } from 'rxjs'; import { CommonModule } from '@angular/common'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { UntypedFormControl, UntypedFormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms'; -import { BrowserModule, By } from '@angular/platform-browser'; +import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; +import { + ComponentFixture, + TestBed, + waitForAsync, +} from '@angular/core/testing'; +import { + FormsModule, + ReactiveFormsModule, + UntypedFormControl, + UntypedFormGroup, + Validators, +} from '@angular/forms'; +import { + BrowserModule, + By, +} from '@angular/platform-browser'; +import { + ActivatedRoute, + Router, + RouterModule, +} from '@angular/router'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; -import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; -import { buildPaginatedList, PaginatedList } from '../../../core/data/paginated-list.model'; +import { TranslateModule } from '@ngx-translate/core'; +import { + Observable, + of as observableOf, +} from 'rxjs'; + +import { AuthService } from '../../../core/auth/auth.service'; +import { EpersonRegistrationService } from '../../../core/data/eperson-registration.service'; +import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service'; +import { FindListOptions } from '../../../core/data/find-list-options.model'; +import { + buildPaginatedList, + 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 { 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 { NotificationsService } from '../../../shared/notifications/notifications.service'; -import { EPeopleRegistryComponent } from '../epeople-registry.component'; -import { EPersonFormComponent } from './eperson-form.component'; -import { EPersonMock, EPersonMock2 } from '../../../shared/testing/eperson.mock'; +import { PaginationComponent } from '../../../shared/pagination/pagination.component'; import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; -import { getMockFormBuilderService } from '../../../shared/mocks/form-builder-service.mock'; -import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub'; -import { TranslateLoaderMock } from '../../../shared/mocks/translate-loader.mock'; -import { AuthService } from '../../../core/auth/auth.service'; +import { ActivatedRouteStub } from '../../../shared/testing/active-router.stub'; import { AuthServiceStub } from '../../../shared/testing/auth-service.stub'; -import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service'; -import { GroupDataService } from '../../../core/eperson/group-data.service'; -import { createPaginatedList } from '../../../shared/testing/utils.test'; -import { RequestService } from '../../../core/data/request.service'; -import { PaginationService } from '../../../core/pagination/pagination.service'; +import { + EPersonMock, + EPersonMock2, +} from '../../../shared/testing/eperson.mock'; +import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub'; import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub'; -import { FindListOptions } from '../../../core/data/find-list-options.model'; +import { RouterStub } from '../../../shared/testing/router.stub'; +import { createPaginatedList } from '../../../shared/testing/utils.test'; +import { FollowLinkConfig } from '../../../shared/utils/follow-link-config.model'; +import { HasNoValuePipe } from '../../../shared/utils/has-no-value.pipe'; +import { EPeopleRegistryComponent } from '../epeople-registry.component'; +import { EPersonFormComponent } from './eperson-form.component'; import { ValidateEmailNotTaken } from './validators/email-taken.validator'; -import { EpersonRegistrationService } from '../../../core/data/eperson-registration.service'; describe('EPersonFormComponent', () => { let component: EPersonFormComponent; @@ -43,6 +77,8 @@ describe('EPersonFormComponent', () => { let authorizationService: AuthorizationDataService; let groupsDataService: GroupDataService; let epersonRegistrationService: EpersonRegistrationService; + let route: ActivatedRouteStub; + let router: RouterStub; let paginationService; @@ -53,9 +89,6 @@ describe('EPersonFormComponent', () => { ePersonDataServiceStub = { activeEPerson: null, allEpeople: mockEPeople, - getEPeople(): Observable>> { - return createSuccessfulRemoteDataObject$(buildPaginatedList(null, this.allEpeople)); - }, getActiveEPerson(): Observable { return observableOf(this.activeEPerson); }, @@ -106,70 +139,73 @@ describe('EPersonFormComponent', () => { }, getEPersonByEmail(email): Observable> { return createSuccessfulRemoteDataObject$(null); - } + }, + findById(_id: string, _useCachedVersionIfAvailable = true, _reRequestOnStale = true, ..._linksToFollow: FollowLinkConfig[]): Observable> { + return createSuccessfulRemoteDataObject$(null); + }, }; builderService = Object.assign(getMockFormBuilderService(),{ createFormGroup(formModel, options = null) { const controls = {}; formModel.forEach( model => { - model.parent = parent; - const controlModel = model; - const controlState = { value: controlModel.value, disabled: controlModel.disabled }; - const controlOptions = this.createAbstractControlOptions(controlModel.validators, controlModel.asyncValidators, controlModel.updateOn); - controls[model.id] = new UntypedFormControl(controlState, controlOptions); + model.parent = parent; + const controlModel = model; + const controlState = { value: controlModel.value, disabled: controlModel.disabled }; + const controlOptions = this.createAbstractControlOptions(controlModel.validators, controlModel.asyncValidators, controlModel.updateOn); + controls[model.id] = new UntypedFormControl(controlState, controlOptions); }); return new UntypedFormGroup(controls, options); }, createAbstractControlOptions(validatorsConfig = null, asyncValidatorsConfig = null, updateOn = null) { return { - validators: validatorsConfig !== null ? this.getValidators(validatorsConfig) : null, + validators: validatorsConfig !== null ? this.getValidators(validatorsConfig) : null, }; }, getValidators(validatorsConfig) { - return this.getValidatorFns(validatorsConfig); + return this.getValidatorFns(validatorsConfig); }, getValidatorFns(validatorsConfig, validatorsToken = this._NG_VALIDATORS) { let validatorFns = []; if (this.isObject(validatorsConfig)) { - validatorFns = Object.keys(validatorsConfig).map(validatorConfigKey => { - const validatorConfigValue = validatorsConfig[validatorConfigKey]; - if (this.isValidatorDescriptor(validatorConfigValue)) { - const descriptor = validatorConfigValue; - return this.getValidatorFn(descriptor.name, descriptor.args, validatorsToken); - } - return this.getValidatorFn(validatorConfigKey, validatorConfigValue, validatorsToken); - }); + validatorFns = Object.keys(validatorsConfig).map(validatorConfigKey => { + const validatorConfigValue = validatorsConfig[validatorConfigKey]; + if (this.isValidatorDescriptor(validatorConfigValue)) { + const descriptor = validatorConfigValue; + return this.getValidatorFn(descriptor.name, descriptor.args, validatorsToken); + } + return this.getValidatorFn(validatorConfigKey, validatorConfigValue, validatorsToken); + }); } return validatorFns; }, getValidatorFn(validatorName, validatorArgs = null, validatorsToken = this._NG_VALIDATORS) { let validatorFn; if (Validators.hasOwnProperty(validatorName)) { // Built-in Angular Validators - validatorFn = Validators[validatorName]; + validatorFn = Validators[validatorName]; } else { // Custom Validators - if (this._DYNAMIC_VALIDATORS && this._DYNAMIC_VALIDATORS.has(validatorName)) { - validatorFn = this._DYNAMIC_VALIDATORS.get(validatorName); - } else if (validatorsToken) { - validatorFn = validatorsToken.find(validator => validator.name === validatorName); - } + if (this._DYNAMIC_VALIDATORS && this._DYNAMIC_VALIDATORS.has(validatorName)) { + validatorFn = this._DYNAMIC_VALIDATORS.get(validatorName); + } else if (validatorsToken) { + validatorFn = validatorsToken.find(validator => validator.name === validatorName); + } } if (validatorFn === undefined) { // throw when no validator could be resolved - throw new Error(`validator '${validatorName}' is not provided via NG_VALIDATORS, NG_ASYNC_VALIDATORS or DYNAMIC_FORM_VALIDATORS`); + throw new Error(`validator '${validatorName}' is not provided via NG_VALIDATORS, NG_ASYNC_VALIDATORS or DYNAMIC_FORM_VALIDATORS`); } if (validatorArgs !== null) { - return validatorFn(validatorArgs); + return validatorFn(validatorArgs); } return validatorFn; - }, + }, isValidatorDescriptor(value) { - if (this.isObject(value)) { - return value.hasOwnProperty('name') && value.hasOwnProperty('args'); - } - return false; + if (this.isObject(value)) { + return value.hasOwnProperty('name') && value.hasOwnProperty('args'); + } + return false; }, isObject(value) { return typeof value === 'object' && value !== null; - } + }, }); authService = new AuthServiceStub(); authorizationService = jasmine.createSpyObj('authorizationService', { @@ -178,20 +214,19 @@ describe('EPersonFormComponent', () => { }); groupsDataService = jasmine.createSpyObj('groupsDataService', { findListByHref: createSuccessfulRemoteDataObject$(createPaginatedList([])), - getGroupRegistryRouterLink: '' + getGroupRegistryRouterLink: '', }); paginationService = new PaginationServiceStub(); + route = new ActivatedRouteStub(); + router = new RouterStub(); TestBed.configureTestingModule({ imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule, - TranslateModule.forRoot({ - loader: { - provide: TranslateLoader, - useClass: TranslateLoaderMock - } - }), + RouterModule.forRoot([]), + TranslateModule.forRoot(), + EPersonFormComponent, + HasNoValuePipe, ], - declarations: [EPersonFormComponent], providers: [ { provide: EPersonDataService, useValue: ePersonDataServiceStub }, { provide: GroupDataService, useValue: groupsDataService }, @@ -200,16 +235,22 @@ describe('EPersonFormComponent', () => { { provide: AuthService, useValue: authService }, { provide: AuthorizationDataService, useValue: authorizationService }, { provide: PaginationService, useValue: paginationService }, - { provide: RequestService, useValue: jasmine.createSpyObj('requestService', ['removeByHrefSubstring'])}, + { provide: RequestService, useValue: jasmine.createSpyObj('requestService', ['removeByHrefSubstring']) }, { provide: EpersonRegistrationService, useValue: epersonRegistrationService }, - EPeopleRegistryComponent + { provide: ActivatedRoute, useValue: route }, + { 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', { - registerEmail: createSuccessfulRemoteDataObject$(null) + registerEmail: createSuccessfulRemoteDataObject$(null), }); beforeEach(() => { @@ -223,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; @@ -263,24 +280,18 @@ describe('EPersonFormComponent', () => { fixture.detectChanges(); }); describe('firstName, lastName and email should be required', () => { - it('form should be invalid because the firstName is required', waitForAsync(() => { - fixture.whenStable().then(() => { - expect(component.formGroup.controls.firstName.valid).toBeFalse(); - expect(component.formGroup.controls.firstName.errors.required).toBeTrue(); - }); - })); - it('form should be invalid because the lastName is required', waitForAsync(() => { - fixture.whenStable().then(() => { - expect(component.formGroup.controls.lastName.valid).toBeFalse(); - expect(component.formGroup.controls.lastName.errors.required).toBeTrue(); - }); - })); - it('form should be invalid because the email is required', waitForAsync(() => { - fixture.whenStable().then(() => { - expect(component.formGroup.controls.email.valid).toBeFalse(); - expect(component.formGroup.controls.email.errors.required).toBeTrue(); - }); - })); + it('form should be invalid because the firstName is required', () => { + expect(component.formGroup.controls.firstName.valid).toBeFalse(); + expect(component.formGroup.controls.firstName.errors.required).toBeTrue(); + }); + it('form should be invalid because the lastName is required', () => { + expect(component.formGroup.controls.lastName.valid).toBeFalse(); + expect(component.formGroup.controls.lastName.errors.required).toBeTrue(); + }); + it('form should be invalid because the email is required', () => { + expect(component.formGroup.controls.email.valid).toBeFalse(); + expect(component.formGroup.controls.email.errors.required).toBeTrue(); + }); }); describe('after inserting information firstName,lastName and email not required', () => { @@ -290,24 +301,18 @@ describe('EPersonFormComponent', () => { component.formGroup.controls.email.setValue('test@test.com'); fixture.detectChanges(); }); - it('firstName should be valid because the firstName is set', waitForAsync(() => { - fixture.whenStable().then(() => { - expect(component.formGroup.controls.firstName.valid).toBeTrue(); - expect(component.formGroup.controls.firstName.errors).toBeNull(); - }); - })); - it('lastName should be valid because the lastName is set', waitForAsync(() => { - fixture.whenStable().then(() => { - expect(component.formGroup.controls.lastName.valid).toBeTrue(); - expect(component.formGroup.controls.lastName.errors).toBeNull(); - }); - })); - it('email should be valid because the email is set', waitForAsync(() => { - fixture.whenStable().then(() => { - expect(component.formGroup.controls.email.valid).toBeTrue(); - expect(component.formGroup.controls.email.errors).toBeNull(); - }); - })); + it('firstName should be valid because the firstName is set', () => { + expect(component.formGroup.controls.firstName.valid).toBeTrue(); + expect(component.formGroup.controls.firstName.errors).toBeNull(); + }); + it('lastName should be valid because the lastName is set', () => { + expect(component.formGroup.controls.lastName.valid).toBeTrue(); + expect(component.formGroup.controls.lastName.errors).toBeNull(); + }); + it('email should be valid because the email is set', () => { + expect(component.formGroup.controls.email.valid).toBeTrue(); + expect(component.formGroup.controls.email.errors).toBeNull(); + }); }); @@ -316,12 +321,10 @@ describe('EPersonFormComponent', () => { component.formGroup.controls.email.setValue('test@test'); fixture.detectChanges(); }); - it('email should not be valid because the email pattern', waitForAsync(() => { - fixture.whenStable().then(() => { - expect(component.formGroup.controls.email.valid).toBeFalse(); - expect(component.formGroup.controls.email.errors.pattern).toBeTruthy(); - }); - })); + it('email should not be valid because the email pattern', () => { + expect(component.formGroup.controls.email.valid).toBeFalse(); + expect(component.formGroup.controls.email.errors.pattern).toBeTruthy(); + }); }); describe('after already utilized email', () => { @@ -329,29 +332,25 @@ describe('EPersonFormComponent', () => { const ePersonServiceWithEperson = Object.assign(ePersonDataServiceStub,{ getEPersonByEmail(): Observable> { return createSuccessfulRemoteDataObject$(EPersonMock); - } + }, }); component.formGroup.controls.email.setValue('test@test.com'); component.formGroup.controls.email.setAsyncValidators(ValidateEmailNotTaken.createValidator(ePersonServiceWithEperson)); fixture.detectChanges(); }); - it('email should not be valid because email is already taken', waitForAsync(() => { - fixture.whenStable().then(() => { - expect(component.formGroup.controls.email.valid).toBeFalse(); - expect(component.formGroup.controls.email.errors.emailTaken).toBeTruthy(); - }); - })); + it('email should not be valid because email is already taken', () => { + expect(component.formGroup.controls.email.valid).toBeFalse(); + 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; @@ -366,12 +365,12 @@ describe('EPersonFormComponent', () => { metadata: { 'eperson.firstname': [ { - value: firstName - } + value: firstName, + }, ], 'eperson.lastname': [ { - value: lastName + value: lastName, }, ], }, @@ -380,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; @@ -393,11 +393,9 @@ describe('EPersonFormComponent', () => { fixture.detectChanges(); }); - it('should emit a new eperson using the correct values', waitForAsync(() => { - fixture.whenStable().then(() => { - expect(component.submitForm.emit).toHaveBeenCalledWith(expected); - }); - })); + it('should emit a new eperson using the correct values', () => { + expect(component.submitForm.emit).toHaveBeenCalledWith(expected); + }); }); describe('with an active eperson', () => { @@ -409,30 +407,36 @@ describe('EPersonFormComponent', () => { metadata: { 'eperson.firstname': [ { - value: firstName - } + value: firstName, + }, ], 'eperson.lastname': [ { - value: lastName + value: lastName, }, ], }, 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(); }); - it('should emit the existing eperson using the correct values', waitForAsync(() => { - fixture.whenStable().then(() => { - expect(component.submitForm.emit).toHaveBeenCalledWith(expectedWithId); - }); - })); + it('should emit the existing eperson using the correct values', () => { + expect(component.submitForm.emit).toHaveBeenCalledWith(expectedWithId); + }); }); }); @@ -443,7 +447,7 @@ describe('EPersonFormComponent', () => { spyOn(authService, 'impersonate').and.callThrough(); ePersonId = 'testEPersonId'; component.epersonInitial = Object.assign(new EPerson(), { - id: ePersonId + id: ePersonId, }); component.impersonate(); }); @@ -473,34 +477,31 @@ 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 active if the eperson can be deleted', () => { + it('the delete button should be visible if the ePerson can be deleted', () => { const deleteButton = fixture.debugElement.query(By.css('.delete-button')); - expect(deleteButton.nativeElement.disabled).toBe(false); + expect(deleteButton).not.toBeNull(); }); - it('the delete button should be disabled if the eperson cannot be deleted', () => { + it('the delete button should be hidden if the ePerson cannot be deleted', () => { component.canDelete$ = observableOf(false); fixture.detectChanges(); const deleteButton = fixture.debugElement.query(By.css('.delete-button')); - expect(deleteButton.nativeElement.disabled).toBe(true); + expect(deleteButton).toBeNull(); }); it('should call the epersonFormComponent delete when clicked on the button', () => { @@ -531,7 +532,7 @@ describe('EPersonFormComponent', () => { ePersonEmail = 'person.email@4science.it'; component.epersonInitial = Object.assign(new EPerson(), { id: ePersonId, - email: ePersonEmail + email: ePersonEmail, }); component.resetPassword(); }); 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 d009d560589..8aa7f152501 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,47 +1,99 @@ -import { ChangeDetectorRef, Component, EventEmitter, OnDestroy, OnInit, Output } from '@angular/core'; +import { + AsyncPipe, + NgClass, + NgFor, + NgIf, +} from '@angular/common'; +import { + ChangeDetectorRef, + Component, + EventEmitter, + OnDestroy, + OnInit, + Output, +} from '@angular/core'; import { UntypedFormGroup } from '@angular/forms'; +import { + ActivatedRoute, + Router, + RouterLink, +} from '@angular/router'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { DynamicCheckboxModel, DynamicFormControlModel, DynamicFormLayout, - DynamicInputModel + DynamicInputModel, } from '@ng-dynamic-forms/core'; -import { TranslateService } from '@ngx-translate/core'; -import { combineLatest as observableCombineLatest, Observable, of as observableOf, Subscription } from 'rxjs'; -import { debounceTime, finalize, map, switchMap, take } from 'rxjs/operators'; +import { + TranslateModule, + TranslateService, +} from '@ngx-translate/core'; +import { + combineLatest as observableCombineLatest, + Observable, + of as observableOf, + Subscription, +} from 'rxjs'; +import { + debounceTime, + finalize, + map, + switchMap, + take, +} from 'rxjs/operators'; + +import { AuthService } from '../../../core/auth/auth.service'; +import { DSONameService } from '../../../core/breadcrumbs/dso-name.service'; +import { EpersonRegistrationService } from '../../../core/data/eperson-registration.service'; +import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service'; +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 { EPerson } from '../../../core/eperson/models/eperson.model'; import { Group } from '../../../core/eperson/models/group.model'; +import { PaginationService } from '../../../core/pagination/pagination.service'; +import { NoContent } from '../../../core/shared/NoContent.model'; import { getFirstCompletedRemoteData, getFirstSucceededRemoteData, - getRemoteDataPayload + getRemoteDataPayload, } from '../../../core/shared/operators'; +import { PageInfo } from '../../../core/shared/page-info.model'; +import { Registration } from '../../../core/shared/registration.model'; +import { TYPE_REQUEST_FORGOT } from '../../../register-email-form/register-email-form.component'; +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 { AuthService } from '../../../core/auth/auth.service'; -import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service'; -import { FeatureID } from '../../../core/data/feature-authorization/feature-id'; -import { ConfirmationModalComponent } from '../../../shared/confirmation-modal/confirmation-modal.component'; -import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { RequestService } from '../../../core/data/request.service'; -import { NoContent } from '../../../core/shared/NoContent.model'; -import { PaginationService } from '../../../core/pagination/pagination.service'; 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'; -import { Registration } from '../../../core/shared/registration.model'; -import { EpersonRegistrationService } from '../../../core/data/eperson-registration.service'; -import { TYPE_REQUEST_FORGOT } from '../../../register-email-form/register-email-form.component'; -import { DSONameService } from '../../../core/breadcrumbs/dso-name.service'; @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 @@ -81,28 +133,28 @@ export class EPersonFormComponent implements OnInit, OnDestroy { formLayout: DynamicFormLayout = { firstName: { grid: { - host: 'row' - } + host: 'row', + }, }, lastName: { grid: { - host: 'row' - } + host: 'row', + }, }, email: { grid: { - host: 'row' - } + host: 'row', + }, }, canLogIn: { grid: { - host: 'col col-sm-6 d-inline-block' - } + host: 'col col-sm-6 d-inline-block', + }, }, requireCertificate: { grid: { - host: 'col col-sm-6 d-inline-block' - } + host: 'col col-sm-6 d-inline-block', + }, }, }; @@ -137,6 +189,11 @@ export class EPersonFormComponent implements OnInit, OnDestroy { */ canImpersonate$: Observable; + /** + * The current {@link EPerson} + */ + activeEPerson$: Observable; + /** * List of subscriptions */ @@ -145,7 +202,12 @@ export class EPersonFormComponent implements OnInit, OnDestroy { /** * A list of all the groups this EPerson is a member of */ - groups: Observable>>; + groups$: Observable>>; + + /** + * The pagination of the {@link groups$} list. + */ + groupsPageInfoState$: Observable; /** * Pagination config used to display the list of groups @@ -153,7 +215,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy { config: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), { id: 'gem', pageSize: 5, - currentPage: 1 + currentPage: 1, }); /** @@ -194,8 +256,14 @@ export class EPersonFormComponent implements OnInit, OnDestroy { public requestService: RequestService, private epersonRegistrationService: EpersonRegistrationService, public dsoNameService: DSONameService, + 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); @@ -203,9 +271,6 @@ export class EPersonFormComponent implements OnInit, OnDestroy { this.submitLabel = 'form.submit'; } })); - } - - ngOnInit() { this.initialisePage(); } @@ -213,124 +278,121 @@ export class EPersonFormComponent implements OnInit, OnDestroy { * This method will initialise the page */ initialisePage() { - - 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, + 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); + })); + } + 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.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.requireCertificate = new DynamicCheckboxModel( + { + id: 'requireCertificate', + label: this.translateService.instant(`${this.messagePrefix}.requireCertificate`), + name: 'requireCertificate', + value: (this.epersonInitial != null ? this.epersonInitial.requireCertificate : false), }); - this.canLogIn = new DynamicCheckboxModel( - { - id: 'canLogIn', - label: canLogIn, - name: 'canLogIn', - value: (this.epersonInitial != null ? this.epersonInitial.canLogIn : true) + 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, }); - this.requireCertificate = new DynamicCheckboxModel( - { - id: 'requireCertificate', - label: requireCertificate, - name: 'requireCertificate', - value: (this.epersonInitial != null ? this.epersonInitial.requireCertificate : false) + } + 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.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) => { + } + })); + + 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) { - this.groups = this.groupsDataService.findListByHref(eperson._links.groups.href, { - currentPage: 1, - elementsPerPage: this.config.pageSize - }); + return this.groupsDataService.findListByHref(eperson._links.groups.href, findListOptions, true, true, 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 - }); + return observableOf(undefined); + }), + ); - 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.groupsPageInfoState$ = this.groups$.pipe( + map(groupsRD => groupsRD.payload.pageInfo), + ); - 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.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.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); } /** @@ -339,6 +401,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy { onCancel() { this.epersonService.cancelEditEPerson(); this.cancelForm.emit(); + void this.router.navigate([getEPersonsRoute()]); } /** @@ -348,18 +411,18 @@ 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: { 'eperson.firstname': [ { - value: this.firstName.value - } + value: this.firstName.value, + }, ], 'eperson.lastname': [ { - value: this.lastName.value + value: this.lastName.value, }, ], }, @@ -372,7 +435,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy { } else { this.editEPerson(ePerson, values); } - } + }, ); } @@ -385,11 +448,13 @@ export class EPersonFormComponent implements OnInit, OnDestroy { const response = this.epersonService.create(ePersonToCreate); response.pipe( - getFirstCompletedRemoteData() + getFirstCompletedRemoteData(), ).subscribe((rd: RemoteData) => { if (rd.hasSucceeded) { this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.created.success', { name: this.dsoNameService.getName(ePersonToCreate) })); this.submitForm.emit(ePersonToCreate); + this.epersonService.clearEPersonRequests(); + void this.router.navigateByUrl(getEPersonsRoute()); } else { this.notificationsService.error(this.translateService.get(this.labelPrefix + 'notification.created.failure', { name: this.dsoNameService.getName(ePersonToCreate) })); this.cancelForm.emit(); @@ -409,12 +474,12 @@ export class EPersonFormComponent implements OnInit, OnDestroy { metadata: { 'eperson.firstname': [ { - value: (this.firstName.value ? this.firstName.value : ePerson.firstMetadataValue('eperson.firstname')) - } + value: (this.firstName.value ? this.firstName.value : ePerson.firstMetadataValue('eperson.firstname')), + }, ], 'eperson.lastname': [ { - value: (this.lastName.value ? this.lastName.value : ePerson.firstMetadataValue('eperson.lastname')) + value: (this.lastName.value ? this.lastName.value : ePerson.firstMetadataValue('eperson.lastname')), }, ], }, @@ -429,6 +494,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy { if (rd.hasSucceeded) { this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.edited.success', { name: this.dsoNameService.getName(editedEperson) })); this.submitForm.emit(editedEperson); + void this.router.navigateByUrl(getEPersonsRoute()); } else { this.notificationsService.error(this.translateService.get(this.labelPrefix + 'notification.edited.failure', { name: this.dsoNameService.getName(editedEperson) })); this.cancelForm.emit(); @@ -447,7 +513,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy { onPageChange(event) { this.updateGroups({ currentPage: event, - elementsPerPage: this.config.pageSize + elementsPerPage: this.config.pageSize, }); } @@ -464,11 +530,11 @@ 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); - modalRef.componentInstance.dso = eperson; + modalRef.componentInstance.name = this.dsoNameService.getName(eperson); modalRef.componentInstance.headerLabel = 'confirmation-modal.delete-eperson.header'; modalRef.componentInstance.infoLabel = 'confirmation-modal.delete-eperson.info'; modalRef.componentInstance.cancelLabel = 'confirmation-modal.delete-eperson.cancel'; @@ -483,18 +549,19 @@ export class EPersonFormComponent implements OnInit, OnDestroy { this.canDelete$ = observableOf(false); return this.epersonService.deleteEPerson(eperson).pipe( getFirstCompletedRemoteData(), - map((restResponse: RemoteData) => ({ restResponse, eperson })) + map((restResponse: RemoteData) => ({ restResponse, eperson })), ); } else { return observableOf(null); } }), - finalize(() => this.canDelete$ = observableOf(true)) + finalize(() => this.canDelete$ = observableOf(true)), ); - }) + }), ).subscribe(({ restResponse, eperson }: { restResponse: RemoteData | null, eperson: EPerson }) => { if (restResponse?.hasSucceeded) { this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.deleted.success', { name: this.dsoNameService.getName(eperson) })); + void this.router.navigate([getEPersonsRoute()]); } else { this.notificationsService.error(`Error occurred when trying to delete EPerson with id: ${eperson?.id} with code: ${restResponse?.statusCode} and message: ${restResponse?.errorMessage}`); } @@ -518,14 +585,14 @@ export class EPersonFormComponent implements OnInit, OnDestroy { if (hasValue(this.epersonInitial.email)) { this.epersonRegistrationService.registerEmail(this.epersonInitial.email, null, TYPE_REQUEST_FORGOT).pipe(getFirstCompletedRemoteData()) .subscribe((response: RemoteData) => { - if (response.hasSucceeded) { - this.notificationsService.success(this.translateService.get('admin.access-control.epeople.actions.reset'), - this.translateService.get('forgot-email.form.success.content', {email: this.epersonInitial.email})); - } else { - this.notificationsService.error(this.translateService.get('forgot-email.form.error.head'), - this.translateService.get('forgot-email.form.error.content', {email: this.epersonInitial.email})); - } + if (response.hasSucceeded) { + this.notificationsService.success(this.translateService.get('admin.access-control.epeople.actions.reset'), + this.translateService.get('forgot-email.form.success.content', { email: this.epersonInitial.email })); + } else { + this.notificationsService.error(this.translateService.get('forgot-email.form.error.head'), + this.translateService.get('forgot-email.form.error.content', { email: this.epersonInitial.email })); } + }, ); } } @@ -541,16 +608,6 @@ export class EPersonFormComponent implements OnInit, OnDestroy { } } - /** - * This method will ensure that the page gets reset and that the cache is cleared - */ - reset() { - this.epersonService.getActiveEPerson().pipe(take(1)).subscribe((eperson: EPerson) => { - this.requestService.removeByHrefSubstring(eperson.self); - }); - this.initialisePage(); - } - /** * Checks for the given ePerson if there is already an ePerson in the system with that email * and shows notification if this is the case @@ -561,13 +618,13 @@ export class EPersonFormComponent implements OnInit, OnDestroy { // Relevant message for email in use this.subs.push(this.epersonService.searchByScope('email', ePerson.email, { currentPage: 1, - elementsPerPage: 0 + elementsPerPage: 0, }).pipe(getFirstSucceededRemoteData(), getRemoteDataPayload()) .subscribe((list: PaginatedList) => { if (list.totalElements > 0) { this.notificationsService.error(this.translateService.get(this.labelPrefix + 'notification.' + notificationSection + '.failure.emailInUse', { name: this.dsoNameService.getName(ePerson), - email: ePerson.email + email: ePerson.email, })); } })); @@ -577,8 +634,8 @@ 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.groups = this.groupsDataService.findListByHref(eperson._links.groups.href, options); + 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-form/validators/email-taken.validator.ts b/src/app/access-control/epeople-registry/eperson-form/validators/email-taken.validator.ts index 5153abae7c5..2a689c0d729 100644 --- a/src/app/access-control/epeople-registry/eperson-form/validators/email-taken.validator.ts +++ b/src/app/access-control/epeople-registry/eperson-form/validators/email-taken.validator.ts @@ -1,9 +1,12 @@ -import { AbstractControl, ValidationErrors } from '@angular/forms'; +import { + AbstractControl, + ValidationErrors, +} from '@angular/forms'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; import { EPersonDataService } from '../../../../core/eperson/eperson-data.service'; -import { getFirstSucceededRemoteData, } from '../../../../core/shared/operators'; +import { getFirstSucceededRemoteData } from '../../../../core/shared/operators'; export class ValidateEmailNotTaken { @@ -17,8 +20,8 @@ export class ValidateEmailNotTaken { .pipe( getFirstSucceededRemoteData(), map(res => { - return !!res.payload ? { emailTaken: true } : null; - }) + return res.payload ? { emailTaken: true } : null; + }), ); }; } diff --git a/src/app/access-control/epeople-registry/eperson-resolver.service.ts b/src/app/access-control/epeople-registry/eperson-resolver.service.ts new file mode 100644 index 00000000000..6c9d7347f73 --- /dev/null +++ b/src/app/access-control/epeople-registry/eperson-resolver.service.ts @@ -0,0 +1,60 @@ +import { Injectable } from '@angular/core'; +import { + ActivatedRouteSnapshot, + RouterStateSnapshot, +} from '@angular/router'; +import { Store } from '@ngrx/store'; +import { Observable } from 'rxjs'; + +import { RemoteData } from '../../core/data/remote-data'; +import { EPersonDataService } from '../../core/eperson/eperson-data.service'; +import { EPerson } from '../../core/eperson/models/eperson.model'; +import { ResolvedAction } from '../../core/resolving/resolver.actions'; +import { getFirstCompletedRemoteData } from '../../core/shared/operators'; +import { + followLink, + FollowLinkConfig, +} from '../../shared/utils/follow-link-config.model'; + +export const EPERSON_EDIT_FOLLOW_LINKS: FollowLinkConfig[] = [ + followLink('groups'), +]; + +/** + * This class represents a resolver that requests a specific {@link EPerson} before the route is activated + */ +@Injectable({ + providedIn: 'root', +}) +export class EPersonResolver { + + constructor( + protected ePersonService: EPersonDataService, + protected store: Store, + ) { + } + + /** + * Method for resolving a {@link EPerson} 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 {@link EPerson} based on the parameters in the current + * route, or an error if something went wrong + */ + resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable> { + const ePersonRD$: Observable> = this.ePersonService.findById(route.params.id, + true, + false, + ...EPERSON_EDIT_FOLLOW_LINKS, + ).pipe( + getFirstCompletedRemoteData(), + ); + + ePersonRD$.subscribe((ePersonRD: RemoteData) => { + this.store.dispatch(new ResolvedAction(state.url, ePersonRD.payload)); + }); + + return ePersonRD$; + } + +} 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 77a81a8daa0..7e8c1ed1b4c 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,14 +2,14 @@
-
+
-

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

+

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

- -

+ +

> {{messagePrefix + '.head.edit' | translate}} -

+

- - - + + + + + + + [displayCancel]="false" (submitForm)="onSubmit()">
-
-
-
-
- -
- - - - + +
+ +
+ +
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 f8c5f3cd870..b7e6a35d4e1 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,60 +1,91 @@ import { CommonModule } from '@angular/common'; import { HttpClient } from '@angular/common/http'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { UntypedFormControl, UntypedFormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms'; -import { BrowserModule, By } from '@angular/platform-browser'; -import { ActivatedRoute, Router } from '@angular/router'; +import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; +import { + ComponentFixture, + TestBed, + waitForAsync, +} from '@angular/core/testing'; +import { + FormsModule, + ReactiveFormsModule, + UntypedFormControl, + UntypedFormGroup, + Validators, +} from '@angular/forms'; +import { + BrowserModule, + By, +} from '@angular/platform-browser'; +import { + ActivatedRoute, + Router, +} from '@angular/router'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { Store } from '@ngrx/store'; -import { TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core'; -import { Observable, of as observableOf } from 'rxjs'; +import { TranslateModule } from '@ngx-translate/core'; +import { Operation } from 'fast-json-patch'; +import { + Observable, + of as observableOf, +} from 'rxjs'; + +import { DSONameService } from '../../../core/breadcrumbs/dso-name.service'; import { RemoteDataBuildService } from '../../../core/cache/builders/remote-data-build.service'; import { ObjectCacheService } from '../../../core/cache/object-cache.service'; import { DSOChangeAnalyzer } from '../../../core/data/dso-change-analyzer.service'; import { DSpaceObjectDataService } from '../../../core/data/dspace-object-data.service'; import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service'; -import { buildPaginatedList, 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 { Group } from '../../../core/eperson/models/group.model'; import { DSpaceObject } from '../../../core/shared/dspace-object.model'; 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 { NotificationsService } from '../../../shared/notifications/notifications.service'; -import { GroupMock, GroupMock2 } from '../../../shared/testing/group-mock'; -import { GroupFormComponent } from './group-form.component'; -import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; +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 { getMockTranslateService } from '../../../shared/mocks/translate.service.mock'; -import { TranslateLoaderMock } from '../../../shared/testing/translate-loader.mock'; import { RouterMock } from '../../../shared/mocks/router.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 { Operation } from 'fast-json-patch'; +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'; -import { NoContent } from '../../../core/shared/NoContent.model'; -import { DSONameService } from '../../../core/breadcrumbs/dso-name.service'; -import { DSONameServiceMock } from '../../../shared/mocks/dso-name.service.mock'; 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]; @@ -65,10 +96,19 @@ describe('GroupFormComponent', () => { metadata: { 'dc.description': [ { - value: groupDescription - } + value: groupDescription, + }, ], }, + object: createSuccessfulRemoteDataObject$(undefined), + _links: { + self: { + href: 'group-selflink', + }, + object: { + href: 'group-objectlink', + }, + }, }); ePersonDataServiceStub = {}; groupsDataServiceStub = { @@ -105,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); }, @@ -114,92 +161,88 @@ describe('GroupFormComponent', () => { }, getGroupEditPageRouterLinkWithID(id: string) { return `group-edit-page-for-${id}`; - } + }, }; authorizationService = jasmine.createSpyObj('authorizationService', { - isAuthorized: observableOf(true) + isAuthorized: observableOf(true), }); dsoDataServiceStub = { findByHref(href: string): Observable> { return null; - } + }, }; builderService = Object.assign(getMockFormBuilderService(),{ createFormGroup(formModel, options = null) { const controls = {}; formModel.forEach( model => { - model.parent = parent; - const controlModel = model; - const controlState = { value: controlModel.value, disabled: controlModel.disabled }; - const controlOptions = this.createAbstractControlOptions(controlModel.validators, controlModel.asyncValidators, controlModel.updateOn); - controls[model.id] = new UntypedFormControl(controlState, controlOptions); + model.parent = parent; + const controlModel = model; + const controlState = { value: controlModel.value, disabled: controlModel.disabled }; + const controlOptions = this.createAbstractControlOptions(controlModel.validators, controlModel.asyncValidators, controlModel.updateOn); + controls[model.id] = new UntypedFormControl(controlState, controlOptions); }); return new UntypedFormGroup(controls, options); }, createAbstractControlOptions(validatorsConfig = null, asyncValidatorsConfig = null, updateOn = null) { return { - validators: validatorsConfig !== null ? this.getValidators(validatorsConfig) : null, + validators: validatorsConfig !== null ? this.getValidators(validatorsConfig) : null, }; }, getValidators(validatorsConfig) { - return this.getValidatorFns(validatorsConfig); + return this.getValidatorFns(validatorsConfig); }, getValidatorFns(validatorsConfig, validatorsToken = this._NG_VALIDATORS) { let validatorFns = []; if (this.isObject(validatorsConfig)) { - validatorFns = Object.keys(validatorsConfig).map(validatorConfigKey => { - const validatorConfigValue = validatorsConfig[validatorConfigKey]; - if (this.isValidatorDescriptor(validatorConfigValue)) { - const descriptor = validatorConfigValue; - return this.getValidatorFn(descriptor.name, descriptor.args, validatorsToken); - } - return this.getValidatorFn(validatorConfigKey, validatorConfigValue, validatorsToken); - }); + validatorFns = Object.keys(validatorsConfig).map(validatorConfigKey => { + const validatorConfigValue = validatorsConfig[validatorConfigKey]; + if (this.isValidatorDescriptor(validatorConfigValue)) { + const descriptor = validatorConfigValue; + return this.getValidatorFn(descriptor.name, descriptor.args, validatorsToken); + } + return this.getValidatorFn(validatorConfigKey, validatorConfigValue, validatorsToken); + }); } return validatorFns; }, getValidatorFn(validatorName, validatorArgs = null, validatorsToken = this._NG_VALIDATORS) { let validatorFn; if (Validators.hasOwnProperty(validatorName)) { // Built-in Angular Validators - validatorFn = Validators[validatorName]; + validatorFn = Validators[validatorName]; } else { // Custom Validators - if (this._DYNAMIC_VALIDATORS && this._DYNAMIC_VALIDATORS.has(validatorName)) { - validatorFn = this._DYNAMIC_VALIDATORS.get(validatorName); - } else if (validatorsToken) { - validatorFn = validatorsToken.find(validator => validator.name === validatorName); - } + if (this._DYNAMIC_VALIDATORS && this._DYNAMIC_VALIDATORS.has(validatorName)) { + validatorFn = this._DYNAMIC_VALIDATORS.get(validatorName); + } else if (validatorsToken) { + validatorFn = validatorsToken.find(validator => validator.name === validatorName); + } } if (validatorFn === undefined) { // throw when no validator could be resolved - throw new Error(`validator '${validatorName}' is not provided via NG_VALIDATORS, NG_ASYNC_VALIDATORS or DYNAMIC_FORM_VALIDATORS`); + throw new Error(`validator '${validatorName}' is not provided via NG_VALIDATORS, NG_ASYNC_VALIDATORS or DYNAMIC_FORM_VALIDATORS`); } if (validatorArgs !== null) { - return validatorFn(validatorArgs); + return validatorFn(validatorArgs); } return validatorFn; - }, + }, isValidatorDescriptor(value) { - if (this.isObject(value)) { - return value.hasOwnProperty('name') && value.hasOwnProperty('args'); - } - return false; + if (this.isObject(value)) { + return value.hasOwnProperty('name') && value.hasOwnProperty('args'); + } + return false; }, isObject(value) { 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 }, @@ -211,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(() => { @@ -234,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(() => { @@ -243,75 +294,92 @@ 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', metadata: { 'dc.description': [ { - value: groupDescription - } + value: groupDescription, + }, ], }, + 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', - value: 'testDescription' + value: 'testDescription', }, { op: 'replace', path: '/name', - value: 'newGroupName' + value: 'newGroupName', }]; expect(groupsDataServiceStub.patch).toHaveBeenCalledWith(expected, operations); }); 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', - value: 'testDescription' + value: 'testDescription', }]; expect(groupsDataServiceStub.patch).toHaveBeenCalledWith(expected, operations); }); 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', - value: 'newGroupName' + value: 'newGroupName', }]; 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(); }); }); @@ -326,11 +394,8 @@ describe('GroupFormComponent', () => { describe('check form validation', () => { - let groupCommunity; - beforeEach(() => { groupName = 'testName'; - groupCommunity = 'testgroupCommunity'; groupDescription = 'testgroupDescription'; expected = Object.assign(new Group(), { @@ -338,12 +403,21 @@ describe('GroupFormComponent', () => { metadata: { 'dc.description': [ { - value: groupDescription - } + value: groupDescription, + }, ], }, + _links: { + self: { + href: 'group-selflink', + }, + object: { + href: 'group-objectlink', + }, + }, }); spyOn(component.submitForm, 'emit'); + spyOn(dsoDataServiceStub, 'findByHref').and.returnValue(observableOf(expected)); fixture.detectChanges(); component.initialisePage(); @@ -376,7 +450,7 @@ describe('GroupFormComponent', () => { const groupsDataServiceStubWithGroup = Object.assign(groupsDataServiceStub,{ searchGroups(query: string): Observable>> { return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), [expected])); - } + }, }); component.formGroup.controls.groupName.setValue('testName'); component.formGroup.controls.groupName.setAsyncValidators(ValidateGroupExists.createValidator(groupsDataServiceStubWithGroup)); @@ -393,21 +467,20 @@ describe('GroupFormComponent', () => { }); describe('delete', () => { - let deleteButton; - - beforeEach(() => { - component.initialisePage(); + let deleteButton: HTMLButtonElement; + beforeEach(async () => { + spyOn(groupsDataServiceStub, 'delete').and.callThrough(); + component.activeGroup$ = observableOf({ + id: 'active-group', + permanent: false, + } as Group); component.canEdit$ = observableOf(true); - component.groupBeingEdited = { - permanent: false - } as Group; + + 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 3c0547cca50..d2ddb3266b1 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,57 +1,105 @@ -import { Component, EventEmitter, HostListener, OnDestroy, OnInit, Output, ChangeDetectorRef } from '@angular/core'; -import { UntypedFormGroup } from '@angular/forms'; -import { ActivatedRoute, Router } from '@angular/router'; +import { + AsyncPipe, + NgIf, +} from '@angular/common'; +import { + ChangeDetectorRef, + Component, + EventEmitter, + HostListener, + OnDestroy, + OnInit, + Output, +} from '@angular/core'; +import { + AbstractControl, + UntypedFormGroup, +} from '@angular/forms'; +import { + ActivatedRoute, + Router, +} from '@angular/router'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { DynamicFormControlModel, DynamicFormLayout, DynamicInputModel, - DynamicTextAreaModel + DynamicTextAreaModel, } from '@ng-dynamic-forms/core'; -import { TranslateService } from '@ngx-translate/core'; import { - ObservedValueOf, + 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, map, switchMap, take, filter, debounceTime } from 'rxjs/operators'; +import { + debounceTime, + map, + switchMap, + take, +} from 'rxjs/operators'; + +import { environment } from '../../../../environments/environment'; import { getCollectionEditRolesRoute } from '../../../collection-page/collection-page-routing-paths'; import { getCommunityEditRolesRoute } from '../../../community-page/community-page-routing-paths'; +import { DSONameService } from '../../../core/breadcrumbs/dso-name.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'; 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'; import { Community } from '../../../core/shared/community.model'; import { DSpaceObject } from '../../../core/shared/dspace-object.model'; +import { NoContent } from '../../../core/shared/NoContent.model'; import { - getRemoteDataPayload, - getFirstSucceededRemoteData, + getAllCompletedRemoteData, getFirstCompletedRemoteData, - getFirstSucceededRemoteDataPayload + getFirstSucceededRemoteData, + getRemoteDataPayload, } from '../../../core/shared/operators'; -import { AlertType } from '../../../shared/alert/aletr-type'; +import { AlertComponent } from '../../../shared/alert/alert.component'; +import { AlertType } from '../../../shared/alert/alert-type'; import { ConfirmationModalComponent } from '../../../shared/confirmation-modal/confirmation-modal.component'; -import { hasValue, isNotEmpty, hasValueOperator } from '../../../shared/empty.util'; +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 { NoContent } from '../../../core/shared/NoContent.model'; -import { Operation } from 'fast-json-patch'; +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'; -import { DSONameService } from '../../../core/breadcrumbs/dso-name.service'; -import { environment } from '../../../../environments/environment'; @Component({ selector: 'ds-group-form', - templateUrl: './group-form.component.html' + templateUrl: './group-form.component.html', + imports: [ + FormComponent, + AlertComponent, + NgIf, + AsyncPipe, + TranslateModule, + ContextHelpDirective, + MembersListComponent, + SubgroupsListComponent, + ], + standalone: true, }) /** * A form used for creating and editing groups @@ -68,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 @@ -83,13 +131,13 @@ export class GroupFormComponent implements OnInit, OnDestroy { formLayout: DynamicFormLayout = { groupName: { grid: { - host: 'row' - } + host: 'row', + }, }, groupDescription: { grid: { - host: 'row' - } + host: 'row', + }, }, }; @@ -114,20 +162,29 @@ export class GroupFormComponent implements OnInit, OnDestroy { subs: Subscription[] = []; /** - * Group currently being edited + * 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 current {@link Group} */ - groupBeingEdited: Group; + activeGroup$: Observable; /** - * 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 + * The current {@link Group}'s linked {@link Community}/{@link Collection} */ - canEdit$: Observable; + activeGroupLinkedDSO$: Observable; + + /** + * Link to the current {@link Group}'s {@link Community}/{@link Collection} edit role tab + */ + linkedEditRolesRoute$: Observable; /** * The AlertType enumeration - * @type {AlertType} */ - public AlertTypeEnum = AlertType; + public readonly AlertType = AlertType; /** * Subscription to email field value change @@ -137,124 +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), - (isAuthorized: ObservedValueOf>, hasLinkedDSO: ObservedValueOf>) => { - return isAuthorized && !hasLinkedDSO; - }); - }) - ); - 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, + 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.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) { + this.subs.push( + observableCombineLatest([ + this.activeGroup$, + this.canEdit$, + this.activeGroupLinkedDSO$, + ]).subscribe(([activeGroup, canEdit, linkedObject]) => { - // Disable group name exists validator - this.formGroup.controls.groupName.clearAsyncValidators(); + if (activeGroup != null) { - this.groupBeingEdited = activeGroup; + // Disable group name exists validator + this.formGroup.controls.groupName.clearAsyncValidators(); - if (linkedObject?.name) { - 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'), - }); + if (isNotEmpty(linkedObject?.name)) { + if (!this.formGroup.controls.groupCommunity) { + this.formBuilderService.insertFormGroupControl(1, this.formGroup, this.formModel, groupCommunityModel); + this.groupDescription = this.formGroup.get('groupCommunity'); } - setTimeout(() => { - if (!canEdit || activeGroup.permanent) { - this.formGroup.disable(); - } - }, 200); + 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(); + } + } + }), + ); } /** @@ -263,7 +317,7 @@ export class GroupFormComponent implements OnInit, OnDestroy { onCancel() { this.groupDataService.cancelEditGroup(); this.cancelForm.emit(); - this.router.navigate([this.groupDataService.getGroupRegistryRouterLink()]); + void this.router.navigate([getGroupsRoute()]); } /** @@ -273,25 +327,22 @@ 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': [ { - value: this.groupDescription.value - } - ] + value: this.groupDescription.value, + }, + ], }, - }; - if (group === null) { - this.createNewGroup(values); - } else { - this.editGroup(group); - } + }); + } else { + this.editGroup(group); } - ); + }); } /** @@ -301,7 +352,7 @@ export class GroupFormComponent implements OnInit, OnDestroy { createNewGroup(values) { const groupToCreate = Object.assign(new Group(), values); this.groupDataService.create(groupToCreate).pipe( - getFirstCompletedRemoteData() + getFirstCompletedRemoteData(), ).subscribe((rd: RemoteData) => { if (rd.hasSucceeded) { this.notificationsService.success(this.translateService.get(this.messagePrefix + '.notification.created.success', { name: groupToCreate.name })); @@ -310,7 +361,7 @@ export class GroupFormComponent implements OnInit, OnDestroy { const groupSelfLink = rd.payload._links.self.href; this.setActiveGroupWithLink(groupSelfLink); this.groupDataService.clearGroupsRequests(); - this.router.navigateByUrl(this.groupDataService.getGroupEditPageRouterLinkWithID(rd.payload.uuid)); + void this.router.navigateByUrl(getGroupEditRoute(rd.payload.uuid)); } } else { this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.created.failure', { name: groupToCreate.name })); @@ -330,7 +381,7 @@ export class GroupFormComponent implements OnInit, OnDestroy { // Relevant message for group name in use this.subs.push(this.groupDataService.searchGroups(group.name, { currentPage: 1, - elementsPerPage: 0 + elementsPerPage: 0, }).pipe(getFirstSucceededRemoteData(), getRemoteDataPayload()) .subscribe((list: PaginatedList) => { if (list.totalElements > 0) { @@ -352,7 +403,7 @@ export class GroupFormComponent implements OnInit, OnDestroy { operations = [...operations, { op: 'add', path: '/metadata/dc.description', - value: this.groupDescription.value + value: this.groupDescription.value, }]; } @@ -360,12 +411,12 @@ export class GroupFormComponent implements OnInit, OnDestroy { operations = [...operations, { op: 'replace', path: '/name', - value: this.groupName.value + value: this.groupName.value, }]; } this.groupDataService.patch(group, operations).pipe( - getFirstCompletedRemoteData() + getFirstCompletedRemoteData(), ).subscribe((rd: RemoteData) => { if (rd.hasSucceeded) { this.notificationsService.success(this.translateService.get(this.messagePrefix + '.notification.edited.success', { name: this.dsoNameService.getName(rd.payload) })); @@ -397,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')) @@ -416,9 +467,9 @@ 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.dso = group; + modalRef.componentInstance.name = this.dsoNameService.getName(group); modalRef.componentInstance.headerLabel = this.messagePrefix + '.delete-group.modal.header'; modalRef.componentInstance.infoLabel = this.messagePrefix + '.delete-group.modal.info'; modalRef.componentInstance.cancelLabel = this.messagePrefix + '.delete-group.modal.cancel'; @@ -460,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 - */ - 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)), - ); - } - } - - /** - * Get group's linked object if it has one (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) */ - 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; - } + 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 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 + * 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 */ - 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 cc9bf34d644..591d80bdd59 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 @@ -1,51 +1,16 @@ -

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

+

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

- - -
-
- -
-
-
- - - - -
-
-
- -
-
+

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

-
- +
@@ -55,33 +20,31 @@ - - + +
{{messagePrefix + '.table.id' | translate}}
{{ePerson.eperson.id}}
{{epersonDTO.eperson.id}} - - {{ dsoNameService.getName(ePerson.eperson) }} + + {{ dsoNameService.getName(epersonDTO.eperson) }} - {{messagePrefix + '.table.email' | translate}}: {{ ePerson.eperson.email ? ePerson.eperson.email : '-' }}
- {{messagePrefix + '.table.netid' | translate}}: {{ ePerson.eperson.netid ? ePerson.eperson.netid : '-' }} + {{messagePrefix + '.table.email' | translate}}: {{ epersonDTO.eperson.email ? epersonDTO.eperson.email : '-' }}
+ {{messagePrefix + '.table.netid' | translate}}: {{ epersonDTO.eperson.netid ? epersonDTO.eperson.netid : '-' }}
- - -
@@ -93,23 +56,49 @@

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

+ - +
+
+ + + + +
+
+
+ +
+ + +
- +
@@ -119,32 +108,23 @@

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

- - + + { +import { Community } from '../../core/shared/community.model'; +import { RouterMock } from '../../shared/mocks/router.mock'; +import { + createFailedRemoteDataObject$, + createSuccessfulRemoteDataObject$, +} from '../../shared/remote-data.utils'; +import { createCollectionPageGuard } from './create-collection-page.guard'; + +describe('createCollectionPageGuard', () => { describe('canActivate', () => { - let guard: CreateCollectionPageGuard; + let guard: any; let router; let communityDataServiceStub: any; @@ -20,46 +24,46 @@ describe('CreateCollectionPageGuard', () => { } else if (id === 'error-id') { return createFailedRemoteDataObject$('not found', 404); } - } + }, }; router = new RouterMock(); - guard = new CreateCollectionPageGuard(router, communityDataServiceStub); + guard = createCollectionPageGuard; }); 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) => - expect(canActivate).toEqual(true) + expect(canActivate).toEqual(true), ); }); it('should return false when no parent ID has been provided', () => { - guard.canActivate({ queryParams: { } } as any, undefined) + guard({ queryParams: { } } as any, undefined, communityDataServiceStub, router) .pipe(first()) .subscribe( (canActivate) => - expect(canActivate).toEqual(false) + expect(canActivate).toEqual(false), ); }); 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) => - expect(canActivate).toEqual(false) + expect(canActivate).toEqual(false), ); }); 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) => - expect(canActivate).toEqual(false) + expect(canActivate).toEqual(false), ); }); }); diff --git a/src/app/collection-page/create-collection-page/create-collection-page.guard.ts b/src/app/collection-page/create-collection-page/create-collection-page.guard.ts index ca84231912b..099578c550e 100644 --- a/src/app/collection-page/create-collection-page/create-collection-page.guard.ts +++ b/src/app/collection-page/create-collection-page/create-collection-page.guard.ts @@ -1,43 +1,52 @@ -import { Injectable } from '@angular/core'; -import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '@angular/router'; +import { inject } from '@angular/core'; +import { + ActivatedRouteSnapshot, + CanActivateFn, + Router, + RouterStateSnapshot, +} from '@angular/router'; +import { + Observable, + of as observableOf, +} from 'rxjs'; +import { + map, + tap, +} from 'rxjs/operators'; -import { hasNoValue, hasValue } from '../../shared/empty.util'; import { CommunityDataService } from '../../core/data/community-data.service'; import { RemoteData } from '../../core/data/remote-data'; import { Community } from '../../core/shared/community.model'; -import { map, tap } from 'rxjs/operators'; -import { Observable, of as observableOf } from 'rxjs'; import { getFirstCompletedRemoteData } from '../../core/shared/operators'; +import { + hasNoValue, + hasValue, +} from '../../shared/empty.util'; /** - * Prevent creation of a collection without a parent community provided - * @class CreateCollectionPageGuard + * True when either a parent ID query parameter has been provided and the parent ID resolves to a valid parent community + * Reroutes to a 404 page when the page cannot be activated */ -@Injectable() -export class CreateCollectionPageGuard implements CanActivate { - public constructor(private router: Router, private communityService: CommunityDataService) { +export const createCollectionPageGuard: CanActivateFn = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + communityService: CommunityDataService = inject(CommunityDataService), + router: Router = inject(Router), +): Observable => { + const parentID = route.queryParams.parent; + if (hasNoValue(parentID)) { + router.navigate(['/404']); + return observableOf(false); } - - /** - * True when either a parent ID query parameter has been provided and 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)) { - this.router.navigate(['/404']); - return observableOf(false); - } - 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/collection-page/delete-collection-page/delete-collection-page.component.html b/src/app/collection-page/delete-collection-page/delete-collection-page.component.html index ba54bbabd59..53c3b45cfab 100644 --- a/src/app/collection-page/delete-collection-page/delete-collection-page.component.html +++ b/src/app/collection-page/delete-collection-page/delete-collection-page.component.html @@ -2,7 +2,7 @@
- +

{{ 'collection.delete.head' | translate}}

{{ 'collection.delete.text' | translate:{ dso: dsoNameService.getName(dso) } }}

@@ -11,7 +11,7 @@
diff --git a/src/app/collection-page/delete-collection-page/delete-collection-page.component.spec.ts b/src/app/collection-page/delete-collection-page/delete-collection-page.component.spec.ts index 9fc88932d07..8a99397b918 100644 --- a/src/app/collection-page/delete-collection-page/delete-collection-page.component.spec.ts +++ b/src/app/collection-page/delete-collection-page/delete-collection-page.component.spec.ts @@ -1,17 +1,21 @@ -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { ActivatedRoute } from '@angular/router'; -import { TranslateModule } from '@ngx-translate/core'; import { CommonModule } from '@angular/common'; -import { RouterTestingModule } from '@angular/router/testing'; import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { SharedModule } from '../../shared/shared.module'; +import { + ComponentFixture, + TestBed, + waitForAsync, +} from '@angular/core/testing'; +import { ActivatedRoute } from '@angular/router'; +import { RouterTestingModule } from '@angular/router/testing'; +import { TranslateModule } from '@ngx-translate/core'; import { of as observableOf } from 'rxjs'; -import { NotificationsService } from '../../shared/notifications/notifications.service'; -import { DeleteCollectionPageComponent } from './delete-collection-page.component'; + +import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; import { CollectionDataService } from '../../core/data/collection-data.service'; import { RequestService } from '../../core/data/request.service'; -import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; import { DSONameServiceMock } from '../../shared/mocks/dso-name.service.mock'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { DeleteCollectionPageComponent } from './delete-collection-page.component'; describe('DeleteCollectionPageComponent', () => { let comp: DeleteCollectionPageComponent; @@ -19,16 +23,15 @@ describe('DeleteCollectionPageComponent', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [TranslateModule.forRoot(), SharedModule, CommonModule, RouterTestingModule], - declarations: [DeleteCollectionPageComponent], + imports: [TranslateModule.forRoot(), CommonModule, RouterTestingModule, DeleteCollectionPageComponent], providers: [ { provide: DSONameService, useValue: new DSONameServiceMock() }, { provide: CollectionDataService, useValue: {} }, { provide: ActivatedRoute, useValue: { data: observableOf({ dso: { payload: {} } }) } }, { provide: NotificationsService, useValue: {} }, - { provide: RequestService, useValue: {} } + { provide: RequestService, useValue: {} }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); diff --git a/src/app/collection-page/delete-collection-page/delete-collection-page.component.ts b/src/app/collection-page/delete-collection-page/delete-collection-page.component.ts index 0a1b87e58b3..dbb5f8846f1 100644 --- a/src/app/collection-page/delete-collection-page/delete-collection-page.component.ts +++ b/src/app/collection-page/delete-collection-page/delete-collection-page.component.ts @@ -1,11 +1,23 @@ +import { + AsyncPipe, + NgIf, +} from '@angular/common'; import { Component } from '@angular/core'; -import { ActivatedRoute, Router } from '@angular/router'; -import { DeleteComColPageComponent } from '../../shared/comcol/comcol-forms/delete-comcol-page/delete-comcol-page.component'; -import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { + ActivatedRoute, + Router, +} from '@angular/router'; +import { + TranslateModule, + TranslateService, +} from '@ngx-translate/core'; + +import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; import { CollectionDataService } from '../../core/data/collection-data.service'; import { Collection } from '../../core/shared/collection.model'; -import { TranslateService } from '@ngx-translate/core'; -import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; +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 Collection @@ -13,7 +25,14 @@ import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; @Component({ selector: 'ds-delete-collection', styleUrls: ['./delete-collection-page.component.scss'], - templateUrl: './delete-collection-page.component.html' + templateUrl: './delete-collection-page.component.html', + imports: [ + TranslateModule, + AsyncPipe, + NgIf, + VarDirective, + ], + standalone: true, }) export class DeleteCollectionPageComponent extends DeleteComColPageComponent { protected frontendURL = '/collections/'; diff --git a/src/app/collection-page/edit-collection-page/collection-access-control/collection-access-control.component.spec.ts b/src/app/collection-page/edit-collection-page/collection-access-control/collection-access-control.component.spec.ts index 04da8bbcd92..a64a8a273d2 100644 --- a/src/app/collection-page/edit-collection-page/collection-access-control/collection-access-control.component.spec.ts +++ b/src/app/collection-page/edit-collection-page/collection-access-control/collection-access-control.component.spec.ts @@ -1,25 +1,76 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { + ComponentFixture, + TestBed, +} from '@angular/core/testing'; +import { ActivatedRoute } from '@angular/router'; +import { + of as observableOf, + of, +} 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 { CollectionAccessControlComponent } from './collection-access-control.component'; -xdescribe('CollectionAccessControlComponent', () => { +describe('CollectionAccessControlComponent', () => { let component: CollectionAccessControlComponent; let fixture: ComponentFixture; + const testCommunity = Object.assign(new Community(), + { + type: 'community', + metadata: { + 'dc.title': [{ value: 'community' }], + }, + uuid: 'communityUUID', + parentCommunity: observableOf(Object.assign(createSuccessfulRemoteDataObject(undefined), { statusCode: 204 })), + _links: { + parentCommunity: 'site', + self: '/' + 'communityUUID', + }, + }, + ); + + const routeStub = { + parent: { + parent: { + data: of({ + dso: createSuccessfulRemoteDataObject(testCommunity), + }), + }, + }, + }; beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [ CollectionAccessControlComponent ] + imports: [CollectionAccessControlComponent], + providers: [{ + provide: ActivatedRoute, useValue: routeStub, + }], }) - .compileComponents(); + .overrideComponent(CollectionAccessControlComponent, { + remove: { + imports: [AccessControlFormContainerComponent], + }, + }) + .compileComponents(); }); beforeEach(() => { fixture = TestBed.createComponent(CollectionAccessControlComponent); component = fixture.componentInstance; fixture.detectChanges(); + component.ngOnInit(); }); it('should create', () => { expect(component).toBeTruthy(); }); + + it('should set itemRD$', (done) => { + component.itemRD$.subscribe(result => { + expect(result).toEqual(createSuccessfulRemoteDataObject(testCommunity)); + done(); + }); + }); }); diff --git a/src/app/collection-page/edit-collection-page/collection-access-control/collection-access-control.component.ts b/src/app/collection-page/edit-collection-page/collection-access-control/collection-access-control.component.ts index 4192fe5a9a3..809fdcede87 100644 --- a/src/app/collection-page/edit-collection-page/collection-access-control/collection-access-control.component.ts +++ b/src/app/collection-page/edit-collection-page/collection-access-control/collection-access-control.component.ts @@ -1,15 +1,30 @@ -import { Component, OnInit } from '@angular/core'; +import { + AsyncPipe, + NgIf, +} from '@angular/common'; +import { + Component, + OnInit, +} from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; + import { RemoteData } from '../../../core/data/remote-data'; import { Community } from '../../../core/shared/community.model'; -import { ActivatedRoute } from '@angular/router'; -import { map } from 'rxjs/operators'; import { getFirstSucceededRemoteData } from '../../../core/shared/operators'; +import { AccessControlFormContainerComponent } from '../../../shared/access-control-form-container/access-control-form-container.component'; @Component({ selector: 'ds-collection-access-control', templateUrl: './collection-access-control.component.html', styleUrls: ['./collection-access-control.component.scss'], + imports: [ + AccessControlFormContainerComponent, + NgIf, + AsyncPipe, + ], + standalone: true, }) export class CollectionAccessControlComponent implements OnInit { itemRD$: Observable>; @@ -18,7 +33,7 @@ export class CollectionAccessControlComponent implements OnInit { ngOnInit(): void { this.itemRD$ = this.route.parent.parent.data.pipe( - map((data) => data.dso) + map((data) => data.dso), ).pipe(getFirstSucceededRemoteData()) as Observable>; } } diff --git a/src/app/collection-page/edit-collection-page/collection-authorizations/collection-authorizations.component.spec.ts b/src/app/collection-page/edit-collection-page/collection-authorizations/collection-authorizations.component.spec.ts index c8e529443a4..3ee3f1c6b04 100644 --- a/src/app/collection-page/edit-collection-page/collection-authorizations/collection-authorizations.component.spec.ts +++ b/src/app/collection-page/edit-collection-page/collection-authorizations/collection-authorizations.component.spec.ts @@ -1,15 +1,22 @@ import { CommonModule } from '@angular/common'; -import { ChangeDetectorRef, NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { + ChangeDetectorRef, + NO_ERRORS_SCHEMA, +} from '@angular/core'; +import { + ComponentFixture, + TestBed, + waitForAsync, +} from '@angular/core/testing'; import { ActivatedRoute } from '@angular/router'; - import { cold } from 'jasmine-marbles'; import { of as observableOf } from 'rxjs'; -import { DSpaceObject } from '../../../core/shared/dspace-object.model'; -import { CollectionAuthorizationsComponent } from './collection-authorizations.component'; 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 { CollectionAuthorizationsComponent } from './collection-authorizations.component'; describe('CollectionAuthorizationsComponent', () => { let comp: CollectionAuthorizationsComponent; @@ -19,8 +26,8 @@ describe('CollectionAuthorizationsComponent', () => { uuid: 'collection', id: 'collection', _links: { - self: { href: 'collection-selflink' } - } + self: { href: 'collection-selflink' }, + }, }); const collectionRD = createSuccessfulRemoteDataObject(collection); @@ -29,25 +36,29 @@ describe('CollectionAuthorizationsComponent', () => { parent: { parent: { data: observableOf({ - dso: collectionRD - }) - } - } + dso: collectionRD, + }), + }, + }, }; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [ - CommonModule + CommonModule, + CollectionAuthorizationsComponent, ], - declarations: [CollectionAuthorizationsComponent], providers: [ { provide: ActivatedRoute, useValue: routeStub }, ChangeDetectorRef, CollectionAuthorizationsComponent, ], schemas: [NO_ERRORS_SCHEMA], - }).compileComponents(); + }) + .overrideComponent(CollectionAuthorizationsComponent, { + remove: { imports: [ResourcePoliciesComponent] }, + }) + .compileComponents(); })); beforeEach(() => { diff --git a/src/app/collection-page/edit-collection-page/collection-authorizations/collection-authorizations.component.ts b/src/app/collection-page/edit-collection-page/collection-authorizations/collection-authorizations.component.ts index d1b59a0c903..31824b7be81 100644 --- a/src/app/collection-page/edit-collection-page/collection-authorizations/collection-authorizations.component.ts +++ b/src/app/collection-page/edit-collection-page/collection-authorizations/collection-authorizations.component.ts @@ -1,15 +1,27 @@ -import { Component, OnInit } from '@angular/core'; +import { AsyncPipe } from '@angular/common'; +import { + Component, + OnInit, +} from '@angular/core'; import { ActivatedRoute } from '@angular/router'; - import { Observable } from 'rxjs'; -import { first, map } from 'rxjs/operators'; +import { + first, + map, +} from 'rxjs/operators'; 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-collection-authorizations', templateUrl: './collection-authorizations.component.html', + imports: [ + ResourcePoliciesComponent, + AsyncPipe, + ], + standalone: true, }) /** * Component that handles the Collection Authorizations @@ -27,7 +39,7 @@ export class CollectionAuthorizationsComponent imp * @param {ActivatedRoute} route */ constructor( - private route: ActivatedRoute + private route: ActivatedRoute, ) { } diff --git a/src/app/collection-page/edit-collection-page/collection-curate/collection-curate.component.html b/src/app/collection-page/edit-collection-page/collection-curate/collection-curate.component.html index 38c9d22f4ed..4fbe60d6b17 100644 --- a/src/app/collection-page/edit-collection-page/collection-curate/collection-curate.component.html +++ b/src/app/collection-page/edit-collection-page/collection-curate/collection-curate.component.html @@ -1,5 +1,5 @@
-

{{'collection.curate.header' |translate:{collection: (collectionName$ |async)} }}

+

{{'collection.curate.header' |translate:{collection: (collectionName$ |async)} }}

diff --git a/src/app/collection-page/edit-collection-page/collection-curate/collection-curate.component.spec.ts b/src/app/collection-page/edit-collection-page/collection-curate/collection-curate.component.spec.ts index 2cf25734e12..b10131e4f40 100644 --- a/src/app/collection-page/edit-collection-page/collection-curate/collection-curate.component.spec.ts +++ b/src/app/collection-page/edit-collection-page/collection-curate/collection-curate.component.spec.ts @@ -1,12 +1,21 @@ -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { + CUSTOM_ELEMENTS_SCHEMA, + DebugElement, +} from '@angular/core'; +import { + ComponentFixture, + TestBed, + waitForAsync, +} from '@angular/core/testing'; +import { ActivatedRoute } from '@angular/router'; import { TranslateModule } from '@ngx-translate/core'; -import { CUSTOM_ELEMENTS_SCHEMA, DebugElement } from '@angular/core'; -import { CollectionCurateComponent } from './collection-curate.component'; import { of as observableOf } from 'rxjs'; -import { createSuccessfulRemoteDataObject } from '../../../shared/remote-data.utils'; -import { Collection } from '../../../core/shared/collection.model'; -import { ActivatedRoute } from '@angular/router'; + import { DSONameService } from '../../../core/breadcrumbs/dso-name.service'; +import { Collection } from '../../../core/shared/collection.model'; +import { CurationFormComponent } from '../../../curation-form/curation-form.component'; +import { createSuccessfulRemoteDataObject } from '../../../shared/remote-data.utils'; +import { CollectionCurateComponent } from './collection-curate.component'; describe('CollectionCurateComponent', () => { let comp: CollectionCurateComponent; @@ -17,31 +26,34 @@ describe('CollectionCurateComponent', () => { let dsoNameService; const collection = Object.assign(new Collection(), { - metadata: {'dc.title': ['Collection Name'], 'dc.identifier.uri': [ { value: '123456789/1'}]} + metadata: { 'dc.title': ['Collection Name'], 'dc.identifier.uri': [ { value: '123456789/1' }] }, }); beforeEach(waitForAsync(() => { routeStub = { parent: { data: observableOf({ - dso: createSuccessfulRemoteDataObject(collection) - }) - } + dso: createSuccessfulRemoteDataObject(collection), + }), + }, }; dsoNameService = jasmine.createSpyObj('dsoNameService', { - getName: 'Collection Name' + getName: 'Collection Name', }); TestBed.configureTestingModule({ - imports: [TranslateModule.forRoot()], - declarations: [CollectionCurateComponent], + imports: [TranslateModule.forRoot(), CollectionCurateComponent], providers: [ - {provide: ActivatedRoute, useValue: routeStub}, - {provide: DSONameService, useValue: dsoNameService} + { provide: ActivatedRoute, useValue: routeStub }, + { provide: DSONameService, useValue: dsoNameService }, ], - schemas: [CUSTOM_ELEMENTS_SCHEMA] - }).compileComponents(); + schemas: [CUSTOM_ELEMENTS_SCHEMA], + }) + .overrideComponent(CollectionCurateComponent, { + remove: { imports: [CurationFormComponent] }, + }) + .compileComponents(); })); beforeEach(() => { @@ -58,7 +70,7 @@ describe('CollectionCurateComponent', () => { }); it('should contain the collection information provided in the route', () => { comp.dsoRD$.subscribe((value) => { - expect(value.payload.handle + expect(value.payload.handle, ).toEqual('123456789/1'); }); comp.collectionName$.subscribe((value) => { diff --git a/src/app/collection-page/edit-collection-page/collection-curate/collection-curate.component.ts b/src/app/collection-page/edit-collection-page/collection-curate/collection-curate.component.ts index e20f229cd64..370506e4732 100644 --- a/src/app/collection-page/edit-collection-page/collection-curate/collection-curate.component.ts +++ b/src/app/collection-page/edit-collection-page/collection-curate/collection-curate.component.ts @@ -1,10 +1,21 @@ -import { Component } from '@angular/core'; -import { filter, map, take } from 'rxjs/operators'; -import { RemoteData } from '../../../core/data/remote-data'; -import { Observable } from 'rxjs'; +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, + map, + take, +} from 'rxjs/operators'; + import { DSONameService } from '../../../core/breadcrumbs/dso-name.service'; +import { RemoteData } from '../../../core/data/remote-data'; import { Collection } from '../../../core/shared/collection.model'; +import { CurationFormComponent } from '../../../curation-form/curation-form.component'; import { hasValue } from '../../../shared/empty.util'; /** @@ -13,8 +24,14 @@ import { hasValue } from '../../../shared/empty.util'; @Component({ selector: 'ds-collection-curate', templateUrl: './collection-curate.component.html', + imports: [ + CurationFormComponent, + TranslateModule, + AsyncPipe, + ], + standalone: true, }) -export class CollectionCurateComponent { +export class CollectionCurateComponent implements OnInit { dsoRD$: Observable>; collectionName$: Observable; @@ -34,7 +51,7 @@ export class CollectionCurateComponent { filter((rd: RemoteData) => hasValue(rd)), map((rd: RemoteData) => { return this.dsoNameService.getName(rd.payload); - }) + }), ); } } diff --git a/src/app/collection-page/edit-collection-page/collection-metadata/collection-metadata.component.html b/src/app/collection-page/edit-collection-page/collection-metadata/collection-metadata.component.html index ffd8f713436..845c82458a5 100644 --- a/src/app/collection-page/edit-collection-page/collection-metadata/collection-metadata.component.html +++ b/src/app/collection-page/edit-collection-page/collection-metadata/collection-metadata.component.html @@ -1,22 +1,23 @@
- + {{ 'collection.edit.template.label' | translate}}
diff --git a/src/app/collection-page/edit-collection-page/collection-metadata/collection-metadata.component.spec.ts b/src/app/collection-page/edit-collection-page/collection-metadata/collection-metadata.component.spec.ts index 7cc54bd994c..8a02f0c1d44 100644 --- a/src/app/collection-page/edit-collection-page/collection-metadata/collection-metadata.component.spec.ts +++ b/src/app/collection-page/edit-collection-page/collection-metadata/collection-metadata.component.spec.ts @@ -1,20 +1,37 @@ -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { TranslateModule } from '@ngx-translate/core'; -import { SharedModule } from '../../../shared/shared.module'; import { CommonModule } from '@angular/common'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { + ComponentFixture, + TestBed, + waitForAsync, +} from '@angular/core/testing'; +import { + ActivatedRoute, + NavigationEnd, + Router, +} from '@angular/router'; import { RouterTestingModule } from '@angular/router/testing'; -import { CollectionDataService } from '../../../core/data/collection-data.service'; -import { ActivatedRoute, NavigationEnd, Router } from '@angular/router'; +import { TranslateModule } from '@ngx-translate/core'; import { of as observableOf } from 'rxjs'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { CollectionMetadataComponent } from './collection-metadata.component'; -import { NotificationsService } from '../../../shared/notifications/notifications.service'; -import { Item } from '../../../core/shared/item.model'; + +import { APP_DATA_SERVICES_MAP } from '../../../../config/app-config.interface'; +import { AuthService } from '../../../core/auth/auth.service'; +import { ObjectCacheService } from '../../../core/cache/object-cache.service'; +import { CollectionDataService } from '../../../core/data/collection-data.service'; +import { CommunityDataService } from '../../../core/data/community-data.service'; import { ItemTemplateDataService } from '../../../core/data/item-template-data.service'; -import { Collection } from '../../../core/shared/collection.model'; import { RequestService } from '../../../core/data/request.service'; -import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; +import { Collection } from '../../../core/shared/collection.model'; +import { Item } from '../../../core/shared/item.model'; +import { AuthServiceMock } from '../../../shared/mocks/auth.service.mock'; +import { NotificationsService } from '../../../shared/notifications/notifications.service'; +import { + createFailedRemoteDataObject$, + createSuccessfulRemoteDataObject, + createSuccessfulRemoteDataObject$, +} from '../../../shared/remote-data.utils'; import { getCollectionItemTemplateRoute } from '../../collection-page-routing-paths'; +import { CollectionMetadataComponent } from './collection-metadata.component'; describe('CollectionMetadataComponent', () => { let comp: CollectionMetadataComponent; @@ -24,16 +41,16 @@ describe('CollectionMetadataComponent', () => { const template = Object.assign(new Item(), { _links: { - self: { href: 'template-selflink' } - } + self: { href: 'template-selflink' }, + }, }); const collection = Object.assign(new Collection(), { uuid: 'collection-id', id: 'collection-id', name: 'Fake Collection', _links: { - self: { href: 'collection-selflink' } - } + self: { href: 'collection-selflink' }, + }, }); const collectionTemplateHref = 'rest/api/test/collections/template'; @@ -46,10 +63,10 @@ describe('CollectionMetadataComponent', () => { const notificationsService = jasmine.createSpyObj('notificationsService', { success: {}, - error: {} + error: {}, }); const requestService = jasmine.createSpyObj('requestService', { - setStaleByHrefSubstring: {} + setStaleByHrefSubstring: {}, }); const routerMock = { @@ -59,17 +76,20 @@ describe('CollectionMetadataComponent', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [TranslateModule.forRoot(), SharedModule, CommonModule, RouterTestingModule], - declarations: [CollectionMetadataComponent], + imports: [TranslateModule.forRoot(), CommonModule, RouterTestingModule, CollectionMetadataComponent], providers: [ { provide: CollectionDataService, useValue: {} }, { provide: ItemTemplateDataService, useValue: itemTemplateServiceStub }, { provide: ActivatedRoute, useValue: { parent: { data: observableOf({ dso: createSuccessfulRemoteDataObject(collection) }) } } }, { provide: NotificationsService, useValue: notificationsService }, { provide: RequestService, useValue: requestService }, - { provide: Router, useValue: routerMock} + { provide: Router, useValue: routerMock }, + { provide: AuthService, useValue: new AuthServiceMock() }, + { provide: CommunityDataService, useValue: {} }, + { provide: ObjectCacheService, useValue: {} }, + { provide: APP_DATA_SERVICES_MAP, useValue: {} }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); diff --git a/src/app/collection-page/edit-collection-page/collection-metadata/collection-metadata.component.ts b/src/app/collection-page/edit-collection-page/collection-metadata/collection-metadata.component.ts index 634363527f7..df351997559 100644 --- a/src/app/collection-page/edit-collection-page/collection-metadata/collection-metadata.component.ts +++ b/src/app/collection-page/edit-collection-page/collection-metadata/collection-metadata.component.ts @@ -1,20 +1,49 @@ -import { ChangeDetectorRef, Component, OnInit } from '@angular/core'; -import { ComcolMetadataComponent } from '../../../shared/comcol/comcol-forms/edit-comcol-page/comcol-metadata/comcol-metadata.component'; -import { Collection } from '../../../core/shared/collection.model'; +import { + AsyncPipe, + NgIf, +} from '@angular/common'; +import { + ChangeDetectorRef, + Component, + OnInit, +} from '@angular/core'; +import { + ActivatedRoute, + NavigationEnd, + Router, + RouterLink, + Scroll, +} from '@angular/router'; +import { + TranslateModule, + TranslateService, +} from '@ngx-translate/core'; +import { + combineLatest as combineLatestObservable, + Observable, +} from 'rxjs'; +import { + map, + switchMap, +} from 'rxjs/operators'; + import { CollectionDataService } from '../../../core/data/collection-data.service'; -import { ActivatedRoute, NavigationEnd, Router, Scroll } from '@angular/router'; import { ItemTemplateDataService } from '../../../core/data/item-template-data.service'; -import { combineLatest as combineLatestObservable, Observable } from 'rxjs'; import { RemoteData } from '../../../core/data/remote-data'; -import { Item } from '../../../core/shared/item.model'; -import { getFirstCompletedRemoteData, getFirstSucceededRemoteDataPayload } from '../../../core/shared/operators'; -import { map, switchMap } from 'rxjs/operators'; -import { NotificationsService } from '../../../shared/notifications/notifications.service'; -import { TranslateService } from '@ngx-translate/core'; import { RequestService } from '../../../core/data/request.service'; -import { getCollectionItemTemplateRoute } from '../../collection-page-routing-paths'; +import { Collection } from '../../../core/shared/collection.model'; +import { Item } from '../../../core/shared/item.model'; import { NoContent } from '../../../core/shared/NoContent.model'; +import { + getFirstCompletedRemoteData, + getFirstSucceededRemoteDataPayload, +} from '../../../core/shared/operators'; +import { ComcolMetadataComponent } from '../../../shared/comcol/comcol-forms/edit-comcol-page/comcol-metadata/comcol-metadata.component'; import { hasValue } from '../../../shared/empty.util'; +import { NotificationsService } from '../../../shared/notifications/notifications.service'; +import { VarDirective } from '../../../shared/utils/var.directive'; +import { CollectionFormComponent } from '../../collection-form/collection-form.component'; +import { getCollectionItemTemplateRoute } from '../../collection-page-routing-paths'; /** * Component for editing a collection's metadata @@ -22,6 +51,15 @@ import { hasValue } from '../../../shared/empty.util'; @Component({ selector: 'ds-collection-metadata', templateUrl: './collection-metadata.component.html', + imports: [ + CollectionFormComponent, + RouterLink, + AsyncPipe, + TranslateModule, + NgIf, + VarDirective, + ], + standalone: true, }) export class CollectionMetadataComponent extends ComcolMetadataComponent implements OnInit { protected frontendURL = '/collections/'; @@ -40,13 +78,13 @@ export class CollectionMetadataComponent extends ComcolMetadataComponent this.itemTemplateService.findByCollectionID(collection.uuid)) + switchMap((collection: Collection) => this.itemTemplateService.findByCollectionID(collection.uuid)), ); } diff --git a/src/app/collection-page/edit-collection-page/collection-roles/collection-roles.component.spec.ts b/src/app/collection-page/edit-collection-page/collection-roles/collection-roles.component.spec.ts index c375a23ddf9..76ab4079f2c 100644 --- a/src/app/collection-page/edit-collection-page/collection-roles/collection-roles.component.spec.ts +++ b/src/app/collection-page/edit-collection-page/collection-roles/collection-roles.component.spec.ts @@ -1,22 +1,30 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { TranslateModule } from '@ngx-translate/core'; +import { + DebugElement, + NO_ERRORS_SCHEMA, +} from '@angular/core'; +import { + ComponentFixture, + TestBed, +} from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { ActivatedRoute } from '@angular/router'; +import { RouterTestingModule } from '@angular/router/testing'; +import { TranslateModule } from '@ngx-translate/core'; import { of as observableOf } from 'rxjs'; -import { DebugElement, NO_ERRORS_SCHEMA } from '@angular/core'; -import { By } from '@angular/platform-browser'; -import { CollectionRolesComponent } from './collection-roles.component'; -import { Collection } from '../../../core/shared/collection.model'; -import { SharedModule } from '../../../shared/shared.module'; -import { GroupDataService } from '../../../core/eperson/group-data.service'; + +import { DSONameService } from '../../../core/breadcrumbs/dso-name.service'; import { RequestService } from '../../../core/data/request.service'; -import { RouterTestingModule } from '@angular/router/testing'; -import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; -import { NoopAnimationsModule } from '@angular/platform-browser/animations'; -import { ComcolModule } from '../../../shared/comcol/comcol.module'; +import { GroupDataService } from '../../../core/eperson/group-data.service'; +import { Collection } from '../../../core/shared/collection.model'; +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 { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub'; -import { DSONameService } from '../../../core/breadcrumbs/dso-name.service'; -import { DSONameServiceMock } from '../../../shared/mocks/dso-name.service.mock'; +import { CollectionRolesComponent } from './collection-roles.component'; describe('CollectionRolesComponent', () => { @@ -54,10 +62,10 @@ describe('CollectionRolesComponent', () => { }, ], }, - }) + }), ), - }) - } + }), + }, }; const requestService = { @@ -70,13 +78,9 @@ describe('CollectionRolesComponent', () => { TestBed.configureTestingModule({ imports: [ - ComcolModule, - SharedModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), - NoopAnimationsModule - ], - declarations: [ + NoopAnimationsModule, CollectionRolesComponent, ], providers: [ @@ -84,9 +88,9 @@ describe('CollectionRolesComponent', () => { { provide: DSONameService, useValue: new DSONameServiceMock() }, { provide: RequestService, useValue: requestService }, { provide: GroupDataService, useValue: groupDataService }, - { provide: NotificationsService, useClass: NotificationsServiceStub } + { provide: NotificationsService, useClass: NotificationsServiceStub }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); fixture = TestBed.createComponent(CollectionRolesComponent); diff --git a/src/app/collection-page/edit-collection-page/collection-roles/collection-roles.component.ts b/src/app/collection-page/edit-collection-page/collection-roles/collection-roles.component.ts index 0177cc3a386..f402c99caf2 100644 --- a/src/app/collection-page/edit-collection-page/collection-roles/collection-roles.component.ts +++ b/src/app/collection-page/edit-collection-page/collection-roles/collection-roles.component.ts @@ -1,11 +1,26 @@ -import { Component, OnInit } from '@angular/core'; +import { + AsyncPipe, + NgForOf, +} from '@angular/common'; +import { + Component, + OnInit, +} from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { Observable } from 'rxjs'; -import { first, map } from 'rxjs/operators'; +import { + first, + map, +} from 'rxjs/operators'; + import { RemoteData } from '../../../core/data/remote-data'; import { Collection } from '../../../core/shared/collection.model'; -import { getRemoteDataPayload, getFirstSucceededRemoteData } from '../../../core/shared/operators'; import { HALLink } from '../../../core/shared/hal-link.model'; +import { + getFirstSucceededRemoteData, + getRemoteDataPayload, +} from '../../../core/shared/operators'; +import { ComcolRoleComponent } from '../../../shared/comcol/comcol-forms/edit-comcol-page/comcol-role/comcol-role.component'; import { hasValue } from '../../../shared/empty.util'; /** @@ -14,6 +29,12 @@ import { hasValue } from '../../../shared/empty.util'; @Component({ selector: 'ds-collection-roles', templateUrl: './collection-roles.component.html', + imports: [ + ComcolRoleComponent, + NgForOf, + AsyncPipe, + ], + standalone: true, }) export class CollectionRolesComponent implements OnInit { diff --git a/src/app/collection-page/edit-collection-page/collection-source/collection-source-controls/collection-source-controls.component.html b/src/app/collection-page/edit-collection-page/collection-source/collection-source-controls/collection-source-controls.component.html index 4d7b3e657ec..7edaadb0a1d 100644 --- a/src/app/collection-page/edit-collection-page/collection-source/collection-source-controls/collection-source-controls.component.html +++ b/src/app/collection-page/edit-collection-page/collection-source/collection-source-controls/collection-source-controls.component.html @@ -1,6 +1,6 @@
-

{{ 'collection.source.controls.head' | translate }}

+

{{ 'collection.source.controls.head' | translate }}

{{'collection.source.controls.harvest.status' | translate}} {{contentSource?.harvestStatus}} @@ -18,7 +18,7 @@

{{ 'collection.source.controls.head' | translate }}

{{contentSource?.message ? contentSource?.message: 'collection.source.controls.harvest.no-information'|translate }}
- - - -
-

{{ 'collection.edit.tabs.source.head' | translate }}

+

{{ 'collection.edit.tabs.source.head' | translate }}

- -

{{ 'collection.edit.tabs.source.form.head' | translate }}

+ +

{{ 'collection.edit.tabs.source.form.head' | translate }}

{{
- -
diff --git a/src/app/collection-page/edit-collection-page/collection-source/collection-source.component.spec.ts b/src/app/collection-page/edit-collection-page/collection-source/collection-source.component.spec.ts index e7e98d95233..3457d751756 100644 --- a/src/app/collection-page/edit-collection-page/collection-source/collection-source.component.spec.ts +++ b/src/app/collection-page/edit-collection-page/collection-source/collection-source.component.spec.ts @@ -1,25 +1,51 @@ -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { TranslateModule } from '@ngx-translate/core'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { + ComponentFixture, + TestBed, + waitForAsync, +} from '@angular/core/testing'; +import { + UntypedFormControl, + UntypedFormGroup, +} from '@angular/forms'; +import { By } from '@angular/platform-browser'; +import { + ActivatedRoute, + Router, +} from '@angular/router'; import { RouterTestingModule } from '@angular/router/testing'; -import { ActivatedRoute, Router } from '@angular/router'; +import { + DynamicFormControlModel, + DynamicFormService, +} from '@ng-dynamic-forms/core'; +import { TranslateModule } from '@ngx-translate/core'; import { of as observableOf } from 'rxjs'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { CollectionSourceComponent } from './collection-source.component'; -import { ContentSource, ContentSourceHarvestType } from '../../../core/shared/content-source.model'; + +import { CollectionDataService } from '../../../core/data/collection-data.service'; +import { FieldUpdate } from '../../../core/data/object-updates/field-update.model'; import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service'; -import { INotification, Notification } from '../../../shared/notifications/models/notification.model'; +import { RequestService } from '../../../core/data/request.service'; +import { Collection } from '../../../core/shared/collection.model'; +import { + ContentSource, + ContentSourceHarvestType, +} from '../../../core/shared/content-source.model'; +import { hasValue } from '../../../shared/empty.util'; +import { FormComponent } from '../../../shared/form/form.component'; +import { ThemedLoadingComponent } from '../../../shared/loading/themed-loading.component'; +import { + INotification, + Notification, +} from '../../../shared/notifications/models/notification.model'; import { NotificationType } from '../../../shared/notifications/models/notification-type'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; -import { DynamicFormControlModel, DynamicFormService } from '@ng-dynamic-forms/core'; -import { hasValue } from '../../../shared/empty.util'; -import { UntypedFormControl, UntypedFormGroup } from '@angular/forms'; +import { + createSuccessfulRemoteDataObject, + createSuccessfulRemoteDataObject$, +} from '../../../shared/remote-data.utils'; import { RouterStub } from '../../../shared/testing/router.stub'; -import { By } from '@angular/platform-browser'; -import { Collection } from '../../../core/shared/collection.model'; -import { CollectionDataService } from '../../../core/data/collection-data.service'; -import { RequestService } from '../../../core/data/request.service'; -import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; -import { FieldUpdate } from '../../../core/data/object-updates/field-update.model'; +import { CollectionSourceComponent } from './collection-source.component'; +import { CollectionSourceControlsComponent } from './collection-source-controls/collection-source-controls.component'; const infoNotification: INotification = new Notification('id', NotificationType.Info, 'info'); const warningNotification: INotification = new Notification('id', NotificationType.Warning, 'warning'); @@ -50,29 +76,29 @@ describe('CollectionSourceComponent', () => { { id: 'dc', label: 'Simple Dublin Core', - nameSpace: 'http://www.openarchives.org/OAI/2.0/oai_dc/' + nameSpace: 'http://www.openarchives.org/OAI/2.0/oai_dc/', }, { id: 'qdc', label: 'Qualified Dublin Core', - nameSpace: 'http://purl.org/dc/terms/' + nameSpace: 'http://purl.org/dc/terms/', }, { id: 'dim', label: 'DSpace Intermediate Metadata', - nameSpace: 'http://www.dspace.org/xmlns/dspace/dim' - } + nameSpace: 'http://www.dspace.org/xmlns/dspace/dim', + }, ], - _links: { self: { href: 'contentsource-selflink' } } + _links: { self: { href: 'contentsource-selflink' } }, }); fieldUpdate = { field: contentSource, - changeType: undefined + changeType: undefined, }; objectUpdatesService = jasmine.createSpyObj('objectUpdatesService', { getFieldUpdates: observableOf({ - [contentSource.uuid]: fieldUpdate + [contentSource.uuid]: fieldUpdate, }), saveAddFieldUpdate: {}, discardFieldUpdates: {}, @@ -82,15 +108,15 @@ describe('CollectionSourceComponent', () => { getLastModified: observableOf(date), hasUpdates: observableOf(true), isReinstatable: observableOf(false), - isValidPage: observableOf(true) - } + isValidPage: observableOf(true), + }, ); notificationsService = jasmine.createSpyObj('notificationsService', { info: infoNotification, warning: warningNotification, - success: successNotification - } + success: successNotification, + }, ); location = jasmine.createSpyObj('location', ['back']); formService = Object.assign({ @@ -103,24 +129,23 @@ describe('CollectionSourceComponent', () => { return new UntypedFormGroup(controls); } return undefined; - } + }, }); router = Object.assign(new RouterStub(), { - url: 'http://test-url.com/test-url' + url: 'http://test-url.com/test-url', }); collection = Object.assign(new Collection(), { - uuid: 'fake-collection-id' + uuid: 'fake-collection-id', }); collectionService = jasmine.createSpyObj('collectionService', { getContentSource: createSuccessfulRemoteDataObject$(contentSource), updateContentSource: observableOf(contentSource), - getHarvesterEndpoint: observableOf('harvester-endpoint') + getHarvesterEndpoint: observableOf('harvester-endpoint'), }); requestService = jasmine.createSpyObj('requestService', ['removeByHrefSubstring', 'setStaleByHrefSubstring']); TestBed.configureTestingModule({ - imports: [TranslateModule.forRoot(), RouterTestingModule], - declarations: [CollectionSourceComponent], + imports: [TranslateModule.forRoot(), RouterTestingModule, CollectionSourceComponent], providers: [ { provide: ObjectUpdatesService, useValue: objectUpdatesService }, { provide: NotificationsService, useValue: notificationsService }, @@ -129,10 +154,18 @@ describe('CollectionSourceComponent', () => { { provide: ActivatedRoute, useValue: { parent: { data: observableOf({ dso: createSuccessfulRemoteDataObject(collection) }) } } }, { provide: Router, useValue: router }, { provide: CollectionDataService, useValue: collectionService }, - { provide: RequestService, useValue: requestService } + { provide: RequestService, useValue: requestService }, ], - schemas: [NO_ERRORS_SCHEMA] - }).compileComponents(); + schemas: [NO_ERRORS_SCHEMA], + }) + .overrideComponent(CollectionSourceComponent, { + remove: { imports: [ + ThemedLoadingComponent, + FormComponent, + CollectionSourceControlsComponent, + ] }, + }) + .compileComponents(); })); beforeEach(() => { diff --git a/src/app/collection-page/edit-collection-page/collection-source/collection-source.component.ts b/src/app/collection-page/edit-collection-page/collection-source/collection-source.component.ts index 2d1308cc83a..43614e43a6c 100644 --- a/src/app/collection-page/edit-collection-page/collection-source/collection-source.component.ts +++ b/src/app/collection-page/edit-collection-page/collection-source/collection-source.component.ts @@ -1,5 +1,18 @@ -import { Component, OnDestroy, OnInit } from '@angular/core'; -import { AbstractTrackableComponent } from '../../../shared/trackable/abstract-trackable.component'; +import { + AsyncPipe, + Location, + NgIf, +} from '@angular/common'; +import { + Component, + OnDestroy, + OnInit, +} from '@angular/core'; +import { UntypedFormGroup } from '@angular/forms'; +import { + ActivatedRoute, + Router, +} from '@angular/router'; import { DynamicFormControlModel, DynamicFormGroupModel, @@ -8,29 +21,52 @@ import { DynamicInputModel, DynamicOptionControlModel, DynamicRadioGroupModel, - DynamicSelectModel + DynamicSelectModel, } from '@ng-dynamic-forms/core'; -import { Location } from '@angular/common'; -import { TranslateService } from '@ngx-translate/core'; +import { + TranslateModule, + TranslateService, +} from '@ngx-translate/core'; +import cloneDeep from 'lodash/cloneDeep'; +import { + Observable, + Subscription, +} from 'rxjs'; +import { + first, + map, + switchMap, + take, +} from 'rxjs/operators'; + +import { environment } from '../../../../environments/environment'; +import { CollectionDataService } from '../../../core/data/collection-data.service'; +import { FieldUpdate } from '../../../core/data/object-updates/field-update.model'; +import { FieldUpdates } from '../../../core/data/object-updates/field-updates.model'; import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service'; -import { NotificationsService } from '../../../shared/notifications/notifications.service'; -import { UntypedFormGroup } from '@angular/forms'; -import { hasNoValue, hasValue, isNotEmpty } from '../../../shared/empty.util'; -import { ContentSource, ContentSourceHarvestType } from '../../../core/shared/content-source.model'; -import { Observable, Subscription } from 'rxjs'; import { RemoteData } from '../../../core/data/remote-data'; +import { RequestService } from '../../../core/data/request.service'; import { Collection } from '../../../core/shared/collection.model'; -import { first, map, switchMap, take } from 'rxjs/operators'; -import { ActivatedRoute, Router } from '@angular/router'; -import cloneDeep from 'lodash/cloneDeep'; -import { CollectionDataService } from '../../../core/data/collection-data.service'; -import { getFirstSucceededRemoteData, getFirstCompletedRemoteData } from '../../../core/shared/operators'; +import { + ContentSource, + ContentSourceHarvestType, +} from '../../../core/shared/content-source.model'; import { MetadataConfig } from '../../../core/shared/metadata-config.model'; +import { + getFirstCompletedRemoteData, + getFirstSucceededRemoteData, +} from '../../../core/shared/operators'; +import { + hasNoValue, + hasValue, + isNotEmpty, +} from '../../../shared/empty.util'; +import { FormComponent } from '../../../shared/form/form.component'; +import { ThemedLoadingComponent } from '../../../shared/loading/themed-loading.component'; import { INotification } from '../../../shared/notifications/models/notification.model'; -import { RequestService } from '../../../core/data/request.service'; -import { environment } from '../../../../environments/environment'; -import { FieldUpdate } from '../../../core/data/object-updates/field-update.model'; -import { FieldUpdates } from '../../../core/data/object-updates/field-updates.model'; +import { NotificationsService } from '../../../shared/notifications/notifications.service'; +import { AbstractTrackableComponent } from '../../../shared/trackable/abstract-trackable.component'; +import { CollectionSourceControlsComponent } from './collection-source-controls/collection-source-controls.component'; /** * Component for managing the content source of the collection @@ -38,6 +74,15 @@ import { FieldUpdates } from '../../../core/data/object-updates/field-updates.mo @Component({ selector: 'ds-collection-source', templateUrl: './collection-source.component.html', + imports: [ + AsyncPipe, + TranslateModule, + NgIf, + ThemedLoadingComponent, + FormComponent, + CollectionSourceControlsComponent, + ], + standalone: true, }) export class CollectionSourceComponent extends AbstractTrackableComponent implements OnInit, OnDestroy { /** @@ -84,11 +129,11 @@ export class CollectionSourceComponent extends AbstractTrackableComponent implem name: 'oaiSource', required: true, validators: { - required: null + required: null, }, errorMessages: { - required: 'You must provide a set id of the target collection.' - } + required: 'You must provide a set id of the target collection.', + }, }); /** @@ -96,7 +141,7 @@ export class CollectionSourceComponent extends AbstractTrackableComponent implem */ oaiSetIdModel = new DynamicInputModel({ id: 'oaiSetId', - name: 'oaiSetId' + name: 'oaiSetId', }); /** @@ -104,7 +149,7 @@ export class CollectionSourceComponent extends AbstractTrackableComponent implem */ metadataConfigIdModel = new DynamicSelectModel({ id: 'metadataConfigId', - name: 'metadataConfigId' + name: 'metadataConfigId', }); /** @@ -115,15 +160,15 @@ export class CollectionSourceComponent extends AbstractTrackableComponent implem name: 'harvestType', options: [ { - value: ContentSourceHarvestType.Metadata + value: ContentSourceHarvestType.Metadata, }, { - value: ContentSourceHarvestType.MetadataAndRef + value: ContentSourceHarvestType.MetadataAndRef, }, { - value: ContentSourceHarvestType.MetadataAndBitstreams - } - ] + value: ContentSourceHarvestType.MetadataAndBitstreams, + }, + ], }); /** @@ -139,22 +184,22 @@ export class CollectionSourceComponent extends AbstractTrackableComponent implem new DynamicFormGroupModel({ id: 'oaiSourceContainer', group: [ - this.oaiSourceModel - ] + this.oaiSourceModel, + ], }), new DynamicFormGroupModel({ id: 'oaiSetContainer', group: [ this.oaiSetIdModel, - this.metadataConfigIdModel - ] + this.metadataConfigIdModel, + ], }), new DynamicFormGroupModel({ id: 'harvestTypeContainer', group: [ - this.harvestTypeModel - ] - }) + this.harvestTypeModel, + ], + }), ]; /** @@ -163,40 +208,40 @@ export class CollectionSourceComponent extends AbstractTrackableComponent implem formLayout: DynamicFormLayout = { oaiSource: { grid: { - host: 'col-12 d-inline-block' - } + host: 'col-12 d-inline-block', + }, }, oaiSetId: { grid: { - host: 'col col-sm-6 d-inline-block' - } + host: 'col col-sm-6 d-inline-block', + }, }, metadataConfigId: { grid: { - host: 'col col-sm-6 d-inline-block' - } + host: 'col col-sm-6 d-inline-block', + }, }, harvestType: { grid: { host: 'col-12', - option: 'btn-outline-secondary' - } + option: 'btn-outline-secondary', + }, }, oaiSetContainer: { grid: { - host: 'row' - } + host: 'row', + }, }, oaiSourceContainer: { grid: { - host: 'row' - } + host: 'row', + }, }, harvestTypeContainer: { grid: { - host: 'row' - } - } + host: 'row', + }, + }, }; /** @@ -204,11 +249,6 @@ export class CollectionSourceComponent extends AbstractTrackableComponent implem */ formGroup: UntypedFormGroup; - /** - * Subscription to update the current form - */ - updateSub: Subscription; - /** * The content harvesting type used when harvesting is disabled */ @@ -228,28 +268,29 @@ export class CollectionSourceComponent extends AbstractTrackableComponent implem */ displayedNotifications: INotification[] = []; - public constructor(public objectUpdatesService: ObjectUpdatesService, - public notificationsService: NotificationsService, - protected location: Location, - protected formService: DynamicFormService, - protected translate: TranslateService, - protected route: ActivatedRoute, - protected router: Router, - protected collectionService: CollectionDataService, - protected requestService: RequestService) { - super(objectUpdatesService, notificationsService, translate); + subs: Subscription[] = []; + + public constructor( + public objectUpdatesService: ObjectUpdatesService, + public notificationsService: NotificationsService, + public translateService: TranslateService, + public router: Router, + protected location: Location, + protected formService: DynamicFormService, + protected route: ActivatedRoute, + protected collectionService: CollectionDataService, + protected requestService: RequestService, + ) { + super(objectUpdatesService, notificationsService, translateService, router); } /** * Initialize properties to setup the Field Update and Form */ ngOnInit(): void { + super.ngOnInit(); this.notificationsPrefix = 'collection.edit.tabs.source.notifications.'; this.discardTimeOut = environment.collection.edit.undoTimeout; - this.url = this.router.url; - if (this.url.indexOf('?') > 0) { - this.url = this.url.substr(0, this.url.indexOf('?')); - } this.formGroup = this.formService.createFormGroup(this.formModel); this.collectionRD$ = this.route.parent.data.pipe(first(), map((data) => data.dso)); @@ -263,10 +304,9 @@ export class CollectionSourceComponent extends AbstractTrackableComponent implem }); this.updateFieldTranslations(); - this.translate.onLangChange - .subscribe(() => { - this.updateFieldTranslations(); - }); + this.subs.push(this.translateService.onLangChange.subscribe(() => { + this.updateFieldTranslations(); + })); } /** @@ -279,9 +319,9 @@ export class CollectionSourceComponent extends AbstractTrackableComponent implem const initialContentSource = cloneDeep(this.contentSource); this.objectUpdatesService.initialize(this.url, [initialContentSource], new Date()); this.update$ = this.objectUpdatesService.getFieldUpdates(this.url, [initialContentSource]).pipe( - map((updates: FieldUpdates) => updates[initialContentSource.uuid]) + map((updates: FieldUpdates) => updates[initialContentSource.uuid]), ); - this.updateSub = this.update$.subscribe((update: FieldUpdate) => { + this.subs.push(this.update$.subscribe((update: FieldUpdate) => { if (update) { const field = update.field as ContentSource; let configId; @@ -294,21 +334,21 @@ export class CollectionSourceComponent extends AbstractTrackableComponent implem if (hasValue(field)) { this.formGroup.patchValue({ oaiSourceContainer: { - oaiSource: field.oaiSource + oaiSource: field.oaiSource, }, oaiSetContainer: { oaiSetId: field.oaiSetId, - metadataConfigId: configId + metadataConfigId: configId, }, harvestTypeContainer: { - harvestType: field.harvestType - } + harvestType: field.harvestType, + }, }); this.contentSource = cloneDeep(field); } this.contentSource.metadataConfigId = configId; } - }); + })); } /** @@ -320,8 +360,8 @@ export class CollectionSourceComponent extends AbstractTrackableComponent implem if (this.metadataConfigIdModel.options.length > 0) { this.formGroup.patchValue({ oaiSetContainer: { - metadataConfigId: this.metadataConfigIdModel.options[0].value - } + metadataConfigId: this.metadataConfigIdModel.options[0].value, + }, }); } } @@ -333,7 +373,7 @@ export class CollectionSourceComponent extends AbstractTrackableComponent implem this.inputModels.forEach( (fieldModel: DynamicFormControlModel) => { this.updateFieldTranslation(fieldModel); - } + }, ); } @@ -342,18 +382,18 @@ export class CollectionSourceComponent extends AbstractTrackableComponent implem * @param fieldModel */ private updateFieldTranslation(fieldModel: DynamicFormControlModel) { - fieldModel.label = this.translate.instant(this.LABEL_KEY_PREFIX + fieldModel.id); + fieldModel.label = this.translateService.instant(this.LABEL_KEY_PREFIX + fieldModel.id); if (isNotEmpty(fieldModel.validators)) { fieldModel.errorMessages = {}; Object.keys(fieldModel.validators).forEach((key) => { - fieldModel.errorMessages[key] = this.translate.instant(this.ERROR_KEY_PREFIX + fieldModel.id + '.' + key); + fieldModel.errorMessages[key] = this.translateService.instant(this.ERROR_KEY_PREFIX + fieldModel.id + '.' + key); }); } if (fieldModel instanceof DynamicOptionControlModel) { if (isNotEmpty(fieldModel.options)) { fieldModel.options.forEach((option) => { if (hasNoValue(option.label)) { - option.label = this.translate.instant(this.OPTIONS_KEY_PREFIX + fieldModel.id + '.' + option.value); + option.label = this.translateService.instant(this.OPTIONS_KEY_PREFIX + fieldModel.id + '.' + option.value); } }); } @@ -378,7 +418,7 @@ export class CollectionSourceComponent extends AbstractTrackableComponent implem getFirstSucceededRemoteData(), map((col) => col.payload.uuid), switchMap((uuid) => this.collectionService.getHarvesterEndpoint(uuid)), - take(1) + take(1), ).subscribe((endpoint) => this.requestService.removeByHrefSubstring(endpoint)); this.requestService.setStaleByHrefSubstring(this.contentSource._links.self.href); // Update harvester @@ -386,7 +426,7 @@ export class CollectionSourceComponent extends AbstractTrackableComponent implem getFirstSucceededRemoteData(), map((col) => col.payload.uuid), switchMap((uuid) => this.collectionService.updateContentSource(uuid, this.contentSource)), - take(1) + take(1), ).subscribe((result: ContentSource | INotification) => { if (hasValue((result as any).harvestType)) { this.clearNotifications(); @@ -433,7 +473,7 @@ export class CollectionSourceComponent extends AbstractTrackableComponent implem this.inputModels.forEach( (fieldModel: DynamicInputModel) => { this.updateContentSourceField(fieldModel, updateHarvestType); - } + }, ); this.saveFieldUpdate(); } @@ -470,8 +510,6 @@ export class CollectionSourceComponent extends AbstractTrackableComponent implem * Make sure open subscriptions are closed */ ngOnDestroy(): void { - if (this.updateSub) { - this.updateSub.unsubscribe(); - } + this.subs.forEach((sub: Subscription) => sub.unsubscribe()); } } diff --git a/src/app/collection-page/edit-collection-page/edit-collection-page-routes.ts b/src/app/collection-page/edit-collection-page/edit-collection-page-routes.ts new file mode 100644 index 00000000000..19dbaa616b2 --- /dev/null +++ b/src/app/collection-page/edit-collection-page/edit-collection-page-routes.ts @@ -0,0 +1,100 @@ +import { Route } from '@angular/router'; + +import { i18nBreadcrumbResolver } from '../../core/breadcrumbs/i18n-breadcrumb.resolver'; +import { collectionAdministratorGuard } from '../../core/data/feature-authorization/feature-authorization-guard/collection-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 { CollectionItemMapperComponent } from '../collection-item-mapper/collection-item-mapper.component'; +import { CollectionAccessControlComponent } from './collection-access-control/collection-access-control.component'; +import { CollectionAuthorizationsComponent } from './collection-authorizations/collection-authorizations.component'; +import { CollectionCurateComponent } from './collection-curate/collection-curate.component'; +import { CollectionMetadataComponent } from './collection-metadata/collection-metadata.component'; +import { CollectionRolesComponent } from './collection-roles/collection-roles.component'; +import { CollectionSourceComponent } from './collection-source/collection-source.component'; +import { EditCollectionPageComponent } from './edit-collection-page.component'; + +/** + * Routing module that handles the routing for the Edit Collection page administrator functionality + */ + +export const ROUTES: Route[] = [ + { + path: '', + resolve: { + breadcrumb: i18nBreadcrumbResolver, + }, + data: { breadcrumbKey: 'collection.edit' }, + component: EditCollectionPageComponent, + canActivate: [collectionAdministratorGuard], + children: [ + { + path: '', + redirectTo: 'metadata', + pathMatch: 'full', + }, + { + path: 'metadata', + component: CollectionMetadataComponent, + data: { + title: 'collection.edit.tabs.metadata.title', + hideReturnButton: true, + showBreadcrumbs: true, + }, + }, + { + path: 'roles', + component: CollectionRolesComponent, + data: { title: 'collection.edit.tabs.roles.title', showBreadcrumbs: true }, + }, + { + path: 'source', + component: CollectionSourceComponent, + data: { title: 'collection.edit.tabs.source.title', showBreadcrumbs: true }, + }, + { + path: 'curate', + component: CollectionCurateComponent, + data: { title: 'collection.edit.tabs.curate.title', showBreadcrumbs: true }, + }, + { + path: 'access-control', + component: CollectionAccessControlComponent, + 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: CollectionAuthorizationsComponent, + data: { title: 'collection.edit.tabs.authorizations.title', showBreadcrumbs: true }, + }, + ], + }, + { + path: 'mapper', + component: CollectionItemMapperComponent, + data: { title: 'collection.edit.tabs.item-mapper.title', hideReturnButton: true, showBreadcrumbs: true }, + }, + ], + }, +]; diff --git a/src/app/collection-page/edit-collection-page/edit-collection-page.component.spec.ts b/src/app/collection-page/edit-collection-page/edit-collection-page.component.spec.ts index 00f05f50c0f..3ef2b65df92 100644 --- a/src/app/collection-page/edit-collection-page/edit-collection-page.component.spec.ts +++ b/src/app/collection-page/edit-collection-page/edit-collection-page.component.spec.ts @@ -1,50 +1,53 @@ -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { ActivatedRoute } from '@angular/router'; -import { TranslateModule } from '@ngx-translate/core'; import { CommonModule } from '@angular/common'; -import { RouterTestingModule } from '@angular/router/testing'; import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { EditCollectionPageComponent } from './edit-collection-page.component'; -import { SharedModule } from '../../shared/shared.module'; -import { CollectionDataService } from '../../core/data/collection-data.service'; +import { + ComponentFixture, + TestBed, + waitForAsync, +} from '@angular/core/testing'; +import { ActivatedRoute } from '@angular/router'; +import { RouterTestingModule } from '@angular/router/testing'; +import { TranslateModule } from '@ngx-translate/core'; import { of as observableOf } from 'rxjs'; +import { CollectionDataService } from '../../core/data/collection-data.service'; +import { EditCollectionPageComponent } from './edit-collection-page.component'; + describe('EditCollectionPageComponent', () => { let comp: EditCollectionPageComponent; let fixture: ComponentFixture; const routeStub = { data: observableOf({ - dso: { payload: {} } + dso: { payload: {} }, }), routeConfig: { children: [ { path: 'mockUrl', data: { - hideReturnButton: false - } - } - ] + hideReturnButton: false, + }, + }, + ], }, snapshot: { firstChild: { routeConfig: { - path: 'mockUrl' - } - } - } + path: 'mockUrl', + }, + }, + }, }; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [TranslateModule.forRoot(), SharedModule, CommonModule, RouterTestingModule], - declarations: [EditCollectionPageComponent], + imports: [TranslateModule.forRoot(), CommonModule, RouterTestingModule, EditCollectionPageComponent], providers: [ { provide: CollectionDataService, useValue: {} }, { provide: ActivatedRoute, useValue: routeStub }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); diff --git a/src/app/collection-page/edit-collection-page/edit-collection-page.component.ts b/src/app/collection-page/edit-collection-page/edit-collection-page.component.ts index 62fbb3ee3df..4508ba73427 100644 --- a/src/app/collection-page/edit-collection-page/edit-collection-page.component.ts +++ b/src/app/collection-page/edit-collection-page/edit-collection-page.component.ts @@ -1,7 +1,20 @@ +import { + AsyncPipe, + NgClass, + NgForOf, + NgIf, +} from '@angular/common'; import { Component } from '@angular/core'; -import { ActivatedRoute, Router } from '@angular/router'; -import { EditComColPageComponent } from '../../shared/comcol/comcol-forms/edit-comcol-page/edit-comcol-page.component'; +import { + ActivatedRoute, + Router, + RouterLink, + RouterOutlet, +} from '@angular/router'; +import { TranslateModule } from '@ngx-translate/core'; + import { Collection } from '../../core/shared/collection.model'; +import { EditComColPageComponent } from '../../shared/comcol/comcol-forms/edit-comcol-page/edit-comcol-page.component'; import { getCollectionPageRoute } from '../collection-page-routing-paths'; /** @@ -9,14 +22,24 @@ import { getCollectionPageRoute } from '../collection-page-routing-paths'; */ @Component({ selector: 'ds-edit-collection', - templateUrl: '../../shared/comcol/comcol-forms/edit-comcol-page/edit-comcol-page.component.html' + templateUrl: '../../shared/comcol/comcol-forms/edit-comcol-page/edit-comcol-page.component.html', + imports: [ + RouterLink, + TranslateModule, + NgClass, + NgForOf, + RouterOutlet, + NgIf, + AsyncPipe, + ], + standalone: true, }) export class EditCollectionPageComponent extends EditComColPageComponent { type = 'collection'; public constructor( protected router: Router, - protected route: ActivatedRoute + protected route: ActivatedRoute, ) { super(router, route); } diff --git a/src/app/collection-page/edit-collection-page/edit-collection-page.module.ts b/src/app/collection-page/edit-collection-page/edit-collection-page.module.ts deleted file mode 100644 index 8d0cb179f1f..00000000000 --- a/src/app/collection-page/edit-collection-page/edit-collection-page.module.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { NgModule } from '@angular/core'; -import { EditCollectionPageComponent } from './edit-collection-page.component'; -import { CommonModule } from '@angular/common'; -import { SharedModule } from '../../shared/shared.module'; -import { EditCollectionPageRoutingModule } from './edit-collection-page.routing.module'; -import { CollectionMetadataComponent } from './collection-metadata/collection-metadata.component'; -import { CollectionRolesComponent } from './collection-roles/collection-roles.component'; -import { CollectionCurateComponent } from './collection-curate/collection-curate.component'; -import { CollectionSourceComponent } from './collection-source/collection-source.component'; -import { CollectionAuthorizationsComponent } from './collection-authorizations/collection-authorizations.component'; -import { CollectionFormModule } from '../collection-form/collection-form.module'; -import { - CollectionSourceControlsComponent -} from './collection-source/collection-source-controls/collection-source-controls.component'; -import { ResourcePoliciesModule } from '../../shared/resource-policies/resource-policies.module'; -import { FormModule } from '../../shared/form/form.module'; -import { ComcolModule } from '../../shared/comcol/comcol.module'; -import { CollectionAccessControlComponent } from './collection-access-control/collection-access-control.component'; -import { AccessControlFormModule } from '../../shared/access-control-form-container/access-control-form.module'; - -/** - * Module that contains all components related to the Edit Collection page administrator functionality - */ -@NgModule({ - imports: [ - CommonModule, - SharedModule, - EditCollectionPageRoutingModule, - CollectionFormModule, - ResourcePoliciesModule, - FormModule, - ComcolModule, - AccessControlFormModule, - ], - declarations: [ - EditCollectionPageComponent, - CollectionMetadataComponent, - CollectionRolesComponent, - CollectionCurateComponent, - CollectionSourceComponent, - CollectionAccessControlComponent, - CollectionSourceControlsComponent, - CollectionAuthorizationsComponent - ] -}) -export class EditCollectionPageModule { - -} diff --git a/src/app/collection-page/edit-collection-page/edit-collection-page.routing.module.ts b/src/app/collection-page/edit-collection-page/edit-collection-page.routing.module.ts deleted file mode 100644 index c4481985c0a..00000000000 --- a/src/app/collection-page/edit-collection-page/edit-collection-page.routing.module.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { RouterModule } from '@angular/router'; -import { NgModule } from '@angular/core'; -import { CollectionItemMapperComponent } from '../collection-item-mapper/collection-item-mapper.component'; -import { EditCollectionPageComponent } from './edit-collection-page.component'; -import { CollectionMetadataComponent } from './collection-metadata/collection-metadata.component'; -import { CollectionRolesComponent } from './collection-roles/collection-roles.component'; -import { CollectionSourceComponent } from './collection-source/collection-source.component'; -import { CollectionCurateComponent } from './collection-curate/collection-curate.component'; -import { CollectionAuthorizationsComponent } from './collection-authorizations/collection-authorizations.component'; -import { I18nBreadcrumbResolver } from '../../core/breadcrumbs/i18n-breadcrumb.resolver'; -import { ResourcePolicyTargetResolver } from '../../shared/resource-policies/resolvers/resource-policy-target.resolver'; -import { ResourcePolicyCreateComponent } from '../../shared/resource-policies/create/resource-policy-create.component'; -import { ResourcePolicyResolver } from '../../shared/resource-policies/resolvers/resource-policy.resolver'; -import { ResourcePolicyEditComponent } from '../../shared/resource-policies/edit/resource-policy-edit.component'; -import { CollectionAdministratorGuard } from '../../core/data/feature-authorization/feature-authorization-guard/collection-administrator.guard'; -import { CollectionAccessControlComponent } from './collection-access-control/collection-access-control.component'; - -/** - * Routing module that handles the routing for the Edit Collection page administrator functionality - */ -@NgModule({ - imports: [ - RouterModule.forChild([ - { - path: '', - resolve: { - breadcrumb: I18nBreadcrumbResolver - }, - data: { breadcrumbKey: 'collection.edit' }, - component: EditCollectionPageComponent, - canActivate: [CollectionAdministratorGuard], - children: [ - { - path: '', - redirectTo: 'metadata', - pathMatch: 'full' - }, - { - path: 'metadata', - component: CollectionMetadataComponent, - data: { - title: 'collection.edit.tabs.metadata.title', - hideReturnButton: true, - showBreadcrumbs: true - } - }, - { - path: 'roles', - component: CollectionRolesComponent, - data: { title: 'collection.edit.tabs.roles.title', showBreadcrumbs: true } - }, - { - path: 'source', - component: CollectionSourceComponent, - data: { title: 'collection.edit.tabs.source.title', showBreadcrumbs: true } - }, - { - path: 'curate', - component: CollectionCurateComponent, - data: { title: 'collection.edit.tabs.curate.title', showBreadcrumbs: true } - }, - { - path: 'access-control', - component: CollectionAccessControlComponent, - data: { title: 'collection.edit.tabs.access-control.title', showBreadcrumbs: true } - }, -/* { - path: 'authorizations', - component: CollectionAuthorizationsComponent, - data: { title: 'collection.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: CollectionAuthorizationsComponent, - data: { title: 'collection.edit.tabs.authorizations.title', showBreadcrumbs: true } - } - ] - }, - { - path: 'mapper', - component: CollectionItemMapperComponent, - data: { title: 'collection.edit.tabs.item-mapper.title', hideReturnButton: true, showBreadcrumbs: true } - }, - ] - } - ]) - ], - providers: [ - ResourcePolicyResolver, - ResourcePolicyTargetResolver - ] -}) -export class EditCollectionPageRoutingModule { - -} diff --git a/src/app/collection-page/edit-item-template-page/edit-item-template-page.component.html b/src/app/collection-page/edit-item-template-page/edit-item-template-page.component.html index 20afd701ffc..7f0b2efba22 100644 --- a/src/app/collection-page/edit-item-template-page/edit-item-template-page.component.html +++ b/src/app/collection-page/edit-item-template-page/edit-item-template-page.component.html @@ -2,11 +2,11 @@
-

{{ 'collection.edit.template.head' | translate:{ collection: dsoNameService.getName(collection) } }}

- +

{{ 'collection.edit.template.head' | translate:{ collection: dsoNameService.getName(collection) } }}

+
- +
diff --git a/src/app/collection-page/edit-item-template-page/edit-item-template-page.component.spec.ts b/src/app/collection-page/edit-item-template-page/edit-item-template-page.component.spec.ts index 72b776dd7d5..4d33e7d008a 100644 --- a/src/app/collection-page/edit-item-template-page/edit-item-template-page.component.spec.ts +++ b/src/app/collection-page/edit-item-template-page/edit-item-template-page.component.spec.ts @@ -1,16 +1,28 @@ -import { EditItemTemplatePageComponent } from './edit-item-template-page.component'; -import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing'; -import { TranslateModule } from '@ngx-translate/core'; -import { SharedModule } from '../../shared/shared.module'; -import { RouterTestingModule } from '@angular/router/testing'; import { CommonModule } from '@angular/common'; -import { ItemTemplateDataService } from '../../core/data/item-template-data.service'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { + ComponentFixture, + TestBed, + waitForAsync, +} from '@angular/core/testing'; import { ActivatedRoute } from '@angular/router'; +import { RouterTestingModule } from '@angular/router/testing'; +import { TranslateModule } from '@ngx-translate/core'; import { of as observableOf } from 'rxjs'; + +import { ItemTemplateDataService } from '../../core/data/item-template-data.service'; import { Collection } from '../../core/shared/collection.model'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; +import { ThemedDsoEditMetadataComponent } from '../../dso-shared/dso-edit-metadata/themed-dso-edit-metadata.component'; +import { getMockThemeService } from '../../shared/mocks/theme-service.mock'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { + createSuccessfulRemoteDataObject, + createSuccessfulRemoteDataObject$, +} from '../../shared/remote-data.utils'; +import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub'; +import { ThemeService } from '../../shared/theme-support/theme.service'; import { getCollectionEditRoute } from '../collection-page-routing-paths'; +import { EditItemTemplatePageComponent } from './edit-item-template-page.component'; describe('EditItemTemplatePageComponent', () => { let comp: EditItemTemplatePageComponent; @@ -22,19 +34,24 @@ describe('EditItemTemplatePageComponent', () => { collection = Object.assign(new Collection(), { uuid: 'collection-id', id: 'collection-id', - name: 'Fake Collection' + name: 'Fake Collection', }); itemTemplateService = jasmine.createSpyObj('itemTemplateService', { - findByCollectionID: createSuccessfulRemoteDataObject$({}) + findByCollectionID: createSuccessfulRemoteDataObject$({}), }); TestBed.configureTestingModule({ - imports: [TranslateModule.forRoot(), SharedModule, CommonModule, RouterTestingModule], - declarations: [EditItemTemplatePageComponent], + imports: [TranslateModule.forRoot(), CommonModule, RouterTestingModule, EditItemTemplatePageComponent], providers: [ { provide: ItemTemplateDataService, useValue: itemTemplateService }, - { provide: ActivatedRoute, useValue: { parent: { data: observableOf({ dso: createSuccessfulRemoteDataObject(collection) }) } } } + { provide: ActivatedRoute, useValue: { parent: { data: observableOf({ dso: createSuccessfulRemoteDataObject(collection) }) } } }, + { provide: ThemeService, useValue: getMockThemeService() }, + { provide: NotificationsService, useValue: new NotificationsServiceStub() }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], + }).overrideComponent(EditItemTemplatePageComponent, { + remove: { + imports: [ThemedDsoEditMetadataComponent], + }, }).compileComponents(); })); diff --git a/src/app/collection-page/edit-item-template-page/edit-item-template-page.component.ts b/src/app/collection-page/edit-item-template-page/edit-item-template-page.component.ts index 6425996fd2b..f7c5dc4b14a 100644 --- a/src/app/collection-page/edit-item-template-page/edit-item-template-page.component.ts +++ b/src/app/collection-page/edit-item-template-page/edit-item-template-page.component.ts @@ -1,19 +1,50 @@ -import { Component, OnInit } from '@angular/core'; +import { + AsyncPipe, + NgIf, +} from '@angular/common'; +import { + Component, + OnInit, +} from '@angular/core'; +import { + ActivatedRoute, + RouterLink, +} from '@angular/router'; +import { TranslateModule } from '@ngx-translate/core'; import { Observable } from 'rxjs'; +import { + first, + map, + switchMap, +} from 'rxjs/operators'; + +import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; +import { ItemTemplateDataService } from '../../core/data/item-template-data.service'; import { RemoteData } from '../../core/data/remote-data'; import { Collection } from '../../core/shared/collection.model'; -import { ActivatedRoute } from '@angular/router'; -import { first, map, switchMap } from 'rxjs/operators'; -import { ItemTemplateDataService } from '../../core/data/item-template-data.service'; -import { getCollectionEditRoute } from '../collection-page-routing-paths'; import { Item } from '../../core/shared/item.model'; import { getFirstSucceededRemoteDataPayload } from '../../core/shared/operators'; -import { AlertType } from '../../shared/alert/aletr-type'; -import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; +import { ThemedDsoEditMetadataComponent } from '../../dso-shared/dso-edit-metadata/themed-dso-edit-metadata.component'; +import { AlertComponent } from '../../shared/alert/alert.component'; +import { AlertType } from '../../shared/alert/alert-type'; +import { ThemedLoadingComponent } from '../../shared/loading/themed-loading.component'; +import { VarDirective } from '../../shared/utils/var.directive'; +import { getCollectionEditRoute } from '../collection-page-routing-paths'; @Component({ - selector: 'ds-edit-item-template-page', + selector: 'ds-base-edit-item-template-page', templateUrl: './edit-item-template-page.component.html', + imports: [ + ThemedDsoEditMetadataComponent, + RouterLink, + AsyncPipe, + VarDirective, + NgIf, + TranslateModule, + ThemedLoadingComponent, + AlertComponent, + ], + standalone: true, }) /** * Component for editing the item template of a collection diff --git a/src/app/collection-page/edit-item-template-page/item-template-page.resolver.spec.ts b/src/app/collection-page/edit-item-template-page/item-template-page.resolver.spec.ts index 95f0d888e47..c622c622679 100644 --- a/src/app/collection-page/edit-item-template-page/item-template-page.resolver.spec.ts +++ b/src/app/collection-page/edit-item-template-page/item-template-page.resolver.spec.ts @@ -1,33 +1,30 @@ +import { Observable } from 'rxjs'; import { first } from 'rxjs/operators'; -import { ItemTemplatePageResolver } from './item-template-page.resolver'; import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; -import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; -import { DSONameServiceMock } from '../../shared/mocks/dso-name.service.mock'; +import { itemTemplatePageResolver } from './item-template-page.resolver'; -describe('ItemTemplatePageResolver', () => { +describe('itemTemplatePageResolver', () => { describe('resolve', () => { - let resolver: ItemTemplatePageResolver; + let resolver: any; let itemTemplateService: any; - let dsoNameService: DSONameServiceMock; const uuid = '1234-65487-12354-1235'; beforeEach(() => { itemTemplateService = { - findByCollectionID: (id: string) => createSuccessfulRemoteDataObject$({ id }) + findByCollectionID: (id: string) => createSuccessfulRemoteDataObject$({ id }), }; - dsoNameService = new DSONameServiceMock(); - resolver = new ItemTemplatePageResolver(dsoNameService as DSONameService, itemTemplateService); + resolver = itemTemplatePageResolver; }); it('should resolve an item template with the correct id', (done) => { - resolver.resolve({ params: { id: uuid } } as any, undefined) + (resolver({ params: { id: uuid } } as any, undefined, itemTemplateService) as Observable) .pipe(first()) .subscribe( (resolved) => { expect(resolved.payload.id).toEqual(uuid); done(); - } + }, ); }); }); diff --git a/src/app/collection-page/edit-item-template-page/item-template-page.resolver.ts b/src/app/collection-page/edit-item-template-page/item-template-page.resolver.ts index 586617c44c1..d35cd0a3b04 100644 --- a/src/app/collection-page/edit-item-template-page/item-template-page.resolver.ts +++ b/src/app/collection-page/edit-item-template-page/item-template-page.resolver.ts @@ -1,34 +1,23 @@ -import { Injectable } from '@angular/core'; -import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router'; +import { inject } from '@angular/core'; +import { + ActivatedRouteSnapshot, + ResolveFn, + RouterStateSnapshot, +} from '@angular/router'; +import { Observable } from 'rxjs'; + +import { ItemTemplateDataService } from '../../core/data/item-template-data.service'; import { RemoteData } from '../../core/data/remote-data'; import { Item } from '../../core/shared/item.model'; -import { ItemTemplateDataService } from '../../core/data/item-template-data.service'; -import { Observable } from 'rxjs'; -import { followLink } from '../../shared/utils/follow-link-config.model'; import { getFirstCompletedRemoteData } from '../../core/shared/operators'; -import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; - -/** - * This class represents a resolver that requests a specific collection's item template before the route is activated - */ -@Injectable() -export class ItemTemplatePageResolver implements Resolve> { - constructor( - public dsoNameService: DSONameService, - private itemTemplateService: ItemTemplateDataService, - ) { - } +import { followLink } from '../../shared/utils/follow-link-config.model'; - /** - * Method for resolving a collection's item template 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 item template based on the parameters in the current route, - * or an error if something went wrong - */ - resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable> { - return this.itemTemplateService.findByCollectionID(route.params.id, true, false, followLink('templateItemOf')).pipe( - getFirstCompletedRemoteData(), - ); - } -} +export const itemTemplatePageResolver: ResolveFn> = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + itemTemplateService: ItemTemplateDataService = inject(ItemTemplateDataService), +): Observable> => { + return itemTemplateService.findByCollectionID(route.params.id, true, false, followLink('templateItemOf')).pipe( + getFirstCompletedRemoteData(), + ); +}; diff --git a/src/app/collection-page/edit-item-template-page/themed-edit-item-template-page.component.ts b/src/app/collection-page/edit-item-template-page/themed-edit-item-template-page.component.ts index b53f4e6c45d..421049990ae 100644 --- a/src/app/collection-page/edit-item-template-page/themed-edit-item-template-page.component.ts +++ b/src/app/collection-page/edit-item-template-page/themed-edit-item-template-page.component.ts @@ -1,11 +1,14 @@ import { Component } from '@angular/core'; + import { ThemedComponent } from '../../shared/theme-support/themed.component'; import { EditItemTemplatePageComponent } from './edit-item-template-page.component'; @Component({ - selector: 'ds-themed-edit-item-template-page', + selector: 'ds-edit-item-template-page', styleUrls: [], templateUrl: '../../shared/theme-support/themed.component.html', + standalone: true, + imports: [EditItemTemplatePageComponent], }) /** * Component for editing the item template of a collection diff --git a/src/app/collection-page/themed-collection-page.component.ts b/src/app/collection-page/themed-collection-page.component.ts index 2faf418423a..c84d7c5fb44 100644 --- a/src/app/collection-page/themed-collection-page.component.ts +++ b/src/app/collection-page/themed-collection-page.component.ts @@ -1,4 +1,5 @@ import { Component } from '@angular/core'; + import { ThemedComponent } from '../shared/theme-support/themed.component'; import { CollectionPageComponent } from './collection-page.component'; @@ -6,9 +7,11 @@ import { CollectionPageComponent } from './collection-page.component'; * Themed wrapper for CollectionPageComponent */ @Component({ - selector: 'ds-themed-collection-page', + selector: 'ds-collection-page', styleUrls: [], templateUrl: '../shared/theme-support/themed.component.html', + standalone: true, + imports: [CollectionPageComponent], }) export class ThemedCollectionPageComponent extends ThemedComponent { protected getComponentName(): string { diff --git a/src/app/community-list-page/community-list-datasource.ts b/src/app/community-list-page/community-list-datasource.ts index e2a2bb748f4..95acc9dd862 100644 --- a/src/app/community-list-page/community-list-datasource.ts +++ b/src/app/community-list-page/community-list-datasource.ts @@ -1,10 +1,18 @@ -import { hasValue } from '../shared/empty.util'; -import { CommunityListService} from './community-list-service'; -import { CollectionViewer, DataSource } from '@angular/cdk/collections'; -import { BehaviorSubject, Observable, Subscription } from 'rxjs'; +import { + CollectionViewer, + DataSource, +} from '@angular/cdk/collections'; +import { + BehaviorSubject, + Observable, + Subscription, +} from 'rxjs'; import { finalize } from 'rxjs/operators'; -import { FlatNode } from './flat-node.model'; + import { FindListOptions } from '../core/data/find-list-options.model'; +import { hasValue } from '../shared/empty.util'; +import { CommunityListService } from './community-list-service'; +import { FlatNode } from './flat-node.model'; /** * DataSource object needed by a CDK Tree to render its nodes. diff --git a/src/app/community-list-page/community-list-page-routes.ts b/src/app/community-list-page/community-list-page-routes.ts new file mode 100644 index 00000000000..9990efb4377 --- /dev/null +++ b/src/app/community-list-page/community-list-page-routes.ts @@ -0,0 +1,19 @@ +import { Route } from '@angular/router'; + +import { i18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver'; +import { ThemedCommunityListPageComponent } from './themed-community-list-page.component'; + +/** + * RouterModule to help navigate to the page with the community list tree + */ +export const ROUTES: Route[] = [ + { + path: '', + component: ThemedCommunityListPageComponent, + pathMatch: 'full', + resolve: { + breadcrumb: i18nBreadcrumbResolver, + }, + data: { title: 'communityList.tabTitle', breadcrumbKey: 'communityList' }, + }, +]; diff --git a/src/app/community-list-page/community-list-page.component.html b/src/app/community-list-page/community-list-page.component.html index 9759f4405da..ca052014780 100644 --- a/src/app/community-list-page/community-list-page.component.html +++ b/src/app/community-list-page/community-list-page.component.html @@ -1,4 +1,4 @@
-

{{ 'communityList.title' | translate }}

- +

{{ 'communityList.title' | translate }}

+
diff --git a/src/app/community-list-page/community-list-page.component.spec.ts b/src/app/community-list-page/community-list-page.component.spec.ts index 080a0a9e18d..8afcf4466fc 100644 --- a/src/app/community-list-page/community-list-page.component.spec.ts +++ b/src/app/community-list-page/community-list-page.component.spec.ts @@ -1,9 +1,20 @@ -import { ComponentFixture, inject, TestBed, waitForAsync } from '@angular/core/testing'; +import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; +import { + ComponentFixture, + inject, + TestBed, + waitForAsync, +} from '@angular/core/testing'; +import { + TranslateLoader, + TranslateModule, +} from '@ngx-translate/core'; -import { CommunityListPageComponent } from './community-list-page.component'; -import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; +import { getMockThemeService } from '../shared/mocks/theme-service.mock'; import { TranslateLoaderMock } from '../shared/mocks/translate-loader.mock'; -import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; +import { ThemeService } from '../shared/theme-support/theme.service'; +import { CommunityListPageComponent } from './community-list-page.component'; +import { CommunityListService } from './community-list-service'; describe('CommunityListPageComponent', () => { let component: CommunityListPageComponent; @@ -15,13 +26,15 @@ describe('CommunityListPageComponent', () => { TranslateModule.forRoot({ loader: { provide: TranslateLoader, - useClass: TranslateLoaderMock + useClass: TranslateLoaderMock, }, }), + CommunityListPageComponent, ], - declarations: [CommunityListPageComponent], providers: [ CommunityListPageComponent, + { provide: ThemeService, useValue: getMockThemeService() }, + { provide: CommunityListService, useValue: {} }, ], schemas: [CUSTOM_ELEMENTS_SCHEMA], }) diff --git a/src/app/community-list-page/community-list-page.component.ts b/src/app/community-list-page/community-list-page.component.ts index 5ab3cce5de3..ca0db89f53d 100644 --- a/src/app/community-list-page/community-list-page.component.ts +++ b/src/app/community-list-page/community-list-page.component.ts @@ -1,12 +1,17 @@ import { Component } from '@angular/core'; +import { TranslateModule } from '@ngx-translate/core'; + +import { ThemedCommunityListComponent } from './community-list/themed-community-list.component'; /** * Page with title and the community list tree, as described in community-list.component; * navigated to with community-list.page.routing.module */ @Component({ - selector: 'ds-community-list-page', + selector: 'ds-base-community-list-page', templateUrl: './community-list-page.component.html', + standalone: true, + imports: [ThemedCommunityListComponent, TranslateModule], }) export class CommunityListPageComponent { diff --git a/src/app/community-list-page/community-list-page.module.ts b/src/app/community-list-page/community-list-page.module.ts deleted file mode 100644 index 15946b2e89a..00000000000 --- a/src/app/community-list-page/community-list-page.module.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { CommonModule } from '@angular/common'; -import { NgModule } from '@angular/core'; -import { SharedModule } from '../shared/shared.module'; -import { CommunityListPageComponent } from './community-list-page.component'; -import { CommunityListPageRoutingModule } from './community-list-page.routing.module'; -import { CommunityListComponent } from './community-list/community-list.component'; -import { ThemedCommunityListPageComponent } from './themed-community-list-page.component'; -import { ThemedCommunityListComponent } from './community-list/themed-community-list.component'; -import { CdkTreeModule } from '@angular/cdk/tree'; - - -const DECLARATIONS = [ - CommunityListPageComponent, - CommunityListComponent, - ThemedCommunityListPageComponent, - ThemedCommunityListComponent -]; -/** - * The page which houses a title and the community list, as described in community-list.component - */ -@NgModule({ - imports: [ - CommonModule, - SharedModule, - CommunityListPageRoutingModule, - CdkTreeModule, - ], - declarations: [ - ...DECLARATIONS - ], - exports: [ - ...DECLARATIONS, - CdkTreeModule, - ], -}) -export class CommunityListPageModule { - -} diff --git a/src/app/community-list-page/community-list-page.routing.module.ts b/src/app/community-list-page/community-list-page.routing.module.ts deleted file mode 100644 index 1754b1b7cf3..00000000000 --- a/src/app/community-list-page/community-list-page.routing.module.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { NgModule } from '@angular/core'; -import { RouterModule } from '@angular/router'; -import { CdkTreeModule } from '@angular/cdk/tree'; - -import { CommunityListService } from './community-list-service'; -import { ThemedCommunityListPageComponent } from './themed-community-list-page.component'; -import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver'; - -/** - * RouterModule to help navigate to the page with the community list tree - */ -@NgModule({ - imports: [ - RouterModule.forChild([ - { - path: '', - component: ThemedCommunityListPageComponent, - pathMatch: 'full', - resolve: { - breadcrumb: I18nBreadcrumbResolver - }, - data: { title: 'communityList.tabTitle', breadcrumbKey: 'communityList' } - } - ]), - CdkTreeModule, - ], - providers: [CommunityListService] -}) -export class CommunityListPageRoutingModule { -} diff --git a/src/app/community-list-page/community-list-service.spec.ts b/src/app/community-list-page/community-list-service.spec.ts index 410dd9f8046..28d3cfe1a95 100644 --- a/src/app/community-list-page/community-list-service.spec.ts +++ b/src/app/community-list-page/community-list-service.spec.ts @@ -1,22 +1,35 @@ -import { inject, TestBed } from '@angular/core/testing'; +import { + inject, + TestBed, +} from '@angular/core/testing'; import { Store } from '@ngrx/store'; import { of as observableOf } from 'rxjs'; import { take } from 'rxjs/operators'; +import { APP_CONFIG } from 'src/config/app-config.interface'; +import { environment } from 'src/environments/environment.test'; + import { AppState } from '../app.reducer'; -import { SortDirection, SortOptions } from '../core/cache/models/sort-options.model'; -import { buildPaginatedList } from '../core/data/paginated-list.model'; -import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils'; -import { StoreMock } from '../shared/testing/store.mock'; -import { CommunityListService, toFlatNode } from './community-list-service'; +import { + SortDirection, + SortOptions, +} from '../core/cache/models/sort-options.model'; import { CollectionDataService } from '../core/data/collection-data.service'; import { CommunityDataService } from '../core/data/community-data.service'; -import { Community } from '../core/shared/community.model'; +import { FindListOptions } from '../core/data/find-list-options.model'; +import { buildPaginatedList } from '../core/data/paginated-list.model'; import { Collection } from '../core/shared/collection.model'; +import { Community } from '../core/shared/community.model'; import { PageInfo } from '../core/shared/page-info.model'; +import { + createFailedRemoteDataObject$, + createSuccessfulRemoteDataObject$, +} from '../shared/remote-data.utils'; +import { StoreMock } from '../shared/testing/store.mock'; +import { + CommunityListService, + toFlatNode, +} from './community-list-service'; import { FlatNode } from './flat-node.model'; -import { FindListOptions } from '../core/data/find-list-options.model'; -import { APP_CONFIG } from 'src/config/app-config.interface'; -import { environment } from 'src/environments/environment.test'; describe('CommunityListService', () => { let store: StoreMock; @@ -38,34 +51,34 @@ describe('CommunityListService', () => { id: 'ce64f48e-2c9b-411a-ac36-ee429c0e6a88', uuid: 'ce64f48e-2c9b-411a-ac36-ee429c0e6a88', }), - Object.assign(new Community(), { - id: '59ee713b-ee53-4220-8c3f-9860dc84fe33', - uuid: '59ee713b-ee53-4220-8c3f-9860dc84fe33', - }) + Object.assign(new Community(), { + id: '59ee713b-ee53-4220-8c3f-9860dc84fe33', + uuid: '59ee713b-ee53-4220-8c3f-9860dc84fe33', + }), ]; mockCollectionsPage1 = [ Object.assign(new Collection(), { id: 'e9dbf393-7127-415f-8919-55be34a6e9ed', uuid: 'e9dbf393-7127-415f-8919-55be34a6e9ed', - name: 'Collection 1' + name: 'Collection 1', }), Object.assign(new Collection(), { id: '59da2ff0-9bf4-45bf-88be-e35abd33f304', uuid: '59da2ff0-9bf4-45bf-88be-e35abd33f304', - name: 'Collection 2' - }) + name: 'Collection 2', + }), ]; mockCollectionsPage2 = [ Object.assign(new Collection(), { id: 'a5159760-f362-4659-9e81-e3253ad91ede', uuid: 'a5159760-f362-4659-9e81-e3253ad91ede', - name: 'Collection 3' + name: 'Collection 3', }), Object.assign(new Collection(), { id: 'a392e16b-fcf2-400a-9a88-53ef7ecbdcd3', uuid: 'a392e16b-fcf2-400a-9a88-53ef7ecbdcd3', - name: 'Collection 4' - }) + name: 'Collection 4', + }), ]; mockListOfTopCommunitiesPage1 = [ Object.assign(new Community(), { @@ -164,7 +177,7 @@ describe('CommunityListService', () => { } else { return createFailedRemoteDataObject$(); } - } + }, }; collectionDataServiceStub = { findByParent(parentUUID: string, options: FindListOptions = {}) { @@ -189,7 +202,7 @@ describe('CommunityListService', () => { } else { return createFailedRemoteDataObject$(); } - } + }, }; TestBed.configureTestingModule({ providers: [CommunityListService, @@ -217,7 +230,7 @@ describe('CommunityListService', () => { service.loadCommunities({ currentPage: 2, - sort: new SortOptions('dc.title', SortDirection.ASC) + sort: new SortOptions('dc.title', SortDirection.ASC), }, null) .pipe(take(1)) .subscribe((value) => { @@ -246,7 +259,7 @@ describe('CommunityListService', () => { beforeEach((done) => { service.loadCommunities({ currentPage: 1, - sort: new SortOptions('dc.title', SortDirection.ASC) + sort: new SortOptions('dc.title', SortDirection.ASC), }, null) .pipe(take(1)) .subscribe((value) => { @@ -279,7 +292,7 @@ describe('CommunityListService', () => { }); service.loadCommunities({ currentPage: 1, - sort: new SortOptions('dc.title', SortDirection.ASC) + sort: new SortOptions('dc.title', SortDirection.ASC), }, expandedNodes) .pipe(take(1)) .subscribe((value) => { @@ -307,7 +320,7 @@ describe('CommunityListService', () => { const expandedNodes = [communityFlatNode]; service.loadCommunities({ currentPage: 1, - sort: new SortOptions('dc.title', SortDirection.ASC) + sort: new SortOptions('dc.title', SortDirection.ASC), }, expandedNodes) .pipe(take(1)) .subscribe((value) => { @@ -332,7 +345,7 @@ describe('CommunityListService', () => { const expandedNodes = [communityFlatNode]; service.loadCommunities({ currentPage: 1, - sort: new SortOptions('dc.title', SortDirection.ASC) + sort: new SortOptions('dc.title', SortDirection.ASC), }, expandedNodes) .pipe(take(1)) .subscribe((value) => { @@ -429,8 +442,8 @@ describe('CommunityListService', () => { collections: createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), [])), metadata: { 'dc.description': [{ language: 'en_US', value: 'no subcoms, 2 coll' }], - 'dc.title': [{ language: 'en_US', value: 'Community 2' }] - } + 'dc.title': [{ language: 'en_US', value: 'Community 2' }], + }, }); let flatNodeList; describe('should return list containing only flatnode corresponding to that community', () => { @@ -461,8 +474,8 @@ describe('CommunityListService', () => { collections: createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), [])), metadata: { 'dc.description': [{ language: 'en_US', value: '2 subcoms, no coll' }], - 'dc.title': [{ language: 'en_US', value: 'Community 1' }] - } + 'dc.title': [{ language: 'en_US', value: 'Community 1' }], + }, }); let flatNodeList; describe('should return list containing only flatnode corresponding to that community', () => { @@ -495,8 +508,8 @@ describe('CommunityListService', () => { collections: createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), [])), metadata: { 'dc.description': [{ language: 'en_US', value: '2 subcoms, no coll' }], - 'dc.title': [{ language: 'en_US', value: 'Community 1' }] - } + 'dc.title': [{ language: 'en_US', value: 'Community 1' }], + }, }); let flatNodeList; beforeEach((done) => { @@ -540,8 +553,8 @@ describe('CommunityListService', () => { collections: createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), [...mockCollectionsPage1, ...mockCollectionsPage2])), metadata: { 'dc.description': [{ language: 'en_US', value: '2 subcoms, no coll' }], - 'dc.title': [{ language: 'en_US', value: 'Community 1' }] - } + 'dc.title': [{ language: 'en_US', value: 'Community 1' }], + }, }); const communityFlatNode = toFlatNode(communityWithCollections, observableOf(true), 0, true, null); communityFlatNode.currentCollectionPage = 2; @@ -591,8 +604,8 @@ describe('CommunityListService', () => { collections: createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), [])), metadata: { 'dc.description': [{ language: 'en_US', value: '2 subcoms, no coll' }], - 'dc.title': [{ language: 'en_US', value: 'Community 1' }] - } + 'dc.title': [{ language: 'en_US', value: 'Community 1' }], + }, }); service.getIsExpandable(communityWithSubcoms).pipe(take(1)).subscribe((result) => { expect(result).toEqual(true); @@ -607,8 +620,8 @@ describe('CommunityListService', () => { collections: createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), mockCollectionsPage1)), metadata: { 'dc.description': [{ language: 'en_US', value: 'no subcoms, 2 coll' }], - 'dc.title': [{ language: 'en_US', value: 'Community 2' }] - } + 'dc.title': [{ language: 'en_US', value: 'Community 2' }], + }, }); service.getIsExpandable(communityWithCollections).pipe(take(1)).subscribe((result) => { expect(result).toEqual(true); @@ -625,8 +638,8 @@ describe('CommunityListService', () => { collections: createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), [])), metadata: { 'dc.description': [{ language: 'en_US', value: 'no subcoms, no coll' }], - 'dc.title': [{ language: 'en_US', value: 'Community 3' }] - } + 'dc.title': [{ language: 'en_US', value: 'Community 3' }], + }, }); service.getIsExpandable(communityWithNoSubcomsOrColls).pipe(take(1)).subscribe((result) => { expect(result).toEqual(false); diff --git a/src/app/community-list-page/community-list-service.ts b/src/app/community-list-page/community-list-service.ts index 99e9dbeb0de..2878d899ebc 100644 --- a/src/app/community-list-page/community-list-service.ts +++ b/src/app/community-list-page/community-list-service.ts @@ -1,31 +1,57 @@ /* eslint-disable max-classes-per-file */ -import { Inject, Injectable } from '@angular/core'; -import { createSelector, Store } from '@ngrx/store'; - -import { combineLatest as observableCombineLatest, Observable, of as observableOf } from 'rxjs'; -import { filter, map, switchMap } from 'rxjs/operators'; +import { + Inject, + Injectable, +} from '@angular/core'; +import { + createSelector, + Store, +} from '@ngrx/store'; +import { + combineLatest as observableCombineLatest, + Observable, + of as observableOf, +} from 'rxjs'; +import { + filter, + map, + switchMap, +} from 'rxjs/operators'; +import { + APP_CONFIG, + AppConfig, +} from 'src/config/app-config.interface'; +import { v4 as uuidv4 } from 'uuid'; import { AppState } from '../app.reducer'; +import { getCollectionPageRoute } from '../collection-page/collection-page-routing-paths'; +import { getCommunityPageRoute } from '../community-page/community-page-routing-paths'; +import { CollectionDataService } from '../core/data/collection-data.service'; import { CommunityDataService } from '../core/data/community-data.service'; -import { Community } from '../core/shared/community.model'; +import { FindListOptions } from '../core/data/find-list-options.model'; +import { + buildPaginatedList, + PaginatedList, +} from '../core/data/paginated-list.model'; +import { RemoteData } from '../core/data/remote-data'; import { Collection } from '../core/shared/collection.model'; +import { Community } from '../core/shared/community.model'; +import { + getFirstCompletedRemoteData, + getFirstSucceededRemoteData, +} from '../core/shared/operators'; import { PageInfo } from '../core/shared/page-info.model'; -import { hasValue, isNotEmpty } from '../shared/empty.util'; -import { RemoteData } from '../core/data/remote-data'; -import { buildPaginatedList, PaginatedList } from '../core/data/paginated-list.model'; -import { CollectionDataService } from '../core/data/collection-data.service'; +import { + hasValue, + isNotEmpty, +} from '../shared/empty.util'; +import { followLink } from '../shared/utils/follow-link-config.model'; import { CommunityListSaveAction } from './community-list.actions'; import { CommunityListState } from './community-list.reducer'; -import { getCommunityPageRoute } from '../community-page/community-page-routing-paths'; -import { getCollectionPageRoute } from '../collection-page/collection-page-routing-paths'; -import { getFirstCompletedRemoteData, getFirstSucceededRemoteData } from '../core/shared/operators'; -import { followLink } from '../shared/utils/follow-link-config.model'; import { FlatNode } from './flat-node.model'; import { ShowMoreFlatNode } from './show-more-flat-node.model'; -import { FindListOptions } from '../core/data/find-list-options.model'; -import { AppConfig, APP_CONFIG } from 'src/config/app-config.interface'; -// Helper method to combine an flatten an array of observables of flatNode arrays +// Helper method to combine and flatten an array of observables of flatNode arrays export const combineAndFlatten = (obsList: Observable[]): Observable => observableCombineLatest([...obsList]).pipe( map((matrix: any[][]) => [].concat(...matrix)), @@ -45,7 +71,7 @@ export const toFlatNode = ( isExpandable: Observable, level: number, isExpanded: boolean, - parent?: FlatNode + parent?: FlatNode, ): FlatNode => ({ isExpandable$: isExpandable, name: c.name, @@ -64,7 +90,7 @@ export const toFlatNode = ( export const showMoreFlatNode = ( id: string, level: number, - parent: FlatNode + parent: FlatNode, ): FlatNode => ({ isExpandable$: observableOf(false), name: 'Show More Flatnode', @@ -85,7 +111,7 @@ const loadingNodeSelector = createSelector(communityListStateSelector, (communit * Service class for the community list, responsible for the creating of the flat list used by communityList dataSource * and connection to the store to retrieve and save the state of the community list */ -@Injectable() +@Injectable({ providedIn: 'root' }) export class CommunityListService { private pageSize: number; @@ -94,13 +120,13 @@ export class CommunityListService { @Inject(APP_CONFIG) protected appConfig: AppConfig, private communityDataService: CommunityDataService, private collectionDataService: CollectionDataService, - private store: Store + private store: Store, ) { this.pageSize = appConfig.communityList.pageSize; } private configOnePage: FindListOptions = Object.assign(new FindListOptions(), { - elementsPerPage: 1 + elementsPerPage: 1, }); saveCommunityListStateToStore(expandedNodes: FlatNode[], loadingNode: FlatNode): void { @@ -137,7 +163,7 @@ export class CommunityListService { newPageInfo = Object.assign({}, coms[0].pageInfo, { currentPage }); } return buildPaginatedList(newPageInfo, newPage); - }) + }), ); return topComs$.pipe( switchMap((topComs: PaginatedList) => this.transformListOfCommunities(topComs, 0, null, expandedNodes)), @@ -150,15 +176,15 @@ export class CommunityListService { */ private getTopCommunities(options: FindListOptions): Observable> { return this.communityDataService.findTop({ - currentPage: options.currentPage, - elementsPerPage: this.pageSize, - sort: { - field: options.sort.field, - direction: options.sort.direction - } + currentPage: options.currentPage, + elementsPerPage: this.pageSize, + sort: { + field: options.sort.field, + direction: options.sort.direction, }, - followLink('subcommunities', { findListOptions: this.configOnePage }), - followLink('collections', { findListOptions: this.configOnePage })) + }, + followLink('subcommunities', { findListOptions: this.configOnePage }), + followLink('collections', { findListOptions: this.configOnePage })) .pipe( getFirstSucceededRemoteData(), map((results) => results.payload), @@ -173,9 +199,9 @@ export class CommunityListService { * @param expandedNodes List of expanded nodes; if a node is not expanded its subcommunities and collections need not be added to the list */ public transformListOfCommunities(listOfPaginatedCommunities: PaginatedList, - level: number, - parent: FlatNode, - expandedNodes: FlatNode[]): Observable { + level: number, + parent: FlatNode, + expandedNodes: FlatNode[]): Observable { if (isNotEmpty(listOfPaginatedCommunities.page)) { let currentPage = listOfPaginatedCommunities.currentPage; if (isNotEmpty(parent)) { @@ -186,7 +212,7 @@ export class CommunityListService { return this.transformCommunity(community, level, parent, expandedNodes); }); if (currentPage < listOfPaginatedCommunities.totalPages && currentPage === listOfPaginatedCommunities.currentPage) { - obsList = [...obsList, observableOf([showMoreFlatNode('community', level, parent)])]; + obsList = [...obsList, observableOf([showMoreFlatNode(`community-${uuidv4()}`, level, parent)])]; } return combineAndFlatten(obsList); @@ -199,7 +225,7 @@ export class CommunityListService { * Transforms a community in a list of FlatNodes containing firstly a flatnode of the community itself, * followed by flatNodes of its possible subcommunities and collection * It gets called recursively for each subcommunity to add its subcommunities and collections to the list - * Number of subcommunities and collections added, is dependant on the current page the parent is at for respectively subcommunities and collections. + * Number of subcommunities and collections added, is dependent on the current page the parent is at for respectively subcommunities and collections. * @param community Community being transformed * @param level Depth of the community in the list, subcommunities and collections go one level deeper * @param parent Flatnode of the parent community @@ -222,11 +248,11 @@ export class CommunityListService { let subcoms = []; for (let i = 1; i <= currentCommunityPage; i++) { const nextSetOfSubcommunitiesPage = this.communityDataService.findByParent(community.uuid, { - elementsPerPage: this.pageSize, - currentPage: i - }, - followLink('subcommunities', { findListOptions: this.configOnePage }), - followLink('collections', { findListOptions: this.configOnePage })) + elementsPerPage: this.pageSize, + currentPage: i, + }, + followLink('subcommunities', { findListOptions: this.configOnePage }), + followLink('collections', { findListOptions: this.configOnePage })) .pipe( getFirstCompletedRemoteData(), switchMap((rd: RemoteData>) => { @@ -235,7 +261,7 @@ export class CommunityListService { } else { return observableOf([]); } - }) + }), ); subcoms = [...subcoms, nextSetOfSubcommunitiesPage]; @@ -248,7 +274,7 @@ export class CommunityListService { for (let i = 1; i <= currentCollectionPage; i++) { const nextSetOfCollectionsPage = this.collectionDataService.findByParent(community.uuid, { elementsPerPage: this.pageSize, - currentPage: i + currentPage: i, }) .pipe( getFirstCompletedRemoteData(), @@ -257,7 +283,7 @@ export class CommunityListService { let nodes = rd.payload.page .map((collection: Collection) => toFlatNode(collection, observableOf(false), level + 1, false, communityFlatNode)); if (currentCollectionPage < rd.payload.totalPages && currentCollectionPage === rd.payload.currentPage) { - nodes = [...nodes, showMoreFlatNode('collection', level + 1, communityFlatNode)]; + nodes = [...nodes, showMoreFlatNode(`collection-${uuidv4()}`, level + 1, communityFlatNode)]; } return nodes; } else { @@ -275,14 +301,12 @@ export class CommunityListService { /** * Checks if a community has subcommunities or collections by querying the respective services with a pageSize = 0 - * Returns an observable that combines the result.payload.totalElements fo the observables that the + * Returns an observable that combines the result.payload.totalElements of the observables that the * respective services return when queried * @param community Community being checked whether it is expandable (if it has subcommunities or collections) */ public getIsExpandable(community: Community): Observable { - let hasSubcoms$: Observable; - let hasColls$: Observable; - hasSubcoms$ = this.communityDataService.findByParent(community.uuid, this.configOnePage) + const hasSubcoms$ = this.communityDataService.findByParent(community.uuid, this.configOnePage) .pipe( map((rd: RemoteData>) => { if (hasValue(rd) && hasValue(rd.payload)) { @@ -293,7 +317,7 @@ export class CommunityListService { }), ); - hasColls$ = this.collectionDataService.findByParent(community.uuid, this.configOnePage) + const hasColls$ = this.collectionDataService.findByParent(community.uuid, this.configOnePage) .pipe( map((rd: RemoteData>) => { if (hasValue(rd) && hasValue(rd.payload)) { @@ -304,12 +328,9 @@ export class CommunityListService { }), ); - let hasChildren$: Observable; - hasChildren$ = observableCombineLatest(hasSubcoms$, hasColls$).pipe( - map(([hasSubcoms, hasColls]: [boolean, boolean]) => hasSubcoms || hasColls) + return observableCombineLatest(hasSubcoms$, hasColls$).pipe( + map(([hasSubcoms, hasColls]: [boolean, boolean]) => hasSubcoms || hasColls), ); - - return hasChildren$; } } diff --git a/src/app/community-list-page/community-list.actions.ts b/src/app/community-list-page/community-list.actions.ts index 8e8d6d87cf2..47c72af9f78 100644 --- a/src/app/community-list-page/community-list.actions.ts +++ b/src/app/community-list-page/community-list.actions.ts @@ -1,4 +1,5 @@ import { Action } from '@ngrx/store'; + import { type } from '../shared/ngrx/type'; import { FlatNode } from './flat-node.model'; @@ -7,7 +8,7 @@ import { FlatNode } from './flat-node.model'; */ export const CommunityListActionTypes = { - SAVE: type('dspace/community-list-page/SAVE') + SAVE: type('dspace/community-list-page/SAVE'), }; /** diff --git a/src/app/community-list-page/community-list.reducer.spec.ts b/src/app/community-list-page/community-list.reducer.spec.ts index 0d0f5c75807..abbd16d4cdc 100644 --- a/src/app/community-list-page/community-list.reducer.spec.ts +++ b/src/app/community-list-page/community-list.reducer.spec.ts @@ -1,11 +1,12 @@ import { of as observableOf } from 'rxjs'; + import { buildPaginatedList } from '../core/data/paginated-list.model'; import { Community } from '../core/shared/community.model'; import { PageInfo } from '../core/shared/page-info.model'; import { createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils'; -import { toFlatNode } from './community-list-service'; import { CommunityListSaveAction } from './community-list.actions'; import { CommunityListReducer } from './community-list.reducer'; +import { toFlatNode } from './community-list-service'; describe('communityListReducer', () => { const mockSubcommunities1Page1 = [Object.assign(new Community(), { @@ -20,7 +21,7 @@ describe('communityListReducer', () => { subcommunities: createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), mockSubcommunities1Page1)), collections: createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), [])), name: 'community1', - }), observableOf(true), 0, false, null + }), observableOf(true), 0, false, null, ); it ('should set init state of the expandedNodes and loadingNode', () => { diff --git a/src/app/community-list-page/community-list.reducer.ts b/src/app/community-list-page/community-list.reducer.ts index 99c8350cf4e..7afcabf067d 100644 --- a/src/app/community-list-page/community-list.reducer.ts +++ b/src/app/community-list-page/community-list.reducer.ts @@ -1,4 +1,8 @@ -import { CommunityListActions, CommunityListActionTypes, CommunityListSaveAction } from './community-list.actions'; +import { + CommunityListActions, + CommunityListActionTypes, + CommunityListSaveAction, +} from './community-list.actions'; import { FlatNode } from './flat-node.model'; /** diff --git a/src/app/community-list-page/community-list/community-list.component.html b/src/app/community-list-page/community-list/community-list.component.html index d6fd77e79bb..82238ddd0d1 100644 --- a/src/app/community-list-page/community-list/community-list.component.html +++ b/src/app/community-list-page/community-list/community-list.component.html @@ -1,18 +1,18 @@ - - + +
- -
- - +
@@ -24,32 +24,32 @@
- + +
-
- - {{ dsoNameService.getName(node.payload) }} - + + {{ dsoNameService.getName(node.payload) }}   - {{node.payload.archivedItemsCount}} -
+ {{node.payload.archivedItemsCount}} +
- + {{node.payload.shortDescription}} @@ -58,33 +58,29 @@
- - + +
- + {{node.payload.shortDescription}} diff --git a/src/app/community-list-page/community-list/community-list.component.scss b/src/app/community-list-page/community-list/community-list.component.scss new file mode 100644 index 00000000000..2e33380a29f --- /dev/null +++ b/src/app/community-list-page/community-list/community-list.component.scss @@ -0,0 +1,4 @@ +::ng-deep .fa-chevron-right::before { + display: block; + width: 16px; +} diff --git a/src/app/community-list-page/community-list/community-list.component.spec.ts b/src/app/community-list-page/community-list/community-list.component.spec.ts index ce6b27dbeb2..f997f5db9c3 100644 --- a/src/app/community-list-page/community-list/community-list.component.spec.ts +++ b/src/app/community-list-page/community-list/community-list.component.spec.ts @@ -1,22 +1,46 @@ -import { ComponentFixture, fakeAsync, inject, TestBed, tick, waitForAsync } from '@angular/core/testing'; - -import { CommunityListComponent } from './community-list.component'; -import { CommunityListService, showMoreFlatNode, toFlatNode } from '../community-list-service'; import { CdkTreeModule } from '@angular/cdk/tree'; -import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; -import { TranslateLoaderMock } from '../../shared/mocks/translate-loader.mock'; -import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; +import { + CUSTOM_ELEMENTS_SCHEMA, + DebugElement, +} from '@angular/core'; +import { + ComponentFixture, + fakeAsync, + inject, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { RouterLinkWithHref } from '@angular/router'; import { RouterTestingModule } from '@angular/router/testing'; -import { Community } from '../../core/shared/community.model'; -import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; +import { + TranslateLoader, + TranslateModule, +} from '@ngx-translate/core'; +import { of as observableOf } from 'rxjs'; +import { v4 as uuidv4 } from 'uuid'; + import { buildPaginatedList } from '../../core/data/paginated-list.model'; -import { PageInfo } from '../../core/shared/page-info.model'; import { Collection } from '../../core/shared/collection.model'; -import { of as observableOf } from 'rxjs'; -import { By } from '@angular/platform-browser'; -import { isEmpty, isNotEmpty } from '../../shared/empty.util'; +import { Community } from '../../core/shared/community.model'; +import { PageInfo } from '../../core/shared/page-info.model'; +import { + isEmpty, + isNotEmpty, +} from '../../shared/empty.util'; +import { ThemedLoadingComponent } from '../../shared/loading/themed-loading.component'; +import { TranslateLoaderMock } from '../../shared/mocks/translate-loader.mock'; +import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; +import { TruncatableComponent } from '../../shared/truncatable/truncatable.component'; +import { TruncatablePartComponent } from '../../shared/truncatable/truncatable-part/truncatable-part.component'; +import { + CommunityListService, + showMoreFlatNode, + toFlatNode, +} from '../community-list-service'; import { FlatNode } from '../flat-node.model'; -import { RouterLinkWithHref } from '@angular/router'; +import { CommunityListComponent } from './community-list.component'; describe('CommunityListComponent', () => { let component: CommunityListComponent; @@ -27,11 +51,11 @@ describe('CommunityListComponent', () => { uuid: 'ce64f48e-2c9b-411a-ac36-ee429c0e6a88', name: 'subcommunity1', }), - Object.assign(new Community(), { - id: '59ee713b-ee53-4220-8c3f-9860dc84fe33', - uuid: '59ee713b-ee53-4220-8c3f-9860dc84fe33', - name: 'subcommunity2', - }) + Object.assign(new Community(), { + id: '59ee713b-ee53-4220-8c3f-9860dc84fe33', + uuid: '59ee713b-ee53-4220-8c3f-9860dc84fe33', + name: 'subcommunity2', + }), ]; const mockCollectionsPage1 = [ Object.assign(new Collection(), { @@ -43,7 +67,7 @@ describe('CommunityListComponent', () => { id: '59da2ff0-9bf4-45bf-88be-e35abd33f304', uuid: '59da2ff0-9bf4-45bf-88be-e35abd33f304', name: 'collection2', - }) + }), ]; const mockCollectionsPage2 = [ Object.assign(new Collection(), { @@ -55,7 +79,7 @@ describe('CommunityListComponent', () => { id: 'a392e16b-fcf2-400a-9a88-53ef7ecbdcd3', uuid: 'a392e16b-fcf2-400a-9a88-53ef7ecbdcd3', name: 'collection4', - }) + }), ]; const mockTopCommunitiesWithChildrenArrays = [ @@ -86,7 +110,7 @@ describe('CommunityListComponent', () => { subcommunities: createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), mockSubcommunities1Page1)), collections: createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), [])), name: 'community1', - }), observableOf(true), 0, false, null + }), observableOf(true), 0, false, null, ), toFlatNode( Object.assign(new Community(), { @@ -95,7 +119,7 @@ describe('CommunityListComponent', () => { subcommunities: createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), [])), collections: createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), [...mockCollectionsPage1, ...mockCollectionsPage2])), name: 'community2', - }), observableOf(true), 0, false, null + }), observableOf(true), 0, false, null, ), toFlatNode( Object.assign(new Community(), { @@ -104,7 +128,7 @@ describe('CommunityListComponent', () => { subcommunities: createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), [])), collections: createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), [])), name: 'community3', - }), observableOf(false), 0, false, null + }), observableOf(false), 0, false, null, ), ]; let communityListServiceStub; @@ -138,7 +162,7 @@ describe('CommunityListComponent', () => { } if (expandedNodes === null || isEmpty(expandedNodes)) { if (showMoreTopComNode) { - return observableOf([...mockTopFlatnodesUnexpanded.slice(0, endPageIndex), showMoreFlatNode('community', 0, null)]); + return observableOf([...mockTopFlatnodesUnexpanded.slice(0, endPageIndex), showMoreFlatNode(`community-${uuidv4()}`, 0, null)]); } else { return observableOf(mockTopFlatnodesUnexpanded.slice(0, endPageIndex)); } @@ -165,42 +189,51 @@ describe('CommunityListComponent', () => { const endSubComIndex = this.pageSize * expandedParent.currentCommunityPage; flatnodes = [...flatnodes, ...subComFlatnodes.slice(0, endSubComIndex)]; if (subComFlatnodes.length > endSubComIndex) { - flatnodes = [...flatnodes, showMoreFlatNode('community', topNode.level + 1, expandedParent)]; + flatnodes = [...flatnodes, showMoreFlatNode(`community-${uuidv4()}`, topNode.level + 1, expandedParent)]; } } if (isNotEmpty(collFlatnodes)) { const endColIndex = this.pageSize * expandedParent.currentCollectionPage; flatnodes = [...flatnodes, ...collFlatnodes.slice(0, endColIndex)]; if (collFlatnodes.length > endColIndex) { - flatnodes = [...flatnodes, showMoreFlatNode('collection', topNode.level + 1, expandedParent)]; + flatnodes = [...flatnodes, showMoreFlatNode(`collection-${uuidv4()}`, topNode.level + 1, expandedParent)]; } } } } }); if (showMoreTopComNode) { - flatnodes = [...flatnodes, showMoreFlatNode('community', 0, null)]; + flatnodes = [...flatnodes, showMoreFlatNode(`community-${uuidv4()}`, 0, null)]; } return observableOf(flatnodes); } - } + }, }; TestBed.configureTestingModule({ imports: [ TranslateModule.forRoot({ loader: { provide: TranslateLoader, - useClass: TranslateLoaderMock + useClass: TranslateLoaderMock, }, }), CdkTreeModule, RouterTestingModule, - RouterLinkWithHref], - declarations: [CommunityListComponent], + RouterLinkWithHref, + CommunityListComponent, + ], providers: [CommunityListComponent, - { provide: CommunityListService, useValue: communityListServiceStub },], + { provide: CommunityListService, useValue: communityListServiceStub }], schemas: [CUSTOM_ELEMENTS_SCHEMA], }) + .overrideComponent(CommunityListComponent, { + remove: { + imports: [ + ThemedLoadingComponent, + TruncatableComponent, + TruncatablePartComponent, + ] }, + }) .compileComponents(); })); @@ -242,7 +275,7 @@ describe('CommunityListComponent', () => { const showMoreLink = fixture.debugElement.query(By.css('.show-more-node .btn-outline-primary')); showMoreLink.triggerEventHandler('click', { preventDefault: () => {/**/ - } + }, }); tick(); fixture.detectChanges(); @@ -299,12 +332,14 @@ describe('CommunityListComponent', () => { describe('second top community node is expanded and has more children (collections) than page size of collection', () => { describe('children of second top com are added (page-limited pageSize 2)', () => { - let allNodes; + let allNodes: DebugElement[]; beforeEach(fakeAsync(() => { - const chevronExpand = fixture.debugElement.queryAll(By.css('.expandable-node button')); - const chevronExpandSpan = fixture.debugElement.queryAll(By.css('.expandable-node button span')); - if (chevronExpandSpan[1].nativeElement.classList.contains('fa-chevron-right')) { - chevronExpand[1].nativeElement.click(); + const toggleButtons: DebugElement[] = fixture.debugElement.queryAll(By.css('.expandable-node button')); + const toggleButtonText: DebugElement = toggleButtons[1].query(By.css('span')); + expect(toggleButtonText).not.toBeNull(); + + if (toggleButtonText.nativeElement.classList.contains('fa-chevron-right')) { + toggleButtons[1].nativeElement.click(); tick(); fixture.detectChanges(); } @@ -314,17 +349,18 @@ describe('CommunityListComponent', () => { allNodes = [...expandableNodesFound, ...childlessNodesFound]; })); it('tree contains 2 (page-limited) top com, 2 (page-limited) coll of 2nd top com, a show more for those page-limited coll and show more for page-limited top com', () => { - mockTopFlatnodesUnexpanded.slice(0, 2).map((topFlatnode: FlatNode) => { - expect(allNodes.find((foundEl) => { - return (foundEl.nativeElement.textContent.trim() === topFlatnode.name); - })).toBeTruthy(); - }); - mockCollectionsPage1.map((coll) => { - expect(allNodes.find((foundEl) => { - return (foundEl.nativeElement.textContent.trim() === coll.name); - })).toBeTruthy(); - }); + const allNodeNames: string[] = allNodes.map((node: DebugElement) => node.nativeElement.innerText.trim()); expect(allNodes.length).toEqual(4); + const flatNodes: string[] = mockTopFlatnodesUnexpanded.slice(0, 2).map((flatNode: FlatNode) => flatNode.name); + for (const flatNode of flatNodes) { + expect(allNodeNames).toContain(flatNode); + } + expect(flatNodes.length).toBe(2); + const page1CollectionNames: string[] = mockCollectionsPage1.map((collection: Collection) => collection.name); + for (const collectionName of page1CollectionNames) { + expect(allNodeNames).toContain(collectionName); + } + expect(page1CollectionNames.length).toBe(2); const showMoreEl = fixture.debugElement.queryAll(By.css('.show-more-node')); expect(showMoreEl.length).toEqual(2); }); diff --git a/src/app/community-list-page/community-list/community-list.component.ts b/src/app/community-list-page/community-list/community-list.component.ts index 5b2f9308137..5819471d7e5 100644 --- a/src/app/community-list-page/community-list/community-list.component.ts +++ b/src/app/community-list-page/community-list/community-list.component.ts @@ -1,13 +1,34 @@ -import { Component, OnDestroy, OnInit } from '@angular/core'; +import { + CdkTreeModule, + FlatTreeControl, +} from '@angular/cdk/tree'; +import { + AsyncPipe, + NgClass, + NgIf, +} from '@angular/common'; +import { + Component, + OnDestroy, + OnInit, +} from '@angular/core'; +import { RouterLink } from '@angular/router'; +import { TranslateModule } from '@ngx-translate/core'; import { take } from 'rxjs/operators'; -import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model'; -import { CommunityListService} from '../community-list-service'; -import { CommunityListDatasource } from '../community-list-datasource'; -import { FlatTreeControl } from '@angular/cdk/tree'; + +import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; +import { + SortDirection, + SortOptions, +} from '../../core/cache/models/sort-options.model'; +import { FindListOptions } from '../../core/data/find-list-options.model'; import { isEmpty } from '../../shared/empty.util'; +import { ThemedLoadingComponent } from '../../shared/loading/themed-loading.component'; +import { TruncatableComponent } from '../../shared/truncatable/truncatable.component'; +import { TruncatablePartComponent } from '../../shared/truncatable/truncatable-part/truncatable-part.component'; +import { CommunityListDatasource } from '../community-list-datasource'; +import { CommunityListService } from '../community-list-service'; import { FlatNode } from '../flat-node.model'; -import { FindListOptions } from '../../core/data/find-list-options.model'; -import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; /** * A tree-structured list of nodes representing the communities, their subCommunities and collections. @@ -17,8 +38,11 @@ import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; * Which nodes were expanded is kept in the store, so this persists across pages. */ @Component({ - selector: 'ds-community-list', + selector: 'ds-base-community-list', templateUrl: './community-list.component.html', + styleUrls: ['./community-list.component.scss'], + standalone: true, + imports: [NgIf, ThemedLoadingComponent, CdkTreeModule, NgClass, RouterLink, TruncatableComponent, TruncatablePartComponent, AsyncPipe, TranslateModule], }) export class CommunityListComponent implements OnInit, OnDestroy { @@ -26,12 +50,11 @@ export class CommunityListComponent implements OnInit, OnDestroy { public loadingNode: FlatNode; treeControl = new FlatTreeControl( - (node: FlatNode) => node.level, (node: FlatNode) => true + (node: FlatNode) => node.level, (node: FlatNode) => true, ); - dataSource: CommunityListDatasource; - paginationConfig: FindListOptions; + trackBy = (index, node: FlatNode) => node.id; constructor( protected communityListService: CommunityListService, @@ -58,24 +81,34 @@ export class CommunityListComponent implements OnInit, OnDestroy { this.communityListService.saveCommunityListStateToStore(this.expandedNodes, this.loadingNode); } - // whether or not this node has children (subcommunities or collections) + /** + * Whether this node has children (subcommunities or collections) + * @param _ + * @param node + */ hasChild(_: number, node: FlatNode) { return node.isExpandable$; } - // whether or not it is a show more node (contains no data, but is indication that there are more topcoms, subcoms or collections + /** + * Whether this is a show more node that contains no data, but indicates that there is + * one or more community or collection. + * @param _ + * @param node + */ isShowMore(_: number, node: FlatNode) { return node.isShowMoreNode; } /** - * Toggles the expanded variable of a node, adds it to the expanded nodes list and reloads the tree so this node is expanded + * Toggles the expanded variable of a node, adds it to the expanded nodes list and reloads the tree + * so this node is expanded * @param node Node we want to expand */ toggleExpanded(node: FlatNode) { this.loadingNode = node; if (node.isExpanded) { - this.expandedNodes = this.expandedNodes.filter((node2) => node2.name !== node.name); + this.expandedNodes = this.expandedNodes.filter((node2) => node2.id !== node.id); node.isExpanded = false; } else { this.expandedNodes.push(node); @@ -92,26 +125,28 @@ export class CommunityListComponent implements OnInit, OnDestroy { /** * Makes sure the next page of a node is added to the tree (top community, sub community of collection) - * > Finds its parent (if not top community) and increases its corresponding collection/subcommunity currentPage - * > Reloads tree with new page added to corresponding top community lis, sub community list or collection list - * @param node The show more node indicating whether it's an increase in top communities, sub communities or collections + * > Finds its parent (if not top community) and increases its corresponding collection/subcommunity + * currentPage + * > Reloads tree with new page added to corresponding top community lis, sub community list or + * collection list + * @param node The show more node indicating whether it's an increase in top communities, sub communities + * or collections */ getNextPage(node: FlatNode): void { this.loadingNode = node; if (node.parent != null) { - if (node.id === 'collection') { + if (node.id.startsWith('collection')) { const parentNodeInExpandedNodes = this.expandedNodes.find((node2: FlatNode) => node.parent.id === node2.id); parentNodeInExpandedNodes.currentCollectionPage++; } - if (node.id === 'community') { + if (node.id.startsWith('community')) { const parentNodeInExpandedNodes = this.expandedNodes.find((node2: FlatNode) => node.parent.id === node2.id); parentNodeInExpandedNodes.currentCommunityPage++; } - this.dataSource.loadCommunities(this.paginationConfig, this.expandedNodes); } else { this.paginationConfig.currentPage++; - this.dataSource.loadCommunities(this.paginationConfig, this.expandedNodes); } + this.dataSource.loadCommunities(this.paginationConfig, this.expandedNodes); } } diff --git a/src/app/community-list-page/community-list/themed-community-list.component.ts b/src/app/community-list-page/community-list/themed-community-list.component.ts index 4a986e737c1..5340384ed5a 100644 --- a/src/app/community-list-page/community-list/themed-community-list.component.ts +++ b/src/app/community-list-page/community-list/themed-community-list.component.ts @@ -1,12 +1,15 @@ +import { Component } from '@angular/core'; + import { ThemedComponent } from '../../shared/theme-support/themed.component'; import { CommunityListComponent } from './community-list.component'; -import { Component } from '@angular/core'; @Component({ - selector: 'ds-themed-community-list', + selector: 'ds-community-list', styleUrls: [], templateUrl: '../../shared/theme-support/themed.component.html', + standalone: true, + imports: [CommunityListComponent], }) export class ThemedCommunityListComponent extends ThemedComponent { protected getComponentName(): string { diff --git a/src/app/community-list-page/flat-node.model.ts b/src/app/community-list-page/flat-node.model.ts index 0aabbeb4891..125ffc1e597 100644 --- a/src/app/community-list-page/flat-node.model.ts +++ b/src/app/community-list-page/flat-node.model.ts @@ -1,6 +1,7 @@ import { Observable } from 'rxjs'; -import { Community } from '../core/shared/community.model'; + import { Collection } from '../core/shared/collection.model'; +import { Community } from '../core/shared/community.model'; import { ShowMoreFlatNode } from './show-more-flat-node.model'; /** diff --git a/src/app/community-list-page/show-more-flat-node.model.ts b/src/app/community-list-page/show-more-flat-node.model.ts index 801c9e7388a..c7b7162d213 100644 --- a/src/app/community-list-page/show-more-flat-node.model.ts +++ b/src/app/community-list-page/show-more-flat-node.model.ts @@ -1,6 +1,6 @@ /** * The show more links in the community tree are also represented by a flatNode so we know where in - * the tree it should be rendered an who its parent is (needed for the action resulting in clicking this link) + * the tree it should be rendered and who its parent is (needed for the action resulting in clicking this link) */ export class ShowMoreFlatNode { } diff --git a/src/app/community-list-page/themed-community-list-page.component.ts b/src/app/community-list-page/themed-community-list-page.component.ts index 20fa97bedd1..d427b1bec1c 100644 --- a/src/app/community-list-page/themed-community-list-page.component.ts +++ b/src/app/community-list-page/themed-community-list-page.component.ts @@ -1,4 +1,5 @@ import { Component } from '@angular/core'; + import { ThemedComponent } from '../shared/theme-support/themed.component'; import { CommunityListPageComponent } from './community-list-page.component'; @@ -6,9 +7,11 @@ import { CommunityListPageComponent } from './community-list-page.component'; * Themed wrapper for CommunityListPageComponent */ @Component({ - selector: 'ds-themed-community-list-page', + selector: 'ds-community-list-page', styleUrls: [], templateUrl: '../shared/theme-support/themed.component.html', + standalone: true, + imports: [CommunityListPageComponent], }) export class ThemedCommunityListPageComponent extends ThemedComponent { protected getComponentName(): string { diff --git a/src/app/community-page/community-form/community-form.component.ts b/src/app/community-page/community-form/community-form.component.ts index fa4809738d9..d32d9e408f2 100644 --- a/src/app/community-page/community-form/community-form.component.ts +++ b/src/app/community-page/community-form/community-form.component.ts @@ -1,19 +1,39 @@ -import { Component, Input, OnChanges, SimpleChange, SimpleChanges } from '@angular/core'; +import { + AsyncPipe, + NgClass, + NgIf, +} from '@angular/common'; +import { + Component, + Input, + OnChanges, + SimpleChange, + SimpleChanges, +} from '@angular/core'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { DynamicFormControlModel, DynamicFormService, DynamicInputModel, - DynamicTextAreaModel + DynamicTextAreaModel, } from '@ng-dynamic-forms/core'; +import { + TranslateModule, + TranslateService, +} from '@ngx-translate/core'; + +import { environment } from '../../../environments/environment'; +import { AuthService } from '../../core/auth/auth.service'; +import { ObjectCacheService } from '../../core/cache/object-cache.service'; +import { CommunityDataService } from '../../core/data/community-data.service'; +import { RequestService } from '../../core/data/request.service'; import { Community } from '../../core/shared/community.model'; import { ComColFormComponent } from '../../shared/comcol/comcol-forms/comcol-form/comcol-form.component'; -import { TranslateService } from '@ngx-translate/core'; +import { ComcolPageLogoComponent } from '../../shared/comcol/comcol-page-logo/comcol-page-logo.component'; +import { FormComponent } from '../../shared/form/form.component'; import { NotificationsService } from '../../shared/notifications/notifications.service'; -import { CommunityDataService } from '../../core/data/community-data.service'; -import { AuthService } from '../../core/auth/auth.service'; -import { RequestService } from '../../core/data/request.service'; -import { ObjectCacheService } from '../../core/cache/object-cache.service'; -import { environment } from '../../../environments/environment'; +import { UploaderComponent } from '../../shared/upload/uploader/uploader.component'; +import { VarDirective } from '../../shared/utils/var.directive'; /** * Form used for creating and editing communities @@ -21,7 +41,18 @@ import { environment } from '../../../environments/environment'; @Component({ selector: 'ds-community-form', styleUrls: ['../../shared/comcol/comcol-forms/comcol-form/comcol-form.component.scss'], - templateUrl: '../../shared/comcol/comcol-forms/comcol-form/comcol-form.component.html' + templateUrl: '../../shared/comcol/comcol-forms/comcol-form/comcol-form.component.html', + standalone: true, + imports: [ + FormComponent, + TranslateModule, + UploaderComponent, + AsyncPipe, + ComcolPageLogoComponent, + NgIf, + NgClass, + VarDirective, + ], }) export class CommunityFormComponent extends ComColFormComponent implements OnChanges { /** @@ -44,10 +75,10 @@ export class CommunityFormComponent extends ComColFormComponent imple name: 'dc.title', required: true, validators: { - required: null + required: null, }, errorMessages: { - required: 'Please enter a name for this title' + required: 'Please enter a name for this title', }, }), new DynamicTextAreaModel({ @@ -78,14 +109,15 @@ export class CommunityFormComponent extends ComColFormComponent imple protected authService: AuthService, protected dsoService: CommunityDataService, protected requestService: RequestService, - protected objectCache: ObjectCacheService) { - super(formService, translate, notificationsService, authService, requestService, objectCache); + protected objectCache: ObjectCacheService, + protected modalService: NgbModal) { + super(formService, translate, notificationsService, authService, requestService, objectCache, modalService); } ngOnChanges(changes: SimpleChanges) { const dsoChange: SimpleChange = changes.dso; if (this.dso && dsoChange && !dsoChange.isFirstChange()) { - super.ngOnInit(); + super.ngOnInit(); } } } diff --git a/src/app/community-page/community-form/community-form.module.ts b/src/app/community-page/community-form/community-form.module.ts deleted file mode 100644 index 925d218973f..00000000000 --- a/src/app/community-page/community-form/community-form.module.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { NgModule } from '@angular/core'; - -import { CommunityFormComponent } from './community-form.component'; -import { SharedModule } from '../../shared/shared.module'; -import { ComcolModule } from '../../shared/comcol/comcol.module'; -import { FormModule } from '../../shared/form/form.module'; - -@NgModule({ - imports: [ - ComcolModule, - FormModule, - SharedModule - ], - declarations: [ - CommunityFormComponent, - ], - exports: [ - CommunityFormComponent - ] -}) -export class CommunityFormModule { - -} diff --git a/src/app/community-page/community-page-administrator.guard.ts b/src/app/community-page/community-page-administrator.guard.ts index fd7ce5f7bf0..ecbc9b86c0c 100644 --- a/src/app/community-page/community-page-administrator.guard.ts +++ b/src/app/community-page/community-page-administrator.guard.ts @@ -1,31 +1,16 @@ -import { Injectable } from '@angular/core'; -import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router'; -import { Community } from '../core/shared/community.model'; -import { CommunityPageResolver } from './community-page.resolver'; -import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service'; -import { Observable, of as observableOf } from 'rxjs'; -import { DsoPageSingleFeatureGuard } from '../core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard'; +import { CanActivateFn } from '@angular/router'; +import { of as observableOf } from 'rxjs'; + +import { dsoPageSingleFeatureGuard } from '../core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard'; import { FeatureID } from '../core/data/feature-authorization/feature-id'; -import { AuthService } from '../core/auth/auth.service'; +import { communityPageResolver } from './community-page.resolver'; -@Injectable({ - providedIn: 'root' -}) /** * Guard for preventing unauthorized access to certain {@link Community} pages requiring administrator rights + * Check administrator authorization rights */ -export class CommunityPageAdministratorGuard extends DsoPageSingleFeatureGuard { - constructor(protected resolver: CommunityPageResolver, - protected authorizationService: AuthorizationDataService, - protected router: Router, - protected authService: AuthService) { - super(resolver, authorizationService, router, authService); - } - - /** - * Check administrator authorization rights - */ - getFeatureID(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { - return observableOf(FeatureID.AdministratorOf); - } -} +export const communityPageAdministratorGuard: CanActivateFn = + dsoPageSingleFeatureGuard( + () => communityPageResolver, + () => observableOf(FeatureID.AdministratorOf), + ); diff --git a/src/app/community-page/community-page-routes.ts b/src/app/community-page/community-page-routes.ts new file mode 100644 index 00000000000..d9505c53b13 --- /dev/null +++ b/src/app/community-page/community-page-routes.ts @@ -0,0 +1,117 @@ +import { Route } from '@angular/router'; + +import { browseByGuard } from '../browse-by/browse-by-guard'; +import { browseByI18nBreadcrumbResolver } from '../browse-by/browse-by-i18n-breadcrumb.resolver'; +import { authenticatedGuard } from '../core/auth/authenticated.guard'; +import { communityBreadcrumbResolver } from '../core/breadcrumbs/community-breadcrumb.resolver'; +import { i18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver'; +import { ComcolBrowseByComponent } from '../shared/comcol/sections/comcol-browse-by/comcol-browse-by.component'; +import { ComcolSearchSectionComponent } from '../shared/comcol/sections/comcol-search-section/comcol-search-section.component'; +import { dsoEditMenuResolver } from '../shared/dso-page/dso-edit-menu.resolver'; +import { LinkMenuItemModel } from '../shared/menu/menu-item/models/link.model'; +import { MenuItemType } from '../shared/menu/menu-item-type.model'; +import { communityPageResolver } from './community-page.resolver'; +import { communityPageAdministratorGuard } from './community-page-administrator.guard'; +import { + COMMUNITY_CREATE_PATH, + COMMUNITY_EDIT_PATH, +} from './community-page-routing-paths'; +import { CreateCommunityPageComponent } from './create-community-page/create-community-page.component'; +import { createCommunityPageGuard } from './create-community-page/create-community-page.guard'; +import { DeleteCommunityPageComponent } from './delete-community-page/delete-community-page.component'; +import { SubComColSectionComponent } from './sections/sub-com-col-section/sub-com-col-section.component'; +import { ThemedCommunityPageComponent } from './themed-community-page.component'; + +export const ROUTES: Route[] = [ + { + path: COMMUNITY_CREATE_PATH, + children: [ + { + path: '', + component: CreateCommunityPageComponent, + resolve: { + breadcrumb: i18nBreadcrumbResolver, + }, + data: { + breadcrumbKey: 'community.create', + }, + }, + ], + canActivate: [authenticatedGuard, createCommunityPageGuard], + data: { + breadcrumbQueryParam: 'parent', + }, + resolve: { + breadcrumb: communityBreadcrumbResolver, + }, + runGuardsAndResolvers: 'always', + }, + { + path: ':id', + resolve: { + dso: communityPageResolver, + breadcrumb: communityBreadcrumbResolver, + menu: dsoEditMenuResolver, + }, + runGuardsAndResolvers: 'always', + children: [ + { + path: COMMUNITY_EDIT_PATH, + loadChildren: () => import('./edit-community-page/edit-community-page-routes') + .then((m) => m.ROUTES), + canActivate: [communityPageAdministratorGuard], + }, + { + path: 'delete', + pathMatch: 'full', + component: DeleteCommunityPageComponent, + canActivate: [authenticatedGuard], + }, + { + path: '', + component: ThemedCommunityPageComponent, + children: [ + { + path: '', + pathMatch: 'full', + component: ComcolSearchSectionComponent, + }, + { + path: 'subcoms-cols', + pathMatch: 'full', + component: SubComColSectionComponent, + resolve: { + breadcrumb: i18nBreadcrumbResolver, + }, + data: { breadcrumbKey: 'community.subcoms-cols' }, + }, + { + path: 'browse/:id', + pathMatch: 'full', + component: ComcolBrowseByComponent, + canActivate: [browseByGuard], + resolve: { + breadcrumb: browseByI18nBreadcrumbResolver, + }, + data: { breadcrumbKey: 'browse.metadata' }, + }, + ], + }, + ], + data: { + menu: { + public: [{ + id: 'statistics_community_:id', + active: true, + visible: true, + index: 2, + model: { + type: MenuItemType.LINK, + text: 'menu.section.statistics', + link: 'statistics/communities/:id/', + } as LinkMenuItemModel, + }], + }, + }, + }, +]; diff --git a/src/app/community-page/community-page-routing.module.ts b/src/app/community-page/community-page-routing.module.ts deleted file mode 100644 index c37f8832f84..00000000000 --- a/src/app/community-page/community-page-routing.module.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { NgModule } from '@angular/core'; -import { RouterModule } from '@angular/router'; - -import { CommunityPageResolver } from './community-page.resolver'; -import { CreateCommunityPageComponent } from './create-community-page/create-community-page.component'; -import { AuthenticatedGuard } from '../core/auth/authenticated.guard'; -import { CreateCommunityPageGuard } from './create-community-page/create-community-page.guard'; -import { DeleteCommunityPageComponent } from './delete-community-page/delete-community-page.component'; -import { CommunityBreadcrumbResolver } from '../core/breadcrumbs/community-breadcrumb.resolver'; -import { DSOBreadcrumbsService } from '../core/breadcrumbs/dso-breadcrumbs.service'; -import { LinkService } from '../core/cache/builders/link.service'; -import { COMMUNITY_EDIT_PATH, COMMUNITY_CREATE_PATH } from './community-page-routing-paths'; -import { CommunityPageAdministratorGuard } from './community-page-administrator.guard'; -import { LinkMenuItemModel } from '../shared/menu/menu-item/models/link.model'; -import { ThemedCommunityPageComponent } from './themed-community-page.component'; -import { MenuItemType } from '../shared/menu/menu-item-type.model'; -import { DSOEditMenuResolver } from '../shared/dso-page/dso-edit-menu.resolver'; - -@NgModule({ - imports: [ - RouterModule.forChild([ - { - path: COMMUNITY_CREATE_PATH, - component: CreateCommunityPageComponent, - canActivate: [AuthenticatedGuard, CreateCommunityPageGuard] - }, - { - path: ':id', - resolve: { - dso: CommunityPageResolver, - breadcrumb: CommunityBreadcrumbResolver, - menu: DSOEditMenuResolver - }, - runGuardsAndResolvers: 'always', - children: [ - { - path: COMMUNITY_EDIT_PATH, - loadChildren: () => import('./edit-community-page/edit-community-page.module') - .then((m) => m.EditCommunityPageModule), - canActivate: [CommunityPageAdministratorGuard] - }, - { - path: 'delete', - pathMatch: 'full', - component: DeleteCommunityPageComponent, - canActivate: [AuthenticatedGuard], - }, - { - path: '', - component: ThemedCommunityPageComponent, - pathMatch: 'full', - } - ], - data: { - menu: { - public: [{ - id: 'statistics_community_:id', - active: true, - visible: true, - index: 2, - model: { - type: MenuItemType.LINK, - text: 'menu.section.statistics', - link: 'statistics/communities/:id/', - } as LinkMenuItemModel, - }], - }, - }, - }, - ]) - ], - providers: [ - CommunityPageResolver, - CommunityBreadcrumbResolver, - DSOBreadcrumbsService, - LinkService, - CreateCommunityPageGuard, - CommunityPageAdministratorGuard, - ] -}) -export class CommunityPageRoutingModule { - -} diff --git a/src/app/community-page/community-page.component.html b/src/app/community-page/community-page.component.html index 6d5262d9338..a695e2019a3 100644 --- a/src/app/community-page/community-page.component.html +++ b/src/app/community-page/community-page.component.html @@ -7,34 +7,30 @@ - + - - + + + [title]="'community.page.news'"> -
- -
- - + + - - +
-
+
@@ -43,5 +39,5 @@
- +
diff --git a/src/app/community-page/community-page.component.ts b/src/app/community-page/community-page.component.ts index a5bbff3cee7..d04ecbee190 100644 --- a/src/app/community-page/community-page.component.ts +++ b/src/app/community-page/community-page.component.ts @@ -1,32 +1,77 @@ -import { mergeMap, filter, map } from 'rxjs/operators'; -import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; -import { ActivatedRoute, Router } from '@angular/router'; - +import { + AsyncPipe, + NgIf, +} from '@angular/common'; +import { + ChangeDetectionStrategy, + Component, + OnInit, +} from '@angular/core'; +import { + ActivatedRoute, + Router, + RouterModule, + RouterOutlet, +} from '@angular/router'; +import { TranslateModule } from '@ngx-translate/core'; import { Observable } from 'rxjs'; -import { CommunityDataService } from '../core/data/community-data.service'; +import { + filter, + map, + mergeMap, +} from 'rxjs/operators'; + +import { AuthService } from '../core/auth/auth.service'; +import { DSONameService } from '../core/breadcrumbs/dso-name.service'; +import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service'; +import { FeatureID } from '../core/data/feature-authorization/feature-id'; import { RemoteData } from '../core/data/remote-data'; +import { redirectOn4xx } from '../core/shared/authorized.operators'; import { Bitstream } from '../core/shared/bitstream.model'; - import { Community } from '../core/shared/community.model'; - -import { MetadataService } from '../core/metadata/metadata.service'; - +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 { getAllSucceededRemoteDataPayload} from '../core/shared/operators'; -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 { 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 { redirectOn4xx } from '../core/shared/authorized.operators'; -import { DSONameService } from '../core/breadcrumbs/dso-name.service'; +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] + 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 @@ -53,8 +98,6 @@ export class CommunityPageComponent implements OnInit { communityPageRoute$: Observable; constructor( - private communityDataService: CommunityDataService, - private metadata: MetadataService, private route: ActivatedRoute, private router: Router, private authService: AuthService, @@ -67,7 +110,7 @@ export class CommunityPageComponent implements OnInit { ngOnInit(): void { this.communityRD$ = this.route.data.pipe( map((data) => data.dso as RemoteData), - redirectOn4xx(this.router, this.authService) + redirectOn4xx(this.router, this.authService), ); this.logoRD$ = this.communityRD$.pipe( map((rd: RemoteData) => rd.payload), @@ -75,7 +118,7 @@ export class CommunityPageComponent implements OnInit { mergeMap((community: Community) => community.logo)); this.communityPageRoute$ = this.communityRD$.pipe( getAllSucceededRemoteDataPayload(), - map((community) => getCommunityPageRoute(community.id)) + map((community) => getCommunityPageRoute(community.id)), ); this.isCommunityAdmin$ = this.authorizationDataService.isAuthorized(FeatureID.IsCommunityAdmin); } 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 45ffb2a7868..00000000000 --- a/src/app/community-page/community-page.module.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { NgModule } from '@angular/core'; -import { CommonModule } from '@angular/common'; - -import { SharedModule } from '../shared/shared.module'; - -import { CommunityPageComponent } from './community-page.component'; -import { CommunityPageSubCollectionListComponent } from './sub-collection-list/community-page-sub-collection-list.component'; -import { CommunityPageRoutingModule } from './community-page-routing.module'; -import { CommunityPageSubCommunityListComponent } from './sub-community-list/community-page-sub-community-list.component'; -import { CreateCommunityPageComponent } from './create-community-page/create-community-page.component'; -import { DeleteCommunityPageComponent } from './delete-community-page/delete-community-page.component'; -import { StatisticsModule } from '../statistics/statistics.module'; -import { CommunityFormModule } from './community-form/community-form.module'; -import { ThemedCommunityPageComponent } from './themed-community-page.component'; -import { ComcolModule } from '../shared/comcol/comcol.module'; -import { - ThemedCommunityPageSubCommunityListComponent -} from './sub-community-list/themed-community-page-sub-community-list.component'; -import { - ThemedCollectionPageSubCollectionListComponent -} from './sub-collection-list/themed-community-page-sub-collection-list.component'; -import { DsoPageModule } from '../shared/dso-page/dso-page.module'; - -const DECLARATIONS = [CommunityPageComponent, - ThemedCommunityPageComponent, - ThemedCommunityPageSubCommunityListComponent, - CommunityPageSubCollectionListComponent, - ThemedCollectionPageSubCollectionListComponent, - CommunityPageSubCommunityListComponent, - CreateCommunityPageComponent, - DeleteCommunityPageComponent]; - -@NgModule({ - imports: [ - CommonModule, - SharedModule, - CommunityPageRoutingModule, - StatisticsModule.forRoot(), - CommunityFormModule, - ComcolModule, - DsoPageModule, - ], - 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 f181dbfff65..e429ecf17c6 100644 --- a/src/app/community-page/community-page.resolver.spec.ts +++ b/src/app/community-page/community-page.resolver.spec.ts @@ -1,32 +1,34 @@ +import { Observable } from 'rxjs'; import { first } from 'rxjs/operators'; -import { CommunityPageResolver } from './community-page.resolver'; + import { createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils'; +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'; beforeEach(() => { communityService = { - findById: (id: string) => createSuccessfulRemoteDataObject$({ id }) + findById: (id: string) => createSuccessfulRemoteDataObject$({ id }), }; 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) => { expect(resolved.payload.id).toEqual(uuid); done(); - } + }, ); }); }); diff --git a/src/app/community-page/community-page.resolver.ts b/src/app/community-page/community-page.resolver.ts index 01de9294f37..b8820629e78 100644 --- a/src/app/community-page/community-page.resolver.ts +++ b/src/app/community-page/community-page.resolver.ts @@ -1,13 +1,22 @@ -import { Injectable } from '@angular/core'; -import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router'; +import { inject } from '@angular/core'; +import { + ActivatedRouteSnapshot, + 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'; import { Community } from '../core/shared/community.model'; -import { CommunityDataService } from '../core/data/community-data.service'; -import { followLink, FollowLinkConfig } from '../shared/utils/follow-link-config.model'; import { getFirstCompletedRemoteData } from '../core/shared/operators'; -import { ResolvedAction } from '../core/resolving/resolver.actions'; -import { Store } from '@ngrx/store'; +import { + followLink, + FollowLinkConfig, +} from '../shared/utils/follow-link-config.model'; /** * The self links defined in this list are expected to be requested somewhere in the near future @@ -17,41 +26,36 @@ export const COMMUNITY_PAGE_LINKS_TO_FOLLOW: FollowLinkConfig[] = [ followLink('logo'), followLink('subcommunities'), followLink('collections'), - followLink('parentCommunity') + followLink('parentCommunity'), ]; /** - * 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 - ) { - } - - /** - * 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(), - ); +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(), + ); - communityRD$.subscribe((communityRD: RemoteData) => { - this.store.dispatch(new ResolvedAction(state.url, communityRD.payload)); - }); + communityRD$.subscribe((communityRD: RemoteData) => { + 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 57039040c2a..1d3d6eb871b 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 fbff82efd86..062c0ea0620 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 @@ -1,17 +1,24 @@ -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { Router } from '@angular/router'; -import { TranslateModule } from '@ngx-translate/core'; import { CommonModule } from '@angular/common'; -import { RouterTestingModule } from '@angular/router/testing'; import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { RouteService } from '../../core/services/route.service'; -import { SharedModule } from '../../shared/shared.module'; +import { + ComponentFixture, + TestBed, + waitForAsync, +} from '@angular/core/testing'; +import { Router } from '@angular/router'; +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 { CreateCommunityPageComponent } from './create-community-page.component'; +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 { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub'; -import { RequestService } from '../../core/data/request.service'; +import { CommunityFormComponent } from '../community-form/community-form.component'; +import { CreateCommunityPageComponent } from './create-community-page.component'; describe('CreateCommunityPageComponent', () => { let comp: CreateCommunityPageComponent; @@ -19,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: RequestService, useValue: {} }, + { provide: AuthService, useValue: new AuthServiceMock() }, ], - schemas: [NO_ERRORS_SCHEMA] - }).compileComponents(); + schemas: [NO_ERRORS_SCHEMA], + }) + .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 eea09083887..082f6c4f0b4 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,13 +1,24 @@ +import { + AsyncPipe, + NgIf, +} from '@angular/common'; import { Component } from '@angular/core'; -import { Community } from '../../core/shared/community.model'; +import { Router } from '@angular/router'; +import { + TranslateModule, + TranslateService, +} from '@ngx-translate/core'; + +import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; import { CommunityDataService } from '../../core/data/community-data.service'; +import { RequestService } from '../../core/data/request.service'; import { RouteService } from '../../core/services/route.service'; -import { Router } from '@angular/router'; +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 { TranslateService } from '@ngx-translate/core'; -import { RequestService } from '../../core/data/request.service'; -import { DSONameService } from '../../core/breadcrumbs/dso-name.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 @@ -15,7 +26,16 @@ import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; @Component({ selector: 'ds-create-community', styleUrls: ['./create-community-page.component.scss'], - templateUrl: './create-community-page.component.html' + templateUrl: './create-community-page.component.html', + imports: [ + CommunityFormComponent, + TranslateModule, + VarDirective, + NgIf, + AsyncPipe, + ThemedLoadingComponent, + ], + standalone: true, }) export class CreateCommunityPageComponent extends CreateComColPageComponent { protected frontendURL = '/communities/'; @@ -28,7 +48,7 @@ export class CreateCommunityPageComponent extends CreateComColPageComponent { +import { Community } from '../../core/shared/community.model'; +import { RouterMock } from '../../shared/mocks/router.mock'; +import { + createFailedRemoteDataObject$, + createSuccessfulRemoteDataObject$, +} from '../../shared/remote-data.utils'; +import { createCommunityPageGuard } from './create-community-page.guard'; + +describe('createCommunityPageGuard', () => { describe('canActivate', () => { - let guard: CreateCommunityPageGuard; + let guard: any; let router; let communityDataServiceStub: any; @@ -20,46 +24,46 @@ describe('CreateCommunityPageGuard', () => { } else if (id === 'error-id') { return createFailedRemoteDataObject$('not found', 404); } - } + }, }; 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) => - expect(canActivate).toEqual(true) + expect(canActivate).toEqual(true), ); }); 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) => - expect(canActivate).toEqual(true) + expect(canActivate).toEqual(true), ); }); 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) => - expect(canActivate).toEqual(false) + expect(canActivate).toEqual(false), ); }); 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) => - expect(canActivate).toEqual(false) + expect(canActivate).toEqual(false), ); }); }); 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 835fbb65891..c3ee8c70914 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,44 +1,52 @@ -import { Injectable } from '@angular/core'; -import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '@angular/router'; +import { inject } from '@angular/core'; +import { + ActivatedRouteSnapshot, + CanActivateFn, + Router, + RouterStateSnapshot, +} from '@angular/router'; +import { + Observable, + of as observableOf, +} from 'rxjs'; +import { + map, + tap, +} from 'rxjs/operators'; -import { hasNoValue, hasValue } from '../../shared/empty.util'; import { CommunityDataService } from '../../core/data/community-data.service'; import { RemoteData } from '../../core/data/remote-data'; import { Community } from '../../core/shared/community.model'; -import { map, tap } from 'rxjs/operators'; -import { Observable, of as observableOf } from 'rxjs'; import { getFirstCompletedRemoteData } from '../../core/shared/operators'; +import { + hasNoValue, + hasValue, +} 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.html b/src/app/community-page/delete-community-page/delete-community-page.component.html index 6bb8460bc95..b241a7027c5 100644 --- a/src/app/community-page/delete-community-page/delete-community-page.component.html +++ b/src/app/community-page/delete-community-page/delete-community-page.component.html @@ -2,16 +2,16 @@
- +

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

{{ 'community.delete.text' | translate:{ dso: dsoNameService.getName(dso) } }}

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 55d0508c103..524f3e31243 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 @@ -1,17 +1,21 @@ import { CommonModule } from '@angular/common'; import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { + ComponentFixture, + TestBed, + waitForAsync, +} from '@angular/core/testing'; import { ActivatedRoute } from '@angular/router'; import { RouterTestingModule } from '@angular/router/testing'; import { TranslateModule } from '@ngx-translate/core'; import { of as observableOf } from 'rxjs'; + +import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; import { CommunityDataService } from '../../core/data/community-data.service'; -import { NotificationsService } from '../../shared/notifications/notifications.service'; -import { SharedModule } from '../../shared/shared.module'; -import { DeleteCommunityPageComponent } from './delete-community-page.component'; import { RequestService } from '../../core/data/request.service'; -import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; import { DSONameServiceMock } from '../../shared/mocks/dso-name.service.mock'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { DeleteCommunityPageComponent } from './delete-community-page.component'; describe('DeleteCommunityPageComponent', () => { let comp: DeleteCommunityPageComponent; @@ -19,16 +23,15 @@ 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: {} }, { provide: ActivatedRoute, useValue: { data: observableOf({ dso: { payload: {} } }) } }, { provide: NotificationsService, useValue: {} }, - { provide: RequestService, useValue: {}} + { provide: RequestService, useValue: {} }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); 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 65b7c81b38b..f35e2d6bd23 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,11 +1,23 @@ +import { + AsyncPipe, + NgIf, +} from '@angular/common'; import { Component } from '@angular/core'; -import { Community } from '../../core/shared/community.model'; +import { + ActivatedRoute, + Router, +} from '@angular/router'; +import { + TranslateModule, + TranslateService, +} from '@ngx-translate/core'; + +import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; import { CommunityDataService } from '../../core/data/community-data.service'; -import { ActivatedRoute, Router } from '@angular/router'; +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 { TranslateService } from '@ngx-translate/core'; -import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; +import { VarDirective } from '../../shared/utils/var.directive'; /** * Component that represents the page where a user can delete an existing Community @@ -13,7 +25,14 @@ import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; @Component({ selector: 'ds-delete-community', styleUrls: ['./delete-community-page.component.scss'], - templateUrl: './delete-community-page.component.html' + 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 d895cfd820b..28879ed7abf 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 @@ -1,25 +1,80 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { + ComponentFixture, + TestBed, +} from '@angular/core/testing'; +import { ActivatedRoute } from '@angular/router'; +import { + of as observableOf, + of, +} 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'; -xdescribe('CommunityAccessControlComponent', () => { +describe('CommunityAccessControlComponent', () => { let component: CommunityAccessControlComponent; let fixture: ComponentFixture; + const testCommunity = Object.assign(new Community(), + { + type: 'community', + metadata: { + 'dc.title': [{ value: 'community' }], + }, + uuid: 'communityUUID', + parentCommunity: observableOf(Object.assign(createSuccessfulRemoteDataObject(undefined), { statusCode: 204 })), + + _links: { + parentCommunity: 'site', + self: '/' + 'communityUUID', + }, + }, + ); + + const routeStub = { + parent: { + parent: { + data: of({ + dso: createSuccessfulRemoteDataObject(testCommunity), + }), + }, + }, + }; + + beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [ CommunityAccessControlComponent ] + imports: [CommunityAccessControlComponent], + providers: [{ + provide: ActivatedRoute, useValue: routeStub, + }], }) - .compileComponents(); + .overrideComponent(CommunityAccessControlComponent, { + remove: { + imports: [AccessControlFormContainerComponent], + }, + }) + .compileComponents(); }); + beforeEach(() => { fixture = TestBed.createComponent(CommunityAccessControlComponent); component = fixture.componentInstance; fixture.detectChanges(); + component.ngOnInit(); }); it('should create', () => { expect(component).toBeTruthy(); }); + + it('should set itemRD$', (done) => { + component.itemRD$.subscribe(result => { + expect(result).toEqual(createSuccessfulRemoteDataObject(testCommunity)); + done(); + }); + }); }); 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 8a216e38dfa..a0e094e21d4 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,15 +1,30 @@ -import { Component, OnInit } from '@angular/core'; -import { Observable } from 'rxjs'; -import { RemoteData } from '../../../core/data/remote-data'; +import { + AsyncPipe, + NgIf, +} from '@angular/common'; +import { + Component, + OnInit, +} from '@angular/core'; import { ActivatedRoute } from '@angular/router'; +import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; -import { getFirstSucceededRemoteData } from '../../../core/shared/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>; @@ -18,7 +33,7 @@ export class CommunityAccessControlComponent implements OnInit { ngOnInit(): void { this.itemRD$ = this.route.parent.parent.data.pipe( - map((data) => data.dso) + map((data) => data.dso), ).pipe(getFirstSucceededRemoteData()) as 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 719cf83a26f..921bbf0cfda 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 @@ -1,15 +1,22 @@ import { CommonModule } from '@angular/common'; -import { ChangeDetectorRef, NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { + ChangeDetectorRef, + NO_ERRORS_SCHEMA, +} from '@angular/core'; +import { + ComponentFixture, + TestBed, + waitForAsync, +} from '@angular/core/testing'; import { ActivatedRoute } from '@angular/router'; - import { cold } from 'jasmine-marbles'; 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'; -import { Collection } from '../../../core/shared/collection.model'; describe('CommunityAuthorizationsComponent', () => { let comp: CommunityAuthorizationsComponent; @@ -19,8 +26,8 @@ describe('CommunityAuthorizationsComponent', () => { uuid: 'community', id: 'community', _links: { - self: { href: 'community-selflink' } - } + self: { href: 'community-selflink' }, + }, }); const communityRD = createSuccessfulRemoteDataObject(community); @@ -29,25 +36,31 @@ describe('CommunityAuthorizationsComponent', () => { parent: { parent: { data: observableOf({ - dso: communityRD - }) - } - } + dso: communityRD, + }), + }, + }, }; beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [ - CommonModule + 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 7a9f224311d..3e42a830bef 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,13 +1,27 @@ -import { Component, OnInit } from '@angular/core'; +import { AsyncPipe } from '@angular/common'; +import { + Component, + OnInit, +} from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { Observable } from 'rxjs'; -import { first, map } from 'rxjs/operators'; +import { + first, + map, +} from 'rxjs/operators'; + 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 @@ -25,7 +39,7 @@ export class CommunityAuthorizationsComponent impl * @param {ActivatedRoute} route */ constructor( - private route: ActivatedRoute + private route: ActivatedRoute, ) { } diff --git a/src/app/community-page/edit-community-page/community-curate/community-curate.component.html b/src/app/community-page/edit-community-page/community-curate/community-curate.component.html index 6c041d17253..5e11fdfbcea 100644 --- a/src/app/community-page/edit-community-page/community-curate/community-curate.component.html +++ b/src/app/community-page/edit-community-page/community-curate/community-curate.component.html @@ -1,5 +1,5 @@
-

{{'community.curate.header' |translate:{community: (communityName$ |async)} }}

+

{{'community.curate.header' |translate:{community: (communityName$ |async)} }}

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 1b1ee2c9f95..541308c9424 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 @@ -1,12 +1,21 @@ -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { + CUSTOM_ELEMENTS_SCHEMA, + DebugElement, +} from '@angular/core'; +import { + ComponentFixture, + TestBed, + waitForAsync, +} from '@angular/core/testing'; +import { ActivatedRoute } from '@angular/router'; import { TranslateModule } from '@ngx-translate/core'; -import { CUSTOM_ELEMENTS_SCHEMA, DebugElement } from '@angular/core'; import { of as observableOf } from 'rxjs'; -import { createSuccessfulRemoteDataObject } from '../../../shared/remote-data.utils'; -import { ActivatedRoute } from '@angular/router'; + import { DSONameService } from '../../../core/breadcrumbs/dso-name.service'; -import { CommunityCurateComponent } from './community-curate.component'; 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'; describe('CommunityCurateComponent', () => { let comp: CommunityCurateComponent; @@ -17,31 +26,36 @@ describe('CommunityCurateComponent', () => { let dsoNameService; const community = Object.assign(new Community(), { - metadata: {'dc.title': ['Community Name'], 'dc.identifier.uri': [ { value: '123456789/1'}]} + metadata: { 'dc.title': ['Community Name'], 'dc.identifier.uri': [ { value: '123456789/1' }] }, }); beforeEach(waitForAsync(() => { routeStub = { parent: { data: observableOf({ - dso: createSuccessfulRemoteDataObject(community) - }) - } + dso: createSuccessfulRemoteDataObject(community), + }), + }, }; dsoNameService = jasmine.createSpyObj('dsoNameService', { - getName: 'Community Name' + getName: 'Community Name', }); TestBed.configureTestingModule({ - imports: [TranslateModule.forRoot()], - declarations: [CommunityCurateComponent], + imports: [TranslateModule.forRoot(), CommunityCurateComponent], providers: [ - {provide: ActivatedRoute, useValue: routeStub}, - {provide: DSONameService, useValue: dsoNameService} + { provide: ActivatedRoute, useValue: routeStub }, + { provide: DSONameService, useValue: dsoNameService }, ], - schemas: [CUSTOM_ELEMENTS_SCHEMA] - }).compileComponents(); + schemas: [CUSTOM_ELEMENTS_SCHEMA], + }) + .overrideComponent(CommunityCurateComponent, { + remove: { + imports: [CurationFormComponent], + }, + }) + .compileComponents(); })); beforeEach(() => { @@ -58,7 +72,7 @@ describe('CommunityCurateComponent', () => { }); it('should contain the community information provided in the route', () => { comp.dsoRD$.subscribe((value) => { - expect(value.payload.handle + expect(value.payload.handle, ).toEqual('123456789/1'); }); comp.communityName$.subscribe((value) => { 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 8ae04af8f15..fd4d2408278 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,10 +1,21 @@ -import { Component, OnInit } from '@angular/core'; -import { Community } from '../../../core/shared/community.model'; +import { AsyncPipe } from '@angular/common'; +import { + Component, + OnInit, +} from '@angular/core'; import { ActivatedRoute } from '@angular/router'; -import { filter, map, take } from 'rxjs/operators'; -import { RemoteData } from '../../../core/data/remote-data'; +import { TranslateModule } from '@ngx-translate/core'; import { Observable } from 'rxjs'; +import { + filter, + map, + take, +} from 'rxjs/operators'; + 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'; /** @@ -13,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 { @@ -35,7 +52,7 @@ export class CommunityCurateComponent implements OnInit { filter((rd: RemoteData) => hasValue(rd)), map((rd: RemoteData) => { return this.dsoNameService.getName(rd.payload); - }) + }), ); } 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 2ca5b768e4d..bf75944242f 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 c597fac0bd3..b82beaa3f73 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 @@ -1,15 +1,20 @@ -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { TranslateModule } from '@ngx-translate/core'; -import { SharedModule } from '../../../shared/shared.module'; import { CommonModule } from '@angular/common'; -import { RouterTestingModule } from '@angular/router/testing'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { + ComponentFixture, + TestBed, + waitForAsync, +} from '@angular/core/testing'; import { ActivatedRoute } from '@angular/router'; +import { RouterTestingModule } from '@angular/router/testing'; +import { TranslateModule } from '@ngx-translate/core'; import { of as observableOf } from 'rxjs'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { CommunityMetadataComponent } from './community-metadata.component'; + import { CommunityDataService } from '../../../core/data/community-data.service'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub'; +import { CommunityFormComponent } from '../../community-form/community-form.component'; +import { CommunityMetadataComponent } from './community-metadata.component'; describe('CommunityMetadataComponent', () => { let comp: CommunityMetadataComponent; @@ -17,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() } + { provide: NotificationsService, useValue: new NotificationsServiceStub() }, ], - schemas: [NO_ERRORS_SCHEMA] - }).compileComponents(); + schemas: [NO_ERRORS_SCHEMA], + }) + .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 a2dbfa6eb61..8001bd29697 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,10 +1,16 @@ +import { AsyncPipe } from '@angular/common'; import { Component } from '@angular/core'; -import { ComcolMetadataComponent } from '../../../shared/comcol/comcol-forms/edit-comcol-page/comcol-metadata/comcol-metadata.component'; -import { ActivatedRoute, Router } from '@angular/router'; -import { Community } from '../../../core/shared/community.model'; +import { + ActivatedRoute, + Router, +} from '@angular/router'; +import { TranslateService } from '@ngx-translate/core'; + 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 { TranslateService } from '@ngx-translate/core'; +import { CommunityFormComponent } from '../../community-form/community-form.component'; /** * Component for editing a community's metadata @@ -12,6 +18,11 @@ import { TranslateService } from '@ngx-translate/core'; @Component({ selector: 'ds-community-metadata', templateUrl: './community-metadata.component.html', + imports: [ + CommunityFormComponent, + AsyncPipe, + ], + standalone: true, }) export class CommunityMetadataComponent extends ComcolMetadataComponent { protected frontendURL = '/communities/'; @@ -22,7 +33,7 @@ export class CommunityMetadataComponent extends ComcolMetadataComponent { @@ -39,10 +47,10 @@ describe('CommunityRolesComponent', () => { href: 'adminGroup link', }, }, - }) + }), ), - }) - } + }), + }, }; const requestService = { @@ -55,13 +63,9 @@ describe('CommunityRolesComponent', () => { TestBed.configureTestingModule({ imports: [ - ComcolModule, - SharedModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), - NoopAnimationsModule - ], - declarations: [ + NoopAnimationsModule, CommunityRolesComponent, ], providers: [ @@ -69,9 +73,9 @@ describe('CommunityRolesComponent', () => { { provide: ActivatedRoute, useValue: route }, { provide: RequestService, useValue: requestService }, { provide: GroupDataService, useValue: groupDataService }, - { provide: NotificationsService, useClass: NotificationsServiceStub } + { provide: NotificationsService, useClass: NotificationsServiceStub }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); fixture = TestBed.createComponent(CommunityRolesComponent); 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 9468aa7048b..2e85cbe4c36 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,11 +1,26 @@ -import { Component, OnInit } from '@angular/core'; +import { + AsyncPipe, + NgForOf, +} from '@angular/common'; +import { + Component, + OnInit, +} from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { Observable } from 'rxjs'; -import { first, map } from 'rxjs/operators'; -import { Community } from '../../../core/shared/community.model'; -import { getRemoteDataPayload, getFirstSucceededRemoteData } from '../../../core/shared/operators'; +import { + first, + map, +} from 'rxjs/operators'; + import { RemoteData } from '../../../core/data/remote-data'; +import { Community } from '../../../core/shared/community.model'; import { HALLink } from '../../../core/shared/hal-link.model'; +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 @@ -13,6 +28,12 @@ import { HALLink } from '../../../core/shared/hal-link.model'; @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 00000000000..2402c2037d5 --- /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 3a4c3351c32..f099f6fc2af 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 @@ -1,13 +1,17 @@ -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { ActivatedRoute } from '@angular/router'; -import { TranslateModule } from '@ngx-translate/core'; import { CommonModule } from '@angular/common'; -import { RouterTestingModule } from '@angular/router/testing'; import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { SharedModule } from '../../shared/shared.module'; +import { + ComponentFixture, + TestBed, + waitForAsync, +} from '@angular/core/testing'; +import { ActivatedRoute } from '@angular/router'; +import { RouterTestingModule } from '@angular/router/testing'; +import { TranslateModule } from '@ngx-translate/core'; import { of as observableOf } from 'rxjs'; -import { EditCommunityPageComponent } from './edit-community-page.component'; + import { CommunityDataService } from '../../core/data/community-data.service'; +import { EditCommunityPageComponent } from './edit-community-page.component'; describe('EditCommunityPageComponent', () => { let comp: EditCommunityPageComponent; @@ -15,36 +19,35 @@ describe('EditCommunityPageComponent', () => { const routeStub = { data: observableOf({ - dso: { payload: {} } + dso: { payload: {} }, }), routeConfig: { children: [ { path: 'mockUrl', data: { - hideReturnButton: false - } - } - ] + hideReturnButton: false, + }, + }, + ], }, snapshot: { firstChild: { routeConfig: { - path: 'mockUrl' - } - } - } + path: 'mockUrl', + }, + }, + }, }; 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 }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); 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 54a6ee49442..194976c3a8d 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,6 +1,19 @@ +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 { ActivatedRoute, Router } from '@angular/router'; import { EditComColPageComponent } from '../../shared/comcol/comcol-forms/edit-comcol-page/edit-comcol-page.component'; import { getCommunityPageRoute } from '../community-page-routing-paths'; @@ -9,14 +22,24 @@ 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' + 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'; public constructor( protected router: Router, - protected route: ActivatedRoute + protected route: ActivatedRoute, ) { super(router, route); } 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 5190d6a0083..00000000000 --- a/src/app/community-page/edit-community-page/edit-community-page.module.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { NgModule } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { SharedModule } from '../../shared/shared.module'; -import { EditCommunityPageRoutingModule } from './edit-community-page.routing.module'; -import { EditCommunityPageComponent } from './edit-community-page.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 { CommunityAuthorizationsComponent } from './community-authorizations/community-authorizations.component'; -import { CommunityFormModule } from '../community-form/community-form.module'; -import { ResourcePoliciesModule } from '../../shared/resource-policies/resource-policies.module'; -import { ComcolModule } from '../../shared/comcol/comcol.module'; -import { CommunityAccessControlComponent } from './community-access-control/community-access-control.component'; -import { - AccessControlFormModule -} from '../../shared/access-control-form-container/access-control-form.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 994c6b5e961..00000000000 --- a/src/app/community-page/edit-community-page/edit-community-page.routing.module.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { EditCommunityPageComponent } from './edit-community-page.component'; -import { RouterModule } from '@angular/router'; -import { NgModule } from '@angular/core'; -import { CommunityMetadataComponent } from './community-metadata/community-metadata.component'; -import { CommunityRolesComponent } from './community-roles/community-roles.component'; -import { CommunityCurateComponent } from './community-curate/community-curate.component'; -import { I18nBreadcrumbResolver } from '../../core/breadcrumbs/i18n-breadcrumb.resolver'; -import { CommunityAuthorizationsComponent } from './community-authorizations/community-authorizations.component'; -import { ResourcePolicyTargetResolver } from '../../shared/resource-policies/resolvers/resource-policy-target.resolver'; -import { ResourcePolicyCreateComponent } from '../../shared/resource-policies/create/resource-policy-create.component'; -import { ResourcePolicyResolver } from '../../shared/resource-policies/resolvers/resource-policy.resolver'; -import { ResourcePolicyEditComponent } from '../../shared/resource-policies/edit/resource-policy-edit.component'; -import { CommunityAdministratorGuard } from '../../core/data/feature-authorization/feature-authorization-guard/community-administrator.guard'; -import { CommunityAccessControlComponent } from './community-access-control/community-access-control.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/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 similarity index 71% rename from src/app/community-page/sub-collection-list/community-page-sub-collection-list.component.html rename to src/app/community-page/sections/sub-com-col-section/sub-collection-list/community-page-sub-collection-list.component.html index 69f16ee3ac0..59d7b3bb5e2 100644 --- a/src/app/community-page/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 @@ -1,6 +1,6 @@
-

{{'community.sub-collection-list.head' | translate}}

+

{{'community.sub-collection-list.head' | translate}}

{{'community.sub-collection-list.head' | translate}}
- +
diff --git a/src/themes/custom/app/community-page/sub-collection-list/community-page-sub-collection-list.component.scss b/src/app/community-page/sections/sub-com-col-section/sub-collection-list/community-page-sub-collection-list.component.scss similarity index 100% rename from src/themes/custom/app/community-page/sub-collection-list/community-page-sub-collection-list.component.scss rename to src/app/community-page/sections/sub-com-col-section/sub-collection-list/community-page-sub-collection-list.component.scss 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 new file mode 100644 index 00000000000..dc4ab520812 --- /dev/null +++ b/src/app/community-page/sections/sub-com-col-section/sub-collection-list/community-page-sub-collection-list.component.spec.ts @@ -0,0 +1,206 @@ +import { + DebugElement, + NO_ERRORS_SCHEMA, +} from '@angular/core'; +import { + ComponentFixture, + TestBed, + waitForAsync, +} from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { TranslateModule } from '@ngx-translate/core'; + +import { CollectionDataService } from '../../../../core/data/collection-data.service'; +import { ConfigurationDataService } from '../../../../core/data/configuration-data.service'; +import { FindListOptions } from '../../../../core/data/find-list-options.model'; +import { buildPaginatedList } from '../../../../core/data/paginated-list.model'; +import { GroupDataService } from '../../../../core/eperson/group-data.service'; +import { PaginationService } from '../../../../core/pagination/pagination.service'; +import { LinkHeadService } from '../../../../core/services/link-head.service'; +import { Community } from '../../../../core/shared/community.model'; +import { ConfigurationProperty } from '../../../../core/shared/configuration-property.model'; +import { PageInfo } from '../../../../core/shared/page-info.model'; +import { SearchConfigurationService } from '../../../../core/shared/search/search-configuration.service'; +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 { 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'; +import { createPaginatedList } from '../../../../shared/testing/utils.test'; +import { ThemeService } from '../../../../shared/theme-support/theme.service'; +import { CommunityPageSubCollectionListComponent } from './community-page-sub-collection-list.component'; + +describe('CommunityPageSubCollectionListComponent', () => { + let comp: CommunityPageSubCollectionListComponent; + let fixture: ComponentFixture; + let collectionDataServiceStub: any; + let themeService; + let subCollList = []; + + const collections = [Object.assign(new Community(), { + id: '123456789-1', + metadata: { + 'dc.title': [ + { language: 'en_US', value: 'Collection 1' }, + ], + }, + }), + Object.assign(new Community(), { + id: '123456789-2', + metadata: { + 'dc.title': [ + { language: 'en_US', value: 'Collection 2' }, + ], + }, + }), + Object.assign(new Community(), { + id: '123456789-3', + metadata: { + 'dc.title': [ + { language: 'en_US', value: 'Collection 3' }, + ], + }, + }), + Object.assign(new Community(), { + id: '123456789-4', + metadata: { + 'dc.title': [ + { language: 'en_US', value: 'Collection 4' }, + ], + }, + }), + Object.assign(new Community(), { + id: '123456789-5', + metadata: { + 'dc.title': [ + { language: 'en_US', value: 'Collection 5' }, + ], + }, + }), + Object.assign(new Community(), { + id: '123456789-6', + metadata: { + 'dc.title': [ + { language: 'en_US', value: 'Collection 6' }, + ], + }, + }), + Object.assign(new Community(), { + id: '123456789-7', + metadata: { + 'dc.title': [ + { language: 'en_US', value: 'Collection 7' }, + ], + }, + }), + ]; + + const mockCommunity = Object.assign(new Community(), { + id: '123456789', + metadata: { + 'dc.title': [ + { language: 'en_US', value: 'Test title' }, + ], + }, + }); + + collectionDataServiceStub = { + findByParent(parentUUID: string, options: FindListOptions = {}) { + let currentPage = options.currentPage; + let elementsPerPage = options.elementsPerPage; + if (currentPage === undefined) { + currentPage = 1; + } + elementsPerPage = 5; + const startPageIndex = (currentPage - 1) * elementsPerPage; + let endPageIndex = (currentPage * elementsPerPage); + if (endPageIndex > subCollList.length) { + endPageIndex = subCollList.length; + } + return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), subCollList.slice(startPageIndex, endPageIndex))); + + }, + }; + + const paginationService = new PaginationServiceStub(); + + themeService = getMockThemeService(); + + const linkHeadService = jasmine.createSpyObj('linkHeadService', { + addTag: '', + }); + + const groupDataService = jasmine.createSpyObj('groupsDataService', { + findListByHref: createSuccessfulRemoteDataObject$(createPaginatedList([])), + getGroupRegistryRouterLink: '', + getUUIDFromString: '', + }); + + const configurationDataService = jasmine.createSpyObj('configurationDataService', { + findByPropertyName: createSuccessfulRemoteDataObject$(Object.assign(new ConfigurationProperty(), { + name: 'test', + values: [ + 'org.dspace.ctask.general.ProfileFormats = test', + ], + })), + }); + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [ + TranslateModule.forRoot(), + RouterTestingModule.withRoutes([]), + NgbModule, + NoopAnimationsModule, + CommunityPageSubCollectionListComponent, + ], + providers: [ + { provide: CollectionDataService, useValue: collectionDataServiceStub }, + { provide: HostWindowService, useValue: new HostWindowServiceStub(0) }, + { provide: PaginationService, useValue: paginationService }, + { provide: SelectableListService, useValue: {} }, + { provide: ThemeService, useValue: themeService }, + { provide: GroupDataService, useValue: groupDataService }, + { provide: LinkHeadService, useValue: linkHeadService }, + { provide: ConfigurationDataService, useValue: configurationDataService }, + { provide: SearchConfigurationService, useValue: new SearchConfigurationServiceStub() }, + ], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(CommunityPageSubCollectionListComponent); + comp = fixture.componentInstance; + comp.community = mockCommunity; + }); + + + it('should display a list of collections', async () => { + subCollList = collections; + fixture.detectChanges(); + await fixture.whenStable(); + fixture.detectChanges(); + + const collList: DebugElement[] = fixture.debugElement.queryAll(By.css('ul[data-test="objects"] li')); + expect(collList.length).toEqual(5); + expect(collList[0].nativeElement.textContent).toContain('Collection 1'); + expect(collList[1].nativeElement.textContent).toContain('Collection 2'); + expect(collList[2].nativeElement.textContent).toContain('Collection 3'); + expect(collList[3].nativeElement.textContent).toContain('Collection 4'); + expect(collList[4].nativeElement.textContent).toContain('Collection 5'); + }); + + it('should not display the header when list of collections is empty', () => { + subCollList = []; + fixture.detectChanges(); + + const subComHead = fixture.debugElement.queryAll(By.css('h2')); + expect(subComHead.length).toEqual(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 new file mode 100644 index 00000000000..1e8ff1d46c3 --- /dev/null +++ b/src/app/community-page/sections/sub-com-col-section/sub-collection-list/community-page-sub-collection-list.component.ts @@ -0,0 +1,130 @@ +import { + AsyncPipe, + NgIf, +} from '@angular/common'; +import { + Component, + Input, + OnDestroy, + OnInit, +} from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { TranslateModule } from '@ngx-translate/core'; +import { + BehaviorSubject, + combineLatest as observableCombineLatest, + Subscription, +} from 'rxjs'; +import { switchMap } from 'rxjs/operators'; + +import { + SortDirection, + SortOptions, +} from '../../../../core/cache/models/sort-options.model'; +import { CollectionDataService } from '../../../../core/data/collection-data.service'; +import { PaginatedList } from '../../../../core/data/paginated-list.model'; +import { RemoteData } from '../../../../core/data/remote-data'; +import { PaginationService } from '../../../../core/pagination/pagination.service'; +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-base-community-page-sub-collection-list', + styleUrls: ['./community-page-sub-collection-list.component.scss'], + templateUrl: './community-page-sub-collection-list.component.html', + animations: [fadeIn], + imports: [ + ObjectCollectionComponent, + ErrorComponent, + ThemedLoadingComponent, + NgIf, + TranslateModule, + AsyncPipe, + VarDirective, + ], + standalone: true, +}) +export class CommunityPageSubCollectionListComponent implements OnInit, OnDestroy { + @Input() community: Community; + + /** + * Optional page size. Overrides communityList.pageSize configuration for this component. + * Value can be added in the themed version of the parent component. + */ + @Input() pageSize: number; + + /** + * The pagination configuration + */ + config: PaginationComponentOptions; + + /** + * The pagination id + */ + pageId = 'cmcl'; + + /** + * The sorting configuration + */ + sortConfig: SortOptions; + + /** + * A list of remote data objects of communities' collections + */ + subCollectionsRDObs: BehaviorSubject>> = new BehaviorSubject>>({} as any); + + subscriptions: Subscription[] = []; + + constructor( + protected cds: CollectionDataService, + protected paginationService: PaginationService, + protected route: ActivatedRoute, + ) { + } + + ngOnInit(): void { + this.config = new PaginationComponentOptions(); + this.config.id = this.pageId; + if (hasValue(this.pageSize)) { + this.config.pageSize = this.pageSize; + } else { + this.config.pageSize = this.route.snapshot.queryParams[this.pageId + '.rpp'] ?? this.config.pageSize; + } + this.config.currentPage = this.route.snapshot.queryParams[this.pageId + '.page'] ?? 1; + this.sortConfig = new SortOptions('dc.title', SortDirection[this.route.snapshot.queryParams[this.pageId + '.sd']] ?? SortDirection.ASC); + this.initPage(); + } + + /** + * Initialise the list of collections + */ + initPage() { + const pagination$ = this.paginationService.getCurrentPagination(this.config.id, this.config); + const sort$ = this.paginationService.getCurrentSort(this.config.id, this.sortConfig); + + this.subscriptions.push(observableCombineLatest([pagination$, sort$]).pipe( + switchMap(([currentPagination, currentSort]) => { + return this.cds.findByParent(this.community.id, { + currentPage: currentPagination.currentPage, + elementsPerPage: currentPagination.pageSize, + sort: { field: currentSort.field, direction: currentSort.direction }, + }); + }), + ).subscribe((results) => { + this.subCollectionsRDObs.next(results); + })); + } + + ngOnDestroy(): void { + this.paginationService.clearPagination(this.config?.id); + this.subscriptions.map((subscription: Subscription) => subscription.unsubscribe()); + } + +} diff --git a/src/app/community-page/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 similarity index 56% rename from src/app/community-page/sub-collection-list/themed-community-page-sub-collection-list.component.ts rename to src/app/community-page/sections/sub-com-col-section/sub-collection-list/themed-community-page-sub-collection-list.component.ts index f1f49f204c2..4a965bc9264 100644 --- a/src/app/community-page/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 @@ -1,12 +1,18 @@ -import { ThemedComponent } from '../../shared/theme-support/themed.component'; +import { + Component, + Input, +} from '@angular/core'; + +import { Community } from '../../../../core/shared/community.model'; +import { ThemedComponent } from '../../../../shared/theme-support/themed.component'; import { CommunityPageSubCollectionListComponent } from './community-page-sub-collection-list.component'; -import { Component, Input } from '@angular/core'; -import { Community } from '../../core/shared/community.model'; @Component({ - selector: 'ds-themed-community-page-sub-collection-list', + selector: 'ds-community-page-sub-collection-list', styleUrls: [], - templateUrl: '../../shared/theme-support/themed.component.html', + templateUrl: '../../../../shared/theme-support/themed.component.html', + standalone: true, + imports: [CommunityPageSubCollectionListComponent], }) export class ThemedCollectionPageSubCollectionListComponent extends ThemedComponent { @Input() community: Community; @@ -18,7 +24,7 @@ export class ThemedCollectionPageSubCollectionListComponent extends ThemedCompon } protected importThemedComponent(themeName: string): Promise { - return import(`../../../themes/${themeName}/app/community-page/sub-collection-list/community-page-sub-collection-list.component`); + return import(`../../../../../themes/${themeName}/app/community-page/sections/sub-com-col-section/sub-collection-list/community-page-sub-collection-list.component`); } protected importUnthemedComponent(): Promise { 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 new file mode 100644 index 00000000000..a811014bcc9 --- /dev/null +++ b/src/app/community-page/sections/sub-com-col-section/sub-com-col-section.component.html @@ -0,0 +1,8 @@ + + + + + + diff --git a/src/themes/custom/app/browse-by/browse-by-date-page/browse-by-date-page.component.html b/src/app/community-page/sections/sub-com-col-section/sub-com-col-section.component.scss similarity index 100% rename from src/themes/custom/app/browse-by/browse-by-date-page/browse-by-date-page.component.html rename to src/app/community-page/sections/sub-com-col-section/sub-com-col-section.component.scss 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 new file mode 100644 index 00000000000..85d8eb4fb70 --- /dev/null +++ b/src/app/community-page/sections/sub-com-col-section/sub-com-col-section.component.spec.ts @@ -0,0 +1,35 @@ +import { + ComponentFixture, + TestBed, +} from '@angular/core/testing'; +import { ActivatedRoute } from '@angular/router'; + +import { ActivatedRouteStub } from '../../../shared/testing/active-router.stub'; +import { SubComColSectionComponent } from './sub-com-col-section.component'; + +describe('SubComColSectionComponent', () => { + let component: SubComColSectionComponent; + let fixture: ComponentFixture; + + let activatedRoute: ActivatedRouteStub; + + beforeEach(async () => { + activatedRoute = new ActivatedRouteStub(); + activatedRoute.parent = new ActivatedRouteStub(); + + await TestBed.configureTestingModule({ + imports: [SubComColSectionComponent], + providers: [ + { provide: ActivatedRoute, useValue: activatedRoute }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(SubComColSectionComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); 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 new file mode 100644 index 00000000000..7aed3be076b --- /dev/null +++ b/src/app/community-page/sections/sub-com-col-section/sub-com-col-section.component.ts @@ -0,0 +1,48 @@ +import { + AsyncPipe, + NgIf, +} from '@angular/common'; +import { + Component, + OnInit, +} from '@angular/core'; +import { + ActivatedRoute, + Data, +} from '@angular/router'; +import { Observable } from 'rxjs'; +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 { + + community$: Observable; + + constructor( + private route: ActivatedRoute, + ) { + } + + ngOnInit(): void { + this.community$ = this.route.parent.data.pipe( + map((data: Data) => (data.dso as RemoteData).payload), + ); + } + +} diff --git a/src/app/community-page/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 similarity index 71% rename from src/app/community-page/sub-community-list/community-page-sub-community-list.component.html rename to src/app/community-page/sections/sub-com-col-section/sub-community-list/community-page-sub-community-list.component.html index be2788a9f40..7f9840f6b7f 100644 --- a/src/app/community-page/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 @@ -1,6 +1,6 @@
-

{{'community.sub-community-list.head' | translate}}

+

{{'community.sub-community-list.head' | translate}}

{{'community.sub-community-list.head' | translate}}
- +
diff --git a/src/themes/custom/app/community-page/sub-community-list/community-page-sub-community-list.component.scss b/src/app/community-page/sections/sub-com-col-section/sub-community-list/community-page-sub-community-list.component.scss similarity index 100% rename from src/themes/custom/app/community-page/sub-community-list/community-page-sub-community-list.component.scss rename to src/app/community-page/sections/sub-com-col-section/sub-community-list/community-page-sub-community-list.component.scss 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 new file mode 100644 index 00000000000..2654585eda9 --- /dev/null +++ b/src/app/community-page/sections/sub-com-col-section/sub-community-list/community-page-sub-community-list.component.spec.ts @@ -0,0 +1,208 @@ +import { + DebugElement, + NO_ERRORS_SCHEMA, +} from '@angular/core'; +import { + ComponentFixture, + TestBed, + waitForAsync, +} from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { TranslateModule } from '@ngx-translate/core'; + +import { CommunityDataService } from '../../../../core/data/community-data.service'; +import { ConfigurationDataService } from '../../../../core/data/configuration-data.service'; +import { FindListOptions } from '../../../../core/data/find-list-options.model'; +import { buildPaginatedList } from '../../../../core/data/paginated-list.model'; +import { GroupDataService } from '../../../../core/eperson/group-data.service'; +import { PaginationService } from '../../../../core/pagination/pagination.service'; +import { LinkHeadService } from '../../../../core/services/link-head.service'; +import { Community } from '../../../../core/shared/community.model'; +import { ConfigurationProperty } from '../../../../core/shared/configuration-property.model'; +import { PageInfo } from '../../../../core/shared/page-info.model'; +import { SearchConfigurationService } from '../../../../core/shared/search/search-configuration.service'; +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 { 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'; +import { createPaginatedList } from '../../../../shared/testing/utils.test'; +import { ThemeService } from '../../../../shared/theme-support/theme.service'; +import { CommunityPageSubCommunityListComponent } from './community-page-sub-community-list.component'; + +describe('CommunityPageSubCommunityListComponent', () => { + let comp: CommunityPageSubCommunityListComponent; + let fixture: ComponentFixture; + let communityDataServiceStub: any; + let themeService; + let subCommList = []; + + const subcommunities = [Object.assign(new Community(), { + id: '123456789-1', + metadata: { + 'dc.title': [ + { language: 'en_US', value: 'SubCommunity 1' }, + ], + }, + }), + Object.assign(new Community(), { + id: '123456789-2', + metadata: { + 'dc.title': [ + { language: 'en_US', value: 'SubCommunity 2' }, + ], + }, + }), + Object.assign(new Community(), { + id: '123456789-3', + metadata: { + 'dc.title': [ + { language: 'en_US', value: 'SubCommunity 3' }, + ], + }, + }), + Object.assign(new Community(), { + id: '12345678942', + metadata: { + 'dc.title': [ + { language: 'en_US', value: 'SubCommunity 4' }, + ], + }, + }), + Object.assign(new Community(), { + id: '123456789-5', + metadata: { + 'dc.title': [ + { language: 'en_US', value: 'SubCommunity 5' }, + ], + }, + }), + Object.assign(new Community(), { + id: '123456789-6', + metadata: { + 'dc.title': [ + { language: 'en_US', value: 'SubCommunity 6' }, + ], + }, + }), + Object.assign(new Community(), { + id: '123456789-7', + metadata: { + 'dc.title': [ + { language: 'en_US', value: 'SubCommunity 7' }, + ], + }, + }), + ]; + + const mockCommunity = Object.assign(new Community(), { + id: '123456789', + metadata: { + 'dc.title': [ + { language: 'en_US', value: 'Test title' }, + ], + }, + }); + + communityDataServiceStub = { + findByParent(parentUUID: string, options: FindListOptions = {}) { + let currentPage = options.currentPage; + let elementsPerPage = options.elementsPerPage; + if (currentPage === undefined) { + currentPage = 1; + } + elementsPerPage = 5; + + const startPageIndex = (currentPage - 1) * elementsPerPage; + let endPageIndex = (currentPage * elementsPerPage); + if (endPageIndex > subCommList.length) { + endPageIndex = subCommList.length; + } + return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), subCommList.slice(startPageIndex, endPageIndex))); + + }, + }; + + const linkHeadService = jasmine.createSpyObj('linkHeadService', { + addTag: '', + }); + + const groupDataService = jasmine.createSpyObj('groupsDataService', { + findListByHref: createSuccessfulRemoteDataObject$(createPaginatedList([])), + getGroupRegistryRouterLink: '', + getUUIDFromString: '', + }); + + const configurationDataService = jasmine.createSpyObj('configurationDataService', { + findByPropertyName: createSuccessfulRemoteDataObject$(Object.assign(new ConfigurationProperty(), { + name: 'test', + values: [ + 'org.dspace.ctask.general.ProfileFormats = test', + ], + })), + }); + + const paginationService = new PaginationServiceStub(); + + themeService = getMockThemeService(); + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [ + TranslateModule.forRoot(), + RouterTestingModule.withRoutes([]), + NgbModule, + NoopAnimationsModule, + CommunityPageSubCommunityListComponent, + ], + providers: [ + { provide: CommunityDataService, useValue: communityDataServiceStub }, + { provide: HostWindowService, useValue: new HostWindowServiceStub(0) }, + { provide: PaginationService, useValue: paginationService }, + { provide: SelectableListService, useValue: {} }, + { provide: ThemeService, useValue: themeService }, + { provide: GroupDataService, useValue: groupDataService }, + { provide: LinkHeadService, useValue: linkHeadService }, + { provide: ConfigurationDataService, useValue: configurationDataService }, + { provide: SearchConfigurationService, useValue: new SearchConfigurationServiceStub() }, + ], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(CommunityPageSubCommunityListComponent); + comp = fixture.componentInstance; + comp.community = mockCommunity; + + }); + + + it('should display a list of sub-communities', async () => { + subCommList = subcommunities; + fixture.detectChanges(); + await fixture.whenStable(); + fixture.detectChanges(); + + const subComList: DebugElement[] = fixture.debugElement.queryAll(By.css('ul[data-test="objects"] li')); + expect(subComList.length).toEqual(5); + expect(subComList[0].nativeElement.textContent).toContain('SubCommunity 1'); + expect(subComList[1].nativeElement.textContent).toContain('SubCommunity 2'); + expect(subComList[2].nativeElement.textContent).toContain('SubCommunity 3'); + expect(subComList[3].nativeElement.textContent).toContain('SubCommunity 4'); + expect(subComList[4].nativeElement.textContent).toContain('SubCommunity 5'); + }); + + it('should not display the header when list of sub-communities is empty', () => { + subCommList = []; + fixture.detectChanges(); + + const subComHead = fixture.debugElement.queryAll(By.css('h2')); + expect(subComHead.length).toEqual(0); + }); +}); diff --git a/src/app/community-page/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 similarity index 55% rename from src/app/community-page/sub-community-list/community-page-sub-community-list.component.ts rename to src/app/community-page/sections/sub-com-col-section/sub-community-list/community-page-sub-community-list.component.ts index 5a0409a0519..36bd9919bb8 100644 --- a/src/app/community-page/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,24 +1,58 @@ -import { Component, Input, OnDestroy, OnInit } from '@angular/core'; +import { + AsyncPipe, + NgIf, +} from '@angular/common'; +import { + Component, + Input, + OnDestroy, + OnInit, +} from '@angular/core'; import { ActivatedRoute } from '@angular/router'; - -import { BehaviorSubject, combineLatest as observableCombineLatest } from 'rxjs'; - -import { RemoteData } from '../../core/data/remote-data'; -import { Community } from '../../core/shared/community.model'; -import { fadeIn } from '../../shared/animations/fade'; -import { PaginatedList } from '../../core/data/paginated-list.model'; -import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; -import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model'; -import { CommunityDataService } from '../../core/data/community-data.service'; +import { TranslateModule } from '@ngx-translate/core'; +import { + BehaviorSubject, + combineLatest as observableCombineLatest, + Subscription, +} from 'rxjs'; import { switchMap } from 'rxjs/operators'; -import { PaginationService } from '../../core/pagination/pagination.service'; -import { hasValue } from '../../shared/empty.util'; + +import { + SortDirection, + SortOptions, +} from '../../../../core/cache/models/sort-options.model'; +import { CommunityDataService } from '../../../../core/data/community-data.service'; +import { PaginatedList } from '../../../../core/data/paginated-list.model'; +import { RemoteData } from '../../../../core/data/remote-data'; +import { PaginationService } from '../../../../core/pagination/pagination.service'; +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] + 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 @@ -52,6 +86,8 @@ export class CommunityPageSubCommunityListComponent implements OnInit, OnDestroy */ subCommunitiesRDObs: BehaviorSubject>> = new BehaviorSubject>>({} as any); + subscriptions: Subscription[] = []; + constructor( protected cds: CommunityDataService, protected paginationService: PaginationService, @@ -79,21 +115,22 @@ export class CommunityPageSubCommunityListComponent implements OnInit, OnDestroy const pagination$ = this.paginationService.getCurrentPagination(this.config.id, this.config); const sort$ = this.paginationService.getCurrentSort(this.config.id, this.sortConfig); - observableCombineLatest([pagination$, sort$]).pipe( + this.subscriptions.push(observableCombineLatest([pagination$, sort$]).pipe( switchMap(([currentPagination, currentSort]) => { return this.cds.findByParent(this.community.id, { currentPage: currentPagination.currentPage, elementsPerPage: currentPagination.pageSize, - sort: { field: currentSort.field, direction: currentSort.direction } + sort: { field: currentSort.field, direction: currentSort.direction }, }); - }) + }), ).subscribe((results) => { this.subCommunitiesRDObs.next(results); - }); + })); } ngOnDestroy(): void { - this.paginationService.clearPagination(this.config.id); + this.paginationService.clearPagination(this.config?.id); + this.subscriptions.map((subscription: Subscription) => subscription.unsubscribe()); } } diff --git a/src/app/community-page/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 similarity index 56% rename from src/app/community-page/sub-community-list/themed-community-page-sub-community-list.component.ts rename to src/app/community-page/sections/sub-com-col-section/sub-community-list/themed-community-page-sub-community-list.component.ts index 852c53186ef..5988ad0f5ea 100644 --- a/src/app/community-page/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 @@ -1,12 +1,18 @@ -import { ThemedComponent } from '../../shared/theme-support/themed.component'; +import { + Component, + Input, +} from '@angular/core'; + +import { Community } from '../../../../core/shared/community.model'; +import { ThemedComponent } from '../../../../shared/theme-support/themed.component'; import { CommunityPageSubCommunityListComponent } from './community-page-sub-community-list.component'; -import { Component, Input } from '@angular/core'; -import { Community } from '../../core/shared/community.model'; @Component({ - selector: 'ds-themed-community-page-sub-community-list', + selector: 'ds-community-page-sub-community-list', styleUrls: [], - templateUrl: '../../shared/theme-support/themed.component.html', + templateUrl: '../../../../shared/theme-support/themed.component.html', + standalone: true, + imports: [CommunityPageSubCommunityListComponent], }) export class ThemedCommunityPageSubCommunityListComponent extends ThemedComponent { @@ -19,7 +25,7 @@ export class ThemedCommunityPageSubCommunityListComponent extends ThemedComponen } protected importThemedComponent(themeName: string): Promise { - return import(`../../../themes/${themeName}/app/community-page/sub-community-list/community-page-sub-community-list.component`); + return import(`../../../../../themes/${themeName}/app/community-page/sections/sub-com-col-section/sub-community-list/community-page-sub-community-list.component`); } protected importUnthemedComponent(): Promise { diff --git a/src/app/community-page/sub-collection-list/community-page-sub-collection-list.component.spec.ts b/src/app/community-page/sub-collection-list/community-page-sub-collection-list.component.spec.ts deleted file mode 100644 index bca3c42a950..00000000000 --- a/src/app/community-page/sub-collection-list/community-page-sub-collection-list.component.spec.ts +++ /dev/null @@ -1,202 +0,0 @@ -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { TranslateModule } from '@ngx-translate/core'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { By } from '@angular/platform-browser'; -import { RouterTestingModule } from '@angular/router/testing'; -import { NoopAnimationsModule } from '@angular/platform-browser/animations'; - -import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; - -import { CommunityPageSubCollectionListComponent } from './community-page-sub-collection-list.component'; -import { Community } from '../../core/shared/community.model'; -import { SharedModule } from '../../shared/shared.module'; -import { CollectionDataService } from '../../core/data/collection-data.service'; -import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; -import { buildPaginatedList } from '../../core/data/paginated-list.model'; -import { PageInfo } from '../../core/shared/page-info.model'; -import { HostWindowService } from '../../shared/host-window.service'; -import { HostWindowServiceStub } from '../../shared/testing/host-window-service.stub'; -import { SelectableListService } from '../../shared/object-list/selectable-list/selectable-list.service'; -import { PaginationService } from '../../core/pagination/pagination.service'; -import { getMockThemeService } from '../../shared/mocks/theme-service.mock'; -import { ThemeService } from '../../shared/theme-support/theme.service'; -import { PaginationServiceStub } from '../../shared/testing/pagination-service.stub'; -import { FindListOptions } from '../../core/data/find-list-options.model'; -import { GroupDataService } from '../../core/eperson/group-data.service'; -import { LinkHeadService } from '../../core/services/link-head.service'; -import { ConfigurationDataService } from '../../core/data/configuration-data.service'; -import { SearchConfigurationService } from '../../core/shared/search/search-configuration.service'; -import { ConfigurationProperty } from '../../core/shared/configuration-property.model'; -import { createPaginatedList } from '../../shared/testing/utils.test'; -import { SearchConfigurationServiceStub } from '../../shared/testing/search-configuration-service.stub'; - -describe('CommunityPageSubCollectionList Component', () => { - let comp: CommunityPageSubCollectionListComponent; - let fixture: ComponentFixture; - let collectionDataServiceStub: any; - let themeService; - let subCollList = []; - - const collections = [Object.assign(new Community(), { - id: '123456789-1', - metadata: { - 'dc.title': [ - { language: 'en_US', value: 'Collection 1' } - ] - } - }), - Object.assign(new Community(), { - id: '123456789-2', - metadata: { - 'dc.title': [ - { language: 'en_US', value: 'Collection 2' } - ] - } - }), - Object.assign(new Community(), { - id: '123456789-3', - metadata: { - 'dc.title': [ - { language: 'en_US', value: 'Collection 3' } - ] - } - }), - Object.assign(new Community(), { - id: '123456789-4', - metadata: { - 'dc.title': [ - { language: 'en_US', value: 'Collection 4' } - ] - } - }), - Object.assign(new Community(), { - id: '123456789-5', - metadata: { - 'dc.title': [ - { language: 'en_US', value: 'Collection 5' } - ] - } - }), - Object.assign(new Community(), { - id: '123456789-6', - metadata: { - 'dc.title': [ - { language: 'en_US', value: 'Collection 6' } - ] - } - }), - Object.assign(new Community(), { - id: '123456789-7', - metadata: { - 'dc.title': [ - { language: 'en_US', value: 'Collection 7' } - ] - } - }) - ]; - - const mockCommunity = Object.assign(new Community(), { - id: '123456789', - metadata: { - 'dc.title': [ - { language: 'en_US', value: 'Test title' } - ] - } - }); - - collectionDataServiceStub = { - findByParent(parentUUID: string, options: FindListOptions = {}) { - let currentPage = options.currentPage; - let elementsPerPage = options.elementsPerPage; - if (currentPage === undefined) { - currentPage = 1; - } - elementsPerPage = 5; - const startPageIndex = (currentPage - 1) * elementsPerPage; - let endPageIndex = (currentPage * elementsPerPage); - if (endPageIndex > subCollList.length) { - endPageIndex = subCollList.length; - } - return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), subCollList.slice(startPageIndex, endPageIndex))); - - } - }; - - const paginationService = new PaginationServiceStub(); - - themeService = getMockThemeService(); - - const linkHeadService = jasmine.createSpyObj('linkHeadService', { - addTag: '' - }); - - const groupDataService = jasmine.createSpyObj('groupsDataService', { - findListByHref: createSuccessfulRemoteDataObject$(createPaginatedList([])), - getGroupRegistryRouterLink: '', - getUUIDFromString: '', - }); - - const configurationDataService = jasmine.createSpyObj('configurationDataService', { - findByPropertyName: createSuccessfulRemoteDataObject$(Object.assign(new ConfigurationProperty(), { - name: 'test', - values: [ - 'org.dspace.ctask.general.ProfileFormats = test' - ] - })) - }); - - beforeEach(waitForAsync(() => { - TestBed.configureTestingModule({ - imports: [ - TranslateModule.forRoot(), - SharedModule, - RouterTestingModule.withRoutes([]), - NgbModule, - NoopAnimationsModule - ], - declarations: [CommunityPageSubCollectionListComponent], - providers: [ - { provide: CollectionDataService, useValue: collectionDataServiceStub }, - { provide: HostWindowService, useValue: new HostWindowServiceStub(0) }, - { provide: PaginationService, useValue: paginationService }, - { provide: SelectableListService, useValue: {} }, - { provide: ThemeService, useValue: themeService }, - { provide: GroupDataService, useValue: groupDataService }, - { provide: LinkHeadService, useValue: linkHeadService }, - { provide: ConfigurationDataService, useValue: configurationDataService }, - { provide: SearchConfigurationService, useValue: new SearchConfigurationServiceStub() }, - ], - schemas: [NO_ERRORS_SCHEMA] - }).compileComponents(); - })); - - beforeEach(() => { - fixture = TestBed.createComponent(CommunityPageSubCollectionListComponent); - comp = fixture.componentInstance; - comp.community = mockCommunity; - }); - - - it('should display a list of collections', () => { - waitForAsync(() => { - subCollList = collections; - fixture.detectChanges(); - - const collList = fixture.debugElement.queryAll(By.css('li')); - expect(collList.length).toEqual(5); - expect(collList[0].nativeElement.textContent).toContain('Collection 1'); - expect(collList[1].nativeElement.textContent).toContain('Collection 2'); - expect(collList[2].nativeElement.textContent).toContain('Collection 3'); - expect(collList[3].nativeElement.textContent).toContain('Collection 4'); - expect(collList[4].nativeElement.textContent).toContain('Collection 5'); - }); - }); - - it('should not display the header when list of collections is empty', () => { - subCollList = []; - fixture.detectChanges(); - - const subComHead = fixture.debugElement.queryAll(By.css('h2')); - expect(subComHead.length).toEqual(0); - }); -}); diff --git a/src/app/community-page/sub-collection-list/community-page-sub-collection-list.component.ts b/src/app/community-page/sub-collection-list/community-page-sub-collection-list.component.ts deleted file mode 100644 index 3a77149e5be..00000000000 --- a/src/app/community-page/sub-collection-list/community-page-sub-collection-list.component.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { Component, Input, OnDestroy, OnInit } from '@angular/core'; -import { ActivatedRoute } from '@angular/router'; - -import { BehaviorSubject, combineLatest as observableCombineLatest } from 'rxjs'; - -import { RemoteData } from '../../core/data/remote-data'; -import { Collection } from '../../core/shared/collection.model'; -import { Community } from '../../core/shared/community.model'; -import { fadeIn } from '../../shared/animations/fade'; -import { PaginatedList } from '../../core/data/paginated-list.model'; -import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; -import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model'; -import { CollectionDataService } from '../../core/data/collection-data.service'; -import { PaginationService } from '../../core/pagination/pagination.service'; -import { switchMap } from 'rxjs/operators'; -import { hasValue } from '../../shared/empty.util'; - -@Component({ - selector: 'ds-community-page-sub-collection-list', - styleUrls: ['./community-page-sub-collection-list.component.scss'], - templateUrl: './community-page-sub-collection-list.component.html', - animations:[fadeIn] -}) -export class CommunityPageSubCollectionListComponent implements OnInit, OnDestroy { - @Input() community: Community; - - /** - * Optional page size. Overrides communityList.pageSize configuration for this component. - * Value can be added in the themed version of the parent component. - */ - @Input() pageSize: number; - - /** - * The pagination configuration - */ - config: PaginationComponentOptions; - - /** - * The pagination id - */ - pageId = 'cmcl'; - - /** - * The sorting configuration - */ - sortConfig: SortOptions; - - /** - * A list of remote data objects of communities' collections - */ - subCollectionsRDObs: BehaviorSubject>> = new BehaviorSubject>>({} as any); - - constructor( - protected cds: CollectionDataService, - protected paginationService: PaginationService, - protected route: ActivatedRoute, - ) { - } - - ngOnInit(): void { - this.config = new PaginationComponentOptions(); - this.config.id = this.pageId; - if (hasValue(this.pageSize)) { - this.config.pageSize = this.pageSize; - } else { - this.config.pageSize = this.route.snapshot.queryParams[this.pageId + '.rpp'] ?? this.config.pageSize; - } - this.config.currentPage = this.route.snapshot.queryParams[this.pageId + '.page'] ?? 1; - this.sortConfig = new SortOptions('dc.title', SortDirection[this.route.snapshot.queryParams[this.pageId + '.sd']] ?? SortDirection.ASC); - this.initPage(); - } - - /** - * Initialise the list of collections - */ - initPage() { - const pagination$ = this.paginationService.getCurrentPagination(this.config.id, this.config); - const sort$ = this.paginationService.getCurrentSort(this.config.id, this.sortConfig); - - observableCombineLatest([pagination$, sort$]).pipe( - switchMap(([currentPagination, currentSort]) => { - return this.cds.findByParent(this.community.id, { - currentPage: currentPagination.currentPage, - elementsPerPage: currentPagination.pageSize, - sort: {field: currentSort.field, direction: currentSort.direction} - }); - }) - ).subscribe((results) => { - this.subCollectionsRDObs.next(results); - }); - } - - ngOnDestroy(): void { - this.paginationService.clearPagination(this.config.id); - } - -} diff --git a/src/app/community-page/sub-community-list/community-page-sub-community-list.component.spec.ts b/src/app/community-page/sub-community-list/community-page-sub-community-list.component.spec.ts deleted file mode 100644 index 0a14fe6dd14..00000000000 --- a/src/app/community-page/sub-community-list/community-page-sub-community-list.component.spec.ts +++ /dev/null @@ -1,204 +0,0 @@ -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { TranslateModule } from '@ngx-translate/core'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { RouterTestingModule } from '@angular/router/testing'; -import { NoopAnimationsModule } from '@angular/platform-browser/animations'; -import { By } from '@angular/platform-browser'; - -import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; - -import { CommunityPageSubCommunityListComponent } from './community-page-sub-community-list.component'; -import { Community } from '../../core/shared/community.model'; -import { buildPaginatedList } from '../../core/data/paginated-list.model'; -import { PageInfo } from '../../core/shared/page-info.model'; -import { SharedModule } from '../../shared/shared.module'; -import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; -import { HostWindowService } from '../../shared/host-window.service'; -import { HostWindowServiceStub } from '../../shared/testing/host-window-service.stub'; -import { CommunityDataService } from '../../core/data/community-data.service'; -import { SelectableListService } from '../../shared/object-list/selectable-list/selectable-list.service'; -import { PaginationService } from '../../core/pagination/pagination.service'; -import { getMockThemeService } from '../../shared/mocks/theme-service.mock'; -import { ThemeService } from '../../shared/theme-support/theme.service'; -import { PaginationServiceStub } from '../../shared/testing/pagination-service.stub'; -import { FindListOptions } from '../../core/data/find-list-options.model'; -import { GroupDataService } from '../../core/eperson/group-data.service'; -import { LinkHeadService } from '../../core/services/link-head.service'; -import { ConfigurationDataService } from '../../core/data/configuration-data.service'; -import { SearchConfigurationService } from '../../core/shared/search/search-configuration.service'; -import { SearchConfigurationServiceStub } from '../../shared/testing/search-configuration-service.stub'; -import { ConfigurationProperty } from '../../core/shared/configuration-property.model'; -import { createPaginatedList } from '../../shared/testing/utils.test'; - -describe('CommunityPageSubCommunityListComponent Component', () => { - let comp: CommunityPageSubCommunityListComponent; - let fixture: ComponentFixture; - let communityDataServiceStub: any; - let themeService; - let subCommList = []; - - const subcommunities = [Object.assign(new Community(), { - id: '123456789-1', - metadata: { - 'dc.title': [ - { language: 'en_US', value: 'SubCommunity 1' } - ] - } - }), - Object.assign(new Community(), { - id: '123456789-2', - metadata: { - 'dc.title': [ - { language: 'en_US', value: 'SubCommunity 2' } - ] - } - }), - Object.assign(new Community(), { - id: '123456789-3', - metadata: { - 'dc.title': [ - { language: 'en_US', value: 'SubCommunity 3' } - ] - } - }), - Object.assign(new Community(), { - id: '12345678942', - metadata: { - 'dc.title': [ - { language: 'en_US', value: 'SubCommunity 4' } - ] - } - }), - Object.assign(new Community(), { - id: '123456789-5', - metadata: { - 'dc.title': [ - { language: 'en_US', value: 'SubCommunity 5' } - ] - } - }), - Object.assign(new Community(), { - id: '123456789-6', - metadata: { - 'dc.title': [ - { language: 'en_US', value: 'SubCommunity 6' } - ] - } - }), - Object.assign(new Community(), { - id: '123456789-7', - metadata: { - 'dc.title': [ - { language: 'en_US', value: 'SubCommunity 7' } - ] - } - }) - ]; - - const mockCommunity = Object.assign(new Community(), { - id: '123456789', - metadata: { - 'dc.title': [ - { language: 'en_US', value: 'Test title' } - ] - } - }); - - communityDataServiceStub = { - findByParent(parentUUID: string, options: FindListOptions = {}) { - let currentPage = options.currentPage; - let elementsPerPage = options.elementsPerPage; - if (currentPage === undefined) { - currentPage = 1; - } - elementsPerPage = 5; - - const startPageIndex = (currentPage - 1) * elementsPerPage; - let endPageIndex = (currentPage * elementsPerPage); - if (endPageIndex > subCommList.length) { - endPageIndex = subCommList.length; - } - return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), subCommList.slice(startPageIndex, endPageIndex))); - - } - }; - - const linkHeadService = jasmine.createSpyObj('linkHeadService', { - addTag: '' - }); - - const groupDataService = jasmine.createSpyObj('groupsDataService', { - findListByHref: createSuccessfulRemoteDataObject$(createPaginatedList([])), - getGroupRegistryRouterLink: '', - getUUIDFromString: '', - }); - - const configurationDataService = jasmine.createSpyObj('configurationDataService', { - findByPropertyName: createSuccessfulRemoteDataObject$(Object.assign(new ConfigurationProperty(), { - name: 'test', - values: [ - 'org.dspace.ctask.general.ProfileFormats = test' - ] - })) - }); - - const paginationService = new PaginationServiceStub(); - - themeService = getMockThemeService(); - - beforeEach(waitForAsync(() => { - TestBed.configureTestingModule({ - imports: [ - TranslateModule.forRoot(), - SharedModule, - RouterTestingModule.withRoutes([]), - NgbModule, - NoopAnimationsModule - ], - declarations: [CommunityPageSubCommunityListComponent], - providers: [ - { provide: CommunityDataService, useValue: communityDataServiceStub }, - { provide: HostWindowService, useValue: new HostWindowServiceStub(0) }, - { provide: PaginationService, useValue: paginationService }, - { provide: SelectableListService, useValue: {} }, - { provide: ThemeService, useValue: themeService }, - { provide: GroupDataService, useValue: groupDataService }, - { provide: LinkHeadService, useValue: linkHeadService }, - { provide: ConfigurationDataService, useValue: configurationDataService }, - { provide: SearchConfigurationService, useValue: new SearchConfigurationServiceStub() }, - ], - schemas: [NO_ERRORS_SCHEMA] - }).compileComponents(); - })); - - beforeEach(() => { - fixture = TestBed.createComponent(CommunityPageSubCommunityListComponent); - comp = fixture.componentInstance; - comp.community = mockCommunity; - - }); - - - it('should display a list of sub-communities', () => { - waitForAsync(() => { - subCommList = subcommunities; - fixture.detectChanges(); - - const subComList = fixture.debugElement.queryAll(By.css('li')); - expect(subComList.length).toEqual(5); - expect(subComList[0].nativeElement.textContent).toContain('SubCommunity 1'); - expect(subComList[1].nativeElement.textContent).toContain('SubCommunity 2'); - expect(subComList[2].nativeElement.textContent).toContain('SubCommunity 3'); - expect(subComList[3].nativeElement.textContent).toContain('SubCommunity 4'); - expect(subComList[4].nativeElement.textContent).toContain('SubCommunity 5'); - }); - }); - - it('should not display the header when list of sub-communities is empty', () => { - subCommList = []; - fixture.detectChanges(); - - const subComHead = fixture.debugElement.queryAll(By.css('h2')); - expect(subComHead.length).toEqual(0); - }); -}); diff --git a/src/app/community-page/themed-community-page.component.ts b/src/app/community-page/themed-community-page.component.ts index eeb058fb047..b655452041a 100644 --- a/src/app/community-page/themed-community-page.component.ts +++ b/src/app/community-page/themed-community-page.component.ts @@ -1,4 +1,5 @@ import { Component } from '@angular/core'; + import { ThemedComponent } from '../shared/theme-support/themed.component'; import { CommunityPageComponent } from './community-page.component'; @@ -6,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 3747edd532c..295e5b1e751 100644 --- a/src/app/core/auth/auth-blocking.guard.spec.ts +++ b/src/app/core/auth/auth-blocking.guard.spec.ts @@ -1,15 +1,26 @@ -import { TestBed, waitForAsync } from '@angular/core/testing'; - -import { MockStore, provideMockStore } from '@ngrx/store/testing'; -import { Store, StoreModule } from '@ngrx/store'; +import { + TestBed, + waitForAsync, +} from '@angular/core/testing'; +import { + Store, + StoreModule, +} from '@ngrx/store'; +import { + MockStore, + provideMockStore, +} from '@ngrx/store/testing'; import { cold } from 'jasmine-marbles'; -import { AppState, storeModuleConfig } from '../../app.reducer'; -import { AuthBlockingGuard } from './auth-blocking.guard'; +import { + AppState, + storeModuleConfig, +} from '../../app.reducer'; import { authReducer } from './auth.reducer'; +import { authBlockingGuard } from './auth-blocking.guard'; -describe('AuthBlockingGuard', () => { - let guard: AuthBlockingGuard; +describe('authBlockingGuard', () => { + let guard: any; let initialState; let store: Store; let mockStore: MockStore; @@ -21,9 +32,9 @@ describe('AuthBlockingGuard', () => { loaded: false, blocking: undefined, loading: false, - authMethods: [] - } - } + authMethods: [], + }, + }, }; beforeEach(waitForAsync(() => { @@ -33,22 +44,22 @@ describe('AuthBlockingGuard', () => { ], providers: [ provideMockStore({ initialState }), - { provide: AuthBlockingGuard, useValue: guard } - ] + { provide: authBlockingGuard, useValue: guard }, + ], }).compileComponents(); })); 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(); }); }); @@ -58,15 +69,15 @@ describe('AuthBlockingGuard', () => { const state = Object.assign({}, initialState, { core: Object.assign({}, initialState.core, { 'auth': { - blocking: true - } - }) + blocking: true, + }, + }), }); mockStore.setState(state); }); it(`should not emit anything`, (done) => { - expect(guard.canActivate()).toBeObservable(cold('-')); + expect(guard(null, null, store)).toBeObservable(cold('-')); done(); }); }); @@ -76,15 +87,15 @@ describe('AuthBlockingGuard', () => { const state = Object.assign({}, initialState, { core: Object.assign({}, initialState.core, { 'auth': { - blocking: false - } - }) + blocking: false, + }, + }), }); mockStore.setState(state); }); 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 9054f66f8b1..c76480ec0d2 100644 --- a/src/app/core/auth/auth-blocking.guard.ts +++ b/src/app/core/auth/auth-blocking.guard.ts @@ -1,8 +1,21 @@ -import { Injectable } from '@angular/core'; -import { CanActivate } from '@angular/router'; -import { select, Store } from '@ngrx/store'; +import { inject } from '@angular/core'; +import { + ActivatedRouteSnapshot, + CanActivateFn, + RouterStateSnapshot, +} from '@angular/router'; +import { + select, + Store, +} from '@ngrx/store'; import { Observable } from 'rxjs'; -import { distinctUntilChanged, filter, map, take } from 'rxjs/operators'; +import { + distinctUntilChanged, + filter, + map, + take, +} from 'rxjs/operators'; + import { AppState } from '../../app.reducer'; import { isAuthenticationBlocking } from './selectors'; @@ -11,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 { - - 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), - ); - } +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), + ); +}; -} diff --git a/src/app/core/auth/auth-request.service.spec.ts b/src/app/core/auth/auth-request.service.spec.ts index 063aad612f1..2220efe5faa 100644 --- a/src/app/core/auth/auth-request.service.spec.ts +++ b/src/app/core/auth/auth-request.service.spec.ts @@ -1,17 +1,21 @@ -import { AuthRequestService } from './auth-request.service'; -import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { RequestService } from '../data/request.service'; -import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; -import { PostRequest } from '../data/request.models'; +import { + Observable, + of as observableOf, +} from 'rxjs'; import { TestScheduler } from 'rxjs/testing'; + import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils'; -import { ShortLivedToken } from './models/short-lived-token.model'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; 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 objectContaining = jasmine.objectContaining; +import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { AuthRequestService } from './auth-request.service'; import { AuthStatus } from './models/auth-status.model'; -import { RestRequestMethod } from '../data/rest-request-method'; -import { Observable, of as observableOf } from 'rxjs'; +import { ShortLivedToken } from './models/short-lived-token.model'; +import objectContaining = jasmine.objectContaining; describe(`AuthRequestService`, () => { let halService: HALEndpointService; @@ -30,7 +34,7 @@ describe(`AuthRequestService`, () => { constructor( hes: HALEndpointService, rs: RequestService, - rdbs: RemoteDataBuildService + rdbs: RemoteDataBuildService, ) { super(hes, rs, rdbs); } @@ -44,19 +48,19 @@ describe(`AuthRequestService`, () => { endpointURL = 'https://rest.api/auth'; requestID = 'requestID'; shortLivedToken = Object.assign(new ShortLivedToken(), { - value: 'some-token' + value: 'some-token', }); shortLivedTokenRD = createSuccessfulRemoteDataObject(shortLivedToken); halService = jasmine.createSpyObj('halService', { - 'getEndpoint': cold('a', { a: endpointURL }) + 'getEndpoint': cold('a', { a: endpointURL }), }); requestService = jasmine.createSpyObj('requestService', { 'generateRequestId': requestID, 'send': null, }); rdbService = jasmine.createSpyObj('rdbService', { - 'buildFromRequestUUID': cold('a', { a: shortLivedTokenRD }) + 'buildFromRequestUUID': cold('a', { a: shortLivedTokenRD }), }); service = new TestAuthRequestService(halService, requestService, rdbService); diff --git a/src/app/core/auth/auth-request.service.ts b/src/app/core/auth/auth-request.service.ts index 7c1f17dec23..5d11b9f4cb0 100644 --- a/src/app/core/auth/auth-request.service.ts +++ b/src/app/core/auth/auth-request.service.ts @@ -1,18 +1,29 @@ import { Observable } from 'rxjs'; -import { distinctUntilChanged, filter, map, switchMap, tap, take } from 'rxjs/operators'; -import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { RequestService } from '../data/request.service'; +import { + distinctUntilChanged, + filter, + map, + switchMap, + take, + tap, +} from 'rxjs/operators'; + import { isNotEmpty } from '../../shared/empty.util'; -import { GetRequest, PostRequest, } from '../data/request.models'; -import { HttpOptions } from '../dspace-rest/dspace-rest.service'; -import { getFirstCompletedRemoteData } from '../shared/operators'; +import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { RemoteData } from '../data/remote-data'; +import { + GetRequest, + PostRequest, +} from '../data/request.models'; +import { RequestService } from '../data/request.service'; +import { RestRequest } from '../data/rest-request.model'; +import { HttpOptions } from '../dspace-rest/dspace-rest.service'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { getFirstCompletedRemoteData } from '../shared/operators'; +import { URLCombiner } from '../url-combiner/url-combiner'; import { AuthStatus } from './models/auth-status.model'; import { ShortLivedToken } from './models/short-lived-token.model'; -import { URLCombiner } from '../url-combiner/url-combiner'; -import { RestRequest } from '../data/rest-request.model'; -import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; /** * Abstract service to send authentication requests @@ -23,8 +34,8 @@ export abstract class AuthRequestService { constructor(protected halService: HALEndpointService, protected requestService: RequestService, - private rdbService: RemoteDataBuildService - ) { + private rdbService: RemoteDataBuildService, + ) { } /** @@ -65,7 +76,7 @@ export abstract class AuthRequestService { map((endpointURL) => this.getEndpointByMethod(endpointURL, method)), distinctUntilChanged(), map((endpointURL: string) => new PostRequest(requestId, endpointURL, body, options)), - take(1) + take(1), ).subscribe((request: PostRequest) => { this.requestService.send(request); }); @@ -90,7 +101,7 @@ export abstract class AuthRequestService { map((endpointURL) => this.getEndpointByMethod(endpointURL, method, ...linksToFollow)), distinctUntilChanged(), map((endpointURL: string) => new GetRequest(requestId, endpointURL, undefined, options)), - take(1) + take(1), ).subscribe((request: GetRequest) => { this.requestService.send(request); }); @@ -125,7 +136,7 @@ export abstract class AuthRequestService { } else { return null; } - }) + }), ); } } diff --git a/src/app/core/auth/auth.actions.ts b/src/app/core/auth/auth.actions.ts index 6bc4565682a..03b6bc11910 100644 --- a/src/app/core/auth/auth.actions.ts +++ b/src/app/core/auth/auth.actions.ts @@ -1,12 +1,13 @@ /* eslint-disable max-classes-per-file */ // import @ngrx import { Action } from '@ngrx/store'; + // import type function import { type } from '../../shared/ngrx/type'; -// import models -import { AuthTokenInfo } from './models/auth-token-info.model'; import { AuthMethod } from './models/auth.method'; import { AuthStatus } from './models/auth-status.model'; +// import models +import { AuthTokenInfo } from './models/auth-token-info.model'; export const AuthActionTypes = { AUTHENTICATE: type('dspace/auth/AUTHENTICATE'), @@ -38,7 +39,7 @@ export const AuthActionTypes = { RETRIEVE_AUTHENTICATED_EPERSON_ERROR: type('dspace/auth/RETRIEVE_AUTHENTICATED_EPERSON_ERROR'), REDIRECT_AFTER_LOGIN_SUCCESS: type('dspace/auth/REDIRECT_AFTER_LOGIN_SUCCESS'), SET_USER_AS_IDLE: type('dspace/auth/SET_USER_AS_IDLE'), - UNSET_USER_AS_IDLE: type('dspace/auth/UNSET_USER_AS_IDLE') + UNSET_USER_AS_IDLE: type('dspace/auth/UNSET_USER_AS_IDLE'), }; @@ -426,6 +427,15 @@ export class UnsetUserAsIdleAction implements Action { public type: string = AuthActionTypes.UNSET_USER_AS_IDLE; } +/** + * Authentication error actions that include Error payloads. + */ +export type AuthErrorActionsWithErrorPayload + = AuthenticatedErrorAction + | AuthenticationErrorAction + | LogOutErrorAction + | RetrieveAuthenticatedEpersonErrorAction; + /** * Actions type. * @type {AuthActions} @@ -433,9 +443,7 @@ export class UnsetUserAsIdleAction implements Action { export type AuthActions = AuthenticateAction | AuthenticatedAction - | AuthenticatedErrorAction | AuthenticatedSuccessAction - | AuthenticationErrorAction | AuthenticationSuccessAction | CheckAuthenticationTokenAction | CheckAuthenticationTokenCookieAction @@ -452,10 +460,9 @@ export type AuthActions | RetrieveAuthMethodsErrorAction | RetrieveTokenAction | RetrieveAuthenticatedEpersonAction - | RetrieveAuthenticatedEpersonErrorAction | RetrieveAuthenticatedEpersonSuccessAction | SetRedirectUrlAction | RedirectAfterLoginSuccessAction | SetUserAsIdleAction - | UnsetUserAsIdleAction; - + | UnsetUserAsIdleAction + | AuthErrorActionsWithErrorPayload; diff --git a/src/app/core/auth/auth.effects.spec.ts b/src/app/core/auth/auth.effects.spec.ts index 2e6ba917aae..a423455594a 100644 --- a/src/app/core/auth/auth.effects.spec.ts +++ b/src/app/core/auth/auth.effects.spec.ts @@ -1,12 +1,38 @@ -import { fakeAsync, TestBed, tick } from '@angular/core/testing'; - +import { + fakeAsync, + TestBed, + tick, +} from '@angular/core/testing'; import { provideMockActions } from '@ngrx/effects/testing'; -import { Store, StoreModule } from '@ngrx/store'; -import { MockStore, provideMockStore } from '@ngrx/store/testing'; -import { cold, hot } from 'jasmine-marbles'; -import { Observable, of as observableOf, throwError as observableThrow } from 'rxjs'; +import { + Store, + StoreModule, +} from '@ngrx/store'; +import { + MockStore, + provideMockStore, +} from '@ngrx/store/testing'; +import { + cold, + hot, +} from 'jasmine-marbles'; +import { + Observable, + of as observableOf, + throwError as observableThrow, +} from 'rxjs'; -import { AuthEffects } from './auth.effects'; +import { + AppState, + storeModuleConfig, +} from '../../app.reducer'; +import { + authMethodsMock, + AuthServiceStub, +} from '../../shared/testing/auth-service.stub'; +import { EPersonMock } from '../../shared/testing/eperson.mock'; +import { StoreActionTypes } from '../../store.actions'; +import { AuthorizationDataService } from '../data/feature-authorization/authorization-data.service'; import { AuthActionTypes, AuthenticatedAction, @@ -25,17 +51,16 @@ import { RetrieveAuthMethodsAction, RetrieveAuthMethodsErrorAction, RetrieveAuthMethodsSuccessAction, - RetrieveTokenAction + RetrieveTokenAction, } from './auth.actions'; -import { authMethodsMock, AuthServiceStub } from '../../shared/testing/auth-service.stub'; -import { AuthService } from './auth.service'; +import { AuthEffects } from './auth.effects'; import { authReducer } from './auth.reducer'; +import { AuthService } from './auth.service'; import { AuthStatus } from './models/auth-status.model'; -import { EPersonMock } from '../../shared/testing/eperson.mock'; -import { AppState, storeModuleConfig } from '../../app.reducer'; -import { StoreActionTypes } from '../../store.actions'; -import { isAuthenticated, isAuthenticatedLoaded } from './selectors'; -import { AuthorizationDataService } from '../data/feature-authorization/authorization-data.service'; +import { + isAuthenticated, + isAuthenticatedLoaded, +} from './selectors'; describe('AuthEffects', () => { let authEffects: AuthEffects; @@ -56,9 +81,9 @@ describe('AuthEffects', () => { authenticated: false, loaded: false, loading: false, - authMethods: [] - } - } + authMethods: [], + }, + }, }; } @@ -66,7 +91,7 @@ describe('AuthEffects', () => { init(); TestBed.configureTestingModule({ imports: [ - StoreModule.forRoot({ auth: authReducer }, storeModuleConfig) + StoreModule.forRoot({ auth: authReducer }, storeModuleConfig), ], providers: [ AuthEffects, @@ -88,8 +113,8 @@ describe('AuthEffects', () => { actions = hot('--a-', { a: { type: AuthActionTypes.AUTHENTICATE, - payload: { email: 'user', password: 'password' } - } + payload: { email: 'user', password: 'password' }, + }, }); const expected = cold('--b-', { b: new AuthenticationSuccessAction(token) }); @@ -105,8 +130,8 @@ describe('AuthEffects', () => { actions = hot('--a-', { a: { type: AuthActionTypes.AUTHENTICATE, - payload: { email: 'user', password: 'wrongpassword' } - } + payload: { email: 'user', password: 'wrongpassword' }, + }, }); const expected = cold('--b-', { b: new AuthenticationErrorAction(new Error('Message Error test')) }); @@ -161,9 +186,9 @@ describe('AuthEffects', () => { type: AuthActionTypes.AUTHENTICATED_SUCCESS, payload: { authenticated: true, authToken: token, - userHref: EPersonMock._links.self.href - } - } + userHref: EPersonMock._links.self.href, + }, + }, }); const expected = cold('--b-', { b: new RetrieveAuthenticatedEpersonAction(EPersonMock._links.self.href) }); @@ -211,8 +236,8 @@ describe('AuthEffects', () => { spyOn((authEffects as any).authService, 'checkAuthenticationCookie').and.returnValue( observableOf( { - authenticated: true - }) + authenticated: true, + }), ); spyOn((authEffects as any).authService, 'setExternalAuthStatus'); actions = hot('--a-', { a: { type: AuthActionTypes.CHECK_AUTHENTICATION_TOKEN_COOKIE } }); @@ -230,7 +255,7 @@ describe('AuthEffects', () => { it('should return a RETRIEVE_AUTH_METHODS action in response to a CHECK_AUTHENTICATION_TOKEN_COOKIE action when authenticated is false', () => { spyOn((authEffects as any).authService, 'checkAuthenticationCookie').and.returnValue( observableOf( - { authenticated: false }) + { authenticated: false }), ); actions = hot('--a-', { a: { type: AuthActionTypes.CHECK_AUTHENTICATION_TOKEN_COOKIE } }); @@ -260,8 +285,8 @@ describe('AuthEffects', () => { actions = hot('--a-', { a: { type: AuthActionTypes.RETRIEVE_AUTHENTICATED_EPERSON, - payload: EPersonMock._links.self.href - } + payload: EPersonMock._links.self.href, + }, }); const expected = cold('--b-', { b: new RetrieveAuthenticatedEpersonSuccessAction(EPersonMock.id) }); @@ -314,8 +339,8 @@ describe('AuthEffects', () => { it('should return a AUTHENTICATE_SUCCESS action in response to a RETRIEVE_TOKEN action', () => { actions = hot('--a-', { a: { - type: AuthActionTypes.RETRIEVE_TOKEN - } + type: AuthActionTypes.RETRIEVE_TOKEN, + }, }); const expected = cold('--b-', { b: new AuthenticationSuccessAction(token) }); @@ -330,8 +355,8 @@ describe('AuthEffects', () => { actions = hot('--a-', { a: { - type: AuthActionTypes.RETRIEVE_TOKEN - } + type: AuthActionTypes.RETRIEVE_TOKEN, + }, }); const expected = cold('--b-', { b: new AuthenticationErrorAction(new Error('Message Error test')) }); diff --git a/src/app/core/auth/auth.effects.ts b/src/app/core/auth/auth.effects.ts index 281355b769e..2919a40fa85 100644 --- a/src/app/core/auth/auth.effects.ts +++ b/src/app/core/auth/auth.effects.ts @@ -1,27 +1,47 @@ -import { Injectable, NgZone } from '@angular/core'; - +import { + Injectable, + NgZone, + Type, +} from '@angular/core'; +// import @ngrx +import { + Actions, + createEffect, + ofType, +} from '@ngrx/effects'; +import { + Action, + select, + Store, +} from '@ngrx/store'; import { asyncScheduler, combineLatest as observableCombineLatest, Observable, of as observableOf, queueScheduler, - timer + timer, } from 'rxjs'; -import { catchError, filter, map, observeOn, switchMap, take, tap } from 'rxjs/operators'; -// import @ngrx -import { Actions, createEffect, ofType } from '@ngrx/effects'; -import { Action, select, Store } from '@ngrx/store'; +import { + catchError, + filter, + map, + observeOn, + switchMap, + take, + tap, +} from 'rxjs/operators'; -// import services -import { AuthService } from './auth.service'; -import { EPerson } from '../eperson/models/eperson.model'; -import { AuthStatus } from './models/auth-status.model'; -import { AuthTokenInfo } from './models/auth-token-info.model'; +import { environment } from '../../../environments/environment'; import { AppState } from '../../app.reducer'; -import { isAuthenticated, isAuthenticatedLoaded } from './selectors'; +import { hasValue } from '../../shared/empty.util'; +import { NotificationsActionTypes } from '../../shared/notifications/notifications.actions'; import { StoreActionTypes } from '../../store.actions'; -import { AuthMethod } from './models/auth.method'; +import { AuthorizationDataService } from '../data/feature-authorization/authorization-data.service'; +import { RequestActionTypes } from '../data/request.actions'; +import { EPerson } from '../eperson/models/eperson.model'; +import { EnterZoneScheduler } from '../utilities/enter-zone.scheduler'; +import { LeaveZoneScheduler } from '../utilities/leave-zone.scheduler'; // import actions import { AuthActionTypes, @@ -31,6 +51,7 @@ import { AuthenticatedSuccessAction, AuthenticationErrorAction, AuthenticationSuccessAction, + AuthErrorActionsWithErrorPayload, CheckAuthenticationTokenCookieAction, LogOutErrorAction, LogOutSuccessAction, @@ -45,20 +66,32 @@ import { RetrieveAuthMethodsErrorAction, RetrieveAuthMethodsSuccessAction, RetrieveTokenAction, - SetUserAsIdleAction + SetUserAsIdleAction, } from './auth.actions'; -import { hasValue } from '../../shared/empty.util'; -import { environment } from '../../../environments/environment'; -import { RequestActionTypes } from '../data/request.actions'; -import { NotificationsActionTypes } from '../../shared/notifications/notifications.actions'; -import { LeaveZoneScheduler } from '../utilities/leave-zone.scheduler'; -import { EnterZoneScheduler } from '../utilities/enter-zone.scheduler'; -import { AuthorizationDataService } from '../data/feature-authorization/authorization-data.service'; +// import services +import { AuthService } from './auth.service'; +import { AuthMethod } from './models/auth.method'; +import { AuthStatus } from './models/auth-status.model'; +import { AuthTokenInfo } from './models/auth-token-info.model'; +import { + isAuthenticated, + isAuthenticatedLoaded, +} from './selectors'; // Action Types that do not break/prevent the user from an idle state const IDLE_TIMER_IGNORE_TYPES: string[] = [...Object.values(AuthActionTypes).filter((t: string) => t !== AuthActionTypes.UNSET_USER_AS_IDLE), - ...Object.values(RequestActionTypes), ...Object.values(NotificationsActionTypes)]; + ...Object.values(RequestActionTypes), ...Object.values(NotificationsActionTypes)]; + +export function errorToAuthAction$(actionType: Type, error: unknown): Observable { + if (error instanceof Error) { + return observableOf(new actionType(error)); + } + + // If we caught something that's not an Error: complain & drop type safety + console.warn('AuthEffects caught non-Error object:', error); + return observableOf(new actionType(error as any)); +} @Injectable() export class AuthEffects { @@ -73,14 +106,14 @@ export class AuthEffects { return this.authService.authenticate(action.payload.email, action.payload.password).pipe( take(1), map((response: AuthStatus) => new AuthenticationSuccessAction(response.token)), - catchError((error) => observableOf(new AuthenticationErrorAction(error))) + catchError((error: unknown) => errorToAuthAction$(AuthenticationErrorAction, error)), ); - }) + }), )); public authenticateSuccess$: Observable = createEffect(() => this.actions$.pipe( ofType(AuthActionTypes.AUTHENTICATE_SUCCESS), - map((action: AuthenticationSuccessAction) => new AuthenticatedAction(action.payload)) + map((action: AuthenticationSuccessAction) => new AuthenticatedAction(action.payload)), )); public authenticated$: Observable = createEffect(() => this.actions$.pipe( @@ -88,8 +121,9 @@ export class AuthEffects { switchMap((action: AuthenticatedAction) => { return this.authService.authenticatedUser(action.payload).pipe( map((userHref: string) => new AuthenticatedSuccessAction((userHref !== null), action.payload, userHref)), - catchError((error) => observableOf(new AuthenticatedErrorAction(error))),); - }) + catchError((error: unknown) => errorToAuthAction$(AuthenticatedErrorAction, error)), + ); + }), )); public authenticatedSuccess$: Observable = createEffect(() => this.actions$.pipe( @@ -97,7 +131,7 @@ export class AuthEffects { tap((action: AuthenticatedSuccessAction) => this.authService.storeToken(action.payload.authToken)), switchMap((action: AuthenticatedSuccessAction) => this.authService.getRedirectUrl().pipe( take(1), - map((redirectUrl: string) => [action, redirectUrl]) + map((redirectUrl: string) => [action, redirectUrl]), )), map(([action, redirectUrl]: [AuthenticatedSuccessAction, string]) => { if (hasValue(redirectUrl)) { @@ -105,7 +139,7 @@ export class AuthEffects { } else { return new RetrieveAuthenticatedEpersonAction(action.payload.userHref); } - }) + }), )); public redirectAfterLoginSuccess$: Observable = createEffect(() => this.actions$.pipe( @@ -113,13 +147,13 @@ export class AuthEffects { tap((action: RedirectAfterLoginSuccessAction) => { this.authService.clearRedirectUrl(); this.authService.navigateToRedirectUrl(action.payload); - }) + }), ), { dispatch: false }); // It means "reacts to this action but don't send another" public authenticatedError$: Observable = createEffect(() => this.actions$.pipe( ofType(AuthActionTypes.AUTHENTICATED_ERROR), - tap((action: LogOutSuccessAction) => this.authService.removeToken()) + tap((action: LogOutSuccessAction) => this.authService.removeToken()), ), { dispatch: false }); public retrieveAuthenticatedEperson$: Observable = createEffect(() => this.actions$.pipe( @@ -134,17 +168,18 @@ export class AuthEffects { } return user$.pipe( map((user: EPerson) => new RetrieveAuthenticatedEpersonSuccessAction(user.id)), - catchError((error) => observableOf(new RetrieveAuthenticatedEpersonErrorAction(error)))); - }) + catchError((error: unknown) => errorToAuthAction$(RetrieveAuthenticatedEpersonErrorAction, error)), + ); + }), )); public checkToken$: Observable = createEffect(() => this.actions$.pipe(ofType(AuthActionTypes.CHECK_AUTHENTICATION_TOKEN), switchMap(() => { return this.authService.hasValidAuthenticationToken().pipe( map((token: AuthTokenInfo) => new AuthenticatedAction(token)), - catchError((error) => observableOf(new CheckAuthenticationTokenCookieAction())) + catchError((error: unknown) => observableOf(new CheckAuthenticationTokenCookieAction())), ); - }) + }), )); public checkTokenCookie$: Observable = createEffect(() => this.actions$.pipe( @@ -160,9 +195,9 @@ export class AuthEffects { return new RetrieveAuthMethodsAction(response); } }), - catchError((error) => observableOf(new AuthenticatedErrorAction(error))) + catchError((error: unknown) => errorToAuthAction$(AuthenticatedErrorAction, error)), ); - }) + }), )); public retrieveToken$: Observable = createEffect(() => this.actions$.pipe( @@ -171,24 +206,24 @@ export class AuthEffects { return this.authService.refreshAuthenticationToken(null).pipe( take(1), map((token: AuthTokenInfo) => new AuthenticationSuccessAction(token)), - catchError((error) => observableOf(new AuthenticationErrorAction(error))) + catchError((error: unknown) => errorToAuthAction$(AuthenticationErrorAction, error)), ); - }) + }), )); public refreshToken$: Observable = createEffect(() => this.actions$.pipe(ofType(AuthActionTypes.REFRESH_TOKEN), switchMap((action: RefreshTokenAction) => { return this.authService.refreshAuthenticationToken(action.payload).pipe( map((token: AuthTokenInfo) => new RefreshTokenSuccessAction(token)), - catchError((error) => observableOf(new RefreshTokenErrorAction())) + catchError((error: unknown) => observableOf(new RefreshTokenErrorAction())), ); - }) + }), )); // It means "reacts to this action but don't send another" public refreshTokenSuccess$: Observable = createEffect(() => this.actions$.pipe( ofType(AuthActionTypes.REFRESH_TOKEN_SUCCESS), - tap((action: RefreshTokenSuccessAction) => this.authService.replaceToken(action.payload)) + tap((action: RefreshTokenSuccessAction) => this.authService.replaceToken(action.payload)), ), { dispatch: false }); /** @@ -204,7 +239,7 @@ export class AuthEffects { take(1), filter(([loaded, authenticated]) => loaded && !authenticated), tap(() => this.authService.removeToken()), - tap(() => this.authService.resetAuthenticationError()) + tap(() => this.authService.resetAuthenticationError()), ); })), { dispatch: false }); @@ -213,9 +248,9 @@ export class AuthEffects { * authorizations endpoint, to be sure to have consistent responses after a login with external idp * */ - invalidateAuthorizationsRequestCache$ = createEffect(() => this.actions$ + invalidateAuthorizationsRequestCache$ = createEffect(() => this.actions$ .pipe(ofType(StoreActionTypes.REHYDRATE), - tap(() => this.authorizationsService.invalidateAuthorizationsRequestCache()) + tap(() => this.authorizationsService.invalidateAuthorizationsRequestCache()), ), { dispatch: false }); public logOut$: Observable = createEffect(() => this.actions$ @@ -224,24 +259,24 @@ export class AuthEffects { switchMap(() => { this.authService.stopImpersonating(); return this.authService.logout().pipe( - map((value) => new LogOutSuccessAction()), - catchError((error) => observableOf(new LogOutErrorAction(error))) + map(() => new LogOutSuccessAction()), + catchError((error: unknown) => errorToAuthAction$(LogOutErrorAction, error)), ); - }) + }), )); public logOutSuccess$: Observable = createEffect(() => this.actions$ .pipe(ofType(AuthActionTypes.LOG_OUT_SUCCESS), tap(() => this.authService.removeToken()), tap(() => this.authService.clearRedirectUrl()), - tap(() => this.authService.refreshAfterLogout()) + tap(() => this.authService.refreshAfterLogout()), ), { dispatch: false }); public redirectToLoginTokenExpired$: Observable = createEffect(() => this.actions$ .pipe( ofType(AuthActionTypes.REDIRECT_TOKEN_EXPIRED), tap(() => this.authService.removeToken()), - tap(() => this.authService.redirectToLoginWhenTokenExpired()) + tap(() => this.authService.redirectToLoginWhenTokenExpired()), ), { dispatch: false }); public retrieveMethods$: Observable = createEffect(() => this.actions$ @@ -251,9 +286,9 @@ export class AuthEffects { return this.authService.retrieveAuthMethodsFromAuthStatus(action.payload) .pipe( map((authMethodModels: AuthMethod[]) => new RetrieveAuthMethodsSuccessAction(authMethodModels)), - catchError((error) => observableOf(new RetrieveAuthMethodsErrorAction())) + catchError(() => observableOf(new RetrieveAuthMethodsErrorAction())), ); - }) + }), )); /** @@ -268,7 +303,7 @@ export class AuthEffects { // in, and start a new timer switchMap(() => // Start a timer outside of Angular's zone - timer(environment.auth.ui.timeUntilIdle, new LeaveZoneScheduler(this.zone, asyncScheduler)) + timer(environment.auth.ui.timeUntilIdle, new LeaveZoneScheduler(this.zone, asyncScheduler)), ), // Re-enter the zone to dispatch the action observeOn(new EnterZoneScheduler(this.zone, queueScheduler)), diff --git a/src/app/core/auth/auth.interceptor.spec.ts b/src/app/core/auth/auth.interceptor.spec.ts index 04bbc4acaf0..d824df472a2 100644 --- a/src/app/core/auth/auth.interceptor.spec.ts +++ b/src/app/core/auth/auth.interceptor.spec.ts @@ -1,18 +1,20 @@ -import { TestBed } from '@angular/core/testing'; -import { HttpClientTestingModule, HttpTestingController, } from '@angular/common/http/testing'; import { HTTP_INTERCEPTORS } from '@angular/common/http'; +import { + HttpClientTestingModule, + HttpTestingController, +} from '@angular/common/http/testing'; +import { TestBed } from '@angular/core/testing'; import { Router } from '@angular/router'; - import { Store } from '@ngrx/store'; import { of as observableOf } from 'rxjs'; -import { AuthInterceptor } from './auth.interceptor'; -import { AuthService } from './auth.service'; -import { DspaceRestService } from '../dspace-rest/dspace-rest.service'; +import { AuthServiceStub } from '../../shared/testing/auth-service.stub'; import { RouterStub } from '../../shared/testing/router.stub'; import { TruncatablesState } from '../../shared/truncatable/truncatable.reducer'; -import { AuthServiceStub } from '../../shared/testing/auth-service.stub'; import { RestRequestMethod } from '../data/rest-request-method'; +import { DspaceRestService } from '../dspace-rest/dspace-rest.service'; +import { AuthInterceptor } from './auth.interceptor'; +import { AuthService } from './auth.service'; describe(`AuthInterceptor`, () => { let service: DspaceRestService; @@ -20,10 +22,8 @@ describe(`AuthInterceptor`, () => { const authServiceStub = new AuthServiceStub(); const store: Store = jasmine.createSpyObj('store', { - /* eslint-disable no-empty,@typescript-eslint/no-empty-function */ dispatch: {}, - /* eslint-enable no-empty, @typescript-eslint/no-empty-function */ - select: observableOf(true) + select: observableOf(true), }); beforeEach(() => { @@ -46,6 +46,10 @@ describe(`AuthInterceptor`, () => { httpMock = TestBed.inject(HttpTestingController); }); + afterEach(() => { + httpMock.verify(); + }); + describe('when has a valid token', () => { it('should not add an Authorization header when we’re sending a HTTP request to \'authn\' endpoint that is not the logout endpoint', () => { @@ -95,14 +99,11 @@ describe(`AuthInterceptor`, () => { }); it('should redirect to login', () => { - - service.request(RestRequestMethod.POST, 'dspace-spring-rest/api/submission/workspaceitems', 'password=password&user=user').subscribe((response) => { - expect(response).toBeTruthy(); - }); - service.request(RestRequestMethod.POST, 'dspace-spring-rest/api/submission/workspaceitems', 'password=password&user=user'); httpMock.expectNone('dspace-spring-rest/api/submission/workspaceitems'); + // HttpTestingController.expectNone will throw an error when a requests is made + expect().nothing(); }); }); diff --git a/src/app/core/auth/auth.interceptor.ts b/src/app/core/auth/auth.interceptor.ts index 672879f436f..84d4c532101 100644 --- a/src/app/core/auth/auth.interceptor.ts +++ b/src/app/core/auth/auth.interceptor.ts @@ -1,7 +1,3 @@ -import { Observable, of as observableOf, throwError as observableThrowError } from 'rxjs'; - -import { catchError, map } from 'rxjs/operators'; -import { Injectable, Injector } from '@angular/core'; import { HttpErrorResponse, HttpEvent, @@ -10,19 +6,36 @@ import { HttpInterceptor, HttpRequest, HttpResponse, - HttpResponseBase + HttpResponseBase, } from '@angular/common/http'; +import { + Injectable, + Injector, +} from '@angular/core'; +import { Router } from '@angular/router'; +import { Store } from '@ngrx/store'; +import { + Observable, + of as observableOf, + throwError as observableThrowError, +} from 'rxjs'; +import { + catchError, + map, +} from 'rxjs/operators'; import { AppState } from '../../app.reducer'; -import { AuthService } from './auth.service'; -import { AuthStatus } from './models/auth-status.model'; -import { AuthTokenInfo } from './models/auth-token-info.model'; -import { hasValue, isNotEmpty, isNotNull } from '../../shared/empty.util'; +import { + hasValue, + isNotEmpty, + isNotNull, +} from '../../shared/empty.util'; import { RedirectWhenTokenExpiredAction } from './auth.actions'; -import { Store } from '@ngrx/store'; -import { Router } from '@angular/router'; +import { AuthService } from './auth.service'; import { AuthMethod } from './models/auth.method'; import { AuthMethodType } from './models/auth.method-type'; +import { AuthStatus } from './models/auth-status.model'; +import { AuthTokenInfo } from './models/auth-token-info.model'; @Injectable() export class AuthInterceptor implements HttpInterceptor { @@ -152,12 +165,12 @@ export class AuthInterceptor implements HttpInterceptor { let authMethodModel: AuthMethod; if (splittedRealm.length === 1) { - authMethodModel = new AuthMethod(methodName); + authMethodModel = new AuthMethod(methodName, Number(j)); authMethodModels.push(authMethodModel); } else if (splittedRealm.length > 1) { let location = splittedRealm[1]; location = this.parseLocation(location); - authMethodModel = new AuthMethod(methodName, location); + authMethodModel = new AuthMethod(methodName, Number(j), location); authMethodModels.push(authMethodModel); } } @@ -165,7 +178,7 @@ export class AuthInterceptor implements HttpInterceptor { // make sure the email + password login component gets rendered first authMethodModels = this.sortAuthMethods(authMethodModels); } else { - authMethodModels.push(new AuthMethod(AuthMethodType.Password)); + authMethodModels.push(new AuthMethod(AuthMethodType.Password, 0)); } return authMethodModels; @@ -207,8 +220,8 @@ export class AuthInterceptor implements HttpInterceptor { message: 'Unknown auth error', status: 500, timestamp: Date.now(), - path: '' - }; + path: '', + }; } } else { authStatus.error = error; @@ -261,7 +274,7 @@ export class AuthInterceptor implements HttpInterceptor { // login successfully const newToken = response.headers.get('authorization'); authRes = response.clone({ - body: this.makeAuthStatusObject(true, newToken) + body: this.makeAuthStatusObject(true, newToken), }); // clean eventually refresh Requests list @@ -269,13 +282,13 @@ export class AuthInterceptor implements HttpInterceptor { } else if (this.isStatusResponse(response)) { authRes = response.clone({ body: Object.assign(response.body, { - authMethods: this.parseAuthMethodsFromHeaders(response.headers) - }) + authMethods: this.parseAuthMethodsFromHeaders(response.headers), + }), }); } else { // logout successfully authRes = response.clone({ - body: this.makeAuthStatusObject(false) + body: this.makeAuthStatusObject(false), }); } return authRes; @@ -283,7 +296,7 @@ export class AuthInterceptor implements HttpInterceptor { return response; } }), - catchError((error, caught) => { + catchError((error: unknown, caught) => { // Intercept an error response if (error instanceof HttpErrorResponse) { @@ -298,7 +311,7 @@ export class AuthInterceptor implements HttpInterceptor { headers: error.headers, status: error.status, statusText: error.statusText, - url: error.url + url: error.url, }); return observableOf(authResponse); } else if (this.isUnauthorized(error) && isNotNull(token) && authService.isTokenExpired()) { diff --git a/src/app/core/auth/auth.reducer.spec.ts b/src/app/core/auth/auth.reducer.spec.ts index c0619adf793..7860744aa5b 100644 --- a/src/app/core/auth/auth.reducer.spec.ts +++ b/src/app/core/auth/auth.reducer.spec.ts @@ -1,4 +1,4 @@ -import { authReducer, AuthState } from './auth.reducer'; +import { EPersonMock } from '../../shared/testing/eperson.mock'; import { AddAuthenticationMessageAction, AuthenticateAction, @@ -8,7 +8,6 @@ import { AuthenticationErrorAction, AuthenticationSuccessAction, CheckAuthenticationTokenAction, - SetAuthCookieStatus, CheckAuthenticationTokenCookieAction, LogOutAction, LogOutErrorAction, @@ -24,15 +23,19 @@ import { RetrieveAuthMethodsAction, RetrieveAuthMethodsErrorAction, RetrieveAuthMethodsSuccessAction, + SetAuthCookieStatus, SetRedirectUrlAction, SetUserAsIdleAction, - UnsetUserAsIdleAction + UnsetUserAsIdleAction, } from './auth.actions'; -import { AuthTokenInfo } from './models/auth-token-info.model'; -import { EPersonMock } from '../../shared/testing/eperson.mock'; -import { AuthStatus } from './models/auth-status.model'; +import { + authReducer, + AuthState, +} from './auth.reducer'; import { AuthMethod } from './models/auth.method'; import { AuthMethodType } from './models/auth.method-type'; +import { AuthStatus } from './models/auth-status.model'; +import { AuthTokenInfo } from './models/auth-token-info.model'; describe('authReducer', () => { @@ -47,7 +50,7 @@ describe('authReducer', () => { loaded: false, blocking: true, loading: false, - idle: false + idle: false, }; const action = new AuthenticateAction('user', 'password'); const newState = authReducer(initialState, action); @@ -58,7 +61,7 @@ describe('authReducer', () => { error: undefined, loading: true, info: undefined, - idle: false + idle: false, }; expect(newState).toEqual(state); @@ -72,7 +75,7 @@ describe('authReducer', () => { blocking: true, loading: true, info: undefined, - idle: false + idle: false, }; const action = new AuthenticationSuccessAction(mockTokenInfo); const newState = authReducer(initialState, action); @@ -88,7 +91,7 @@ describe('authReducer', () => { blocking: true, loading: true, info: undefined, - idle: false + idle: false, }; const action = new AuthenticationErrorAction(mockError); const newState = authReducer(initialState, action); @@ -100,7 +103,7 @@ describe('authReducer', () => { info: undefined, authToken: undefined, error: 'Test error message', - idle: false + idle: false, }; expect(newState).toEqual(state); @@ -114,7 +117,7 @@ describe('authReducer', () => { error: undefined, loading: true, info: undefined, - idle: false + idle: false, }; const action = new AuthenticatedAction(mockTokenInfo); const newState = authReducer(initialState, action); @@ -125,7 +128,7 @@ describe('authReducer', () => { error: undefined, loading: true, info: undefined, - idle: false + idle: false, }; expect(newState).toEqual(state); }); @@ -138,7 +141,7 @@ describe('authReducer', () => { blocking: true, loading: true, info: undefined, - idle: false + idle: false, }; const action = new AuthenticatedSuccessAction(true, mockTokenInfo, EPersonMock._links.self.href); const newState = authReducer(initialState, action); @@ -150,7 +153,7 @@ describe('authReducer', () => { blocking: true, loading: true, info: undefined, - idle: false + idle: false, }; expect(newState).toEqual(state); }); @@ -163,7 +166,7 @@ describe('authReducer', () => { blocking: true, loading: true, info: undefined, - idle: false + idle: false, }; const action = new AuthenticatedErrorAction(mockError); const newState = authReducer(initialState, action); @@ -175,7 +178,7 @@ describe('authReducer', () => { blocking: false, loading: false, info: undefined, - idle: false + idle: false, }; expect(newState).toEqual(state); }); @@ -186,7 +189,7 @@ describe('authReducer', () => { loaded: false, blocking: false, loading: false, - idle: false + idle: false, }; const action = new CheckAuthenticationTokenAction(); const newState = authReducer(initialState, action); @@ -195,7 +198,7 @@ describe('authReducer', () => { loaded: false, blocking: false, loading: true, - idle: false + idle: false, }; expect(newState).toEqual(state); }); @@ -206,7 +209,7 @@ describe('authReducer', () => { loaded: false, blocking: false, loading: true, - idle: false + idle: false, }; const action = new CheckAuthenticationTokenCookieAction(); const newState = authReducer(initialState, action); @@ -215,7 +218,7 @@ describe('authReducer', () => { loaded: false, blocking: false, loading: true, - idle: false + idle: false, }; expect(newState).toEqual(state); }); @@ -227,7 +230,7 @@ describe('authReducer', () => { blocking: false, loading: true, externalAuth: false, - idle: false + idle: false, }; const action = new SetAuthCookieStatus(true); const newState = authReducer(initialState, action); @@ -237,7 +240,7 @@ describe('authReducer', () => { blocking: false, loading: true, externalAuth: true, - idle: false + idle: false, }; expect(newState).toEqual(state); }); @@ -252,7 +255,7 @@ describe('authReducer', () => { loading: false, info: undefined, userId: EPersonMock.id, - idle: false + idle: false, }; const action = new LogOutAction(); @@ -271,7 +274,7 @@ describe('authReducer', () => { loading: false, info: undefined, userId: EPersonMock.id, - idle: false + idle: false, }; const action = new LogOutSuccessAction(); @@ -286,7 +289,7 @@ describe('authReducer', () => { info: undefined, refreshing: false, userId: undefined, - idle: false + idle: false, }; expect(newState).toEqual(state); }); @@ -301,7 +304,7 @@ describe('authReducer', () => { loading: false, info: undefined, userId: EPersonMock.id, - idle: false + idle: false, }; const action = new LogOutErrorAction(mockError); @@ -315,7 +318,7 @@ describe('authReducer', () => { loading: false, info: undefined, userId: EPersonMock.id, - idle: false + idle: false, }; expect(newState).toEqual(state); }); @@ -329,7 +332,7 @@ describe('authReducer', () => { blocking: true, loading: true, info: undefined, - idle: false + idle: false, }; const action = new RetrieveAuthenticatedEpersonSuccessAction(EPersonMock.id); const newState = authReducer(initialState, action); @@ -342,7 +345,7 @@ describe('authReducer', () => { loading: false, info: undefined, userId: EPersonMock.id, - idle: false + idle: false, }; expect(newState).toEqual(state); }); @@ -355,7 +358,7 @@ describe('authReducer', () => { blocking: true, loading: true, info: undefined, - idle: false + idle: false, }; const action = new RetrieveAuthenticatedEpersonErrorAction(mockError); const newState = authReducer(initialState, action); @@ -367,7 +370,7 @@ describe('authReducer', () => { blocking: false, loading: false, info: undefined, - idle: false + idle: false, }; expect(newState).toEqual(state); }); @@ -382,7 +385,7 @@ describe('authReducer', () => { loading: false, info: undefined, userId: EPersonMock.id, - idle: false + idle: false, }; const newTokenInfo = new AuthTokenInfo('Refreshed token'); const action = new RefreshTokenAction(newTokenInfo); @@ -397,7 +400,7 @@ describe('authReducer', () => { info: undefined, userId: EPersonMock.id, refreshing: true, - idle: false + idle: false, }; expect(newState).toEqual(state); }); @@ -413,7 +416,7 @@ describe('authReducer', () => { info: undefined, userId: EPersonMock.id, refreshing: true, - idle: false + idle: false, }; const newTokenInfo = new AuthTokenInfo('Refreshed token'); const action = new RefreshTokenSuccessAction(newTokenInfo); @@ -428,7 +431,7 @@ describe('authReducer', () => { info: undefined, userId: EPersonMock.id, refreshing: false, - idle: false + idle: false, }; expect(newState).toEqual(state); }); @@ -444,7 +447,7 @@ describe('authReducer', () => { info: undefined, userId: EPersonMock.id, refreshing: true, - idle: false + idle: false, }; const action = new RefreshTokenErrorAction(); const newState = authReducer(initialState, action); @@ -458,7 +461,7 @@ describe('authReducer', () => { info: undefined, refreshing: false, userId: undefined, - idle: false + idle: false, }; expect(newState).toEqual(state); }); @@ -473,7 +476,7 @@ describe('authReducer', () => { loading: false, info: undefined, userId: EPersonMock.id, - idle: false + idle: false, }; state = { @@ -485,7 +488,7 @@ describe('authReducer', () => { error: undefined, info: 'Message', userId: undefined, - idle: false + idle: false, }; }); @@ -507,7 +510,7 @@ describe('authReducer', () => { loaded: false, blocking: false, loading: false, - idle: false + idle: false, }; const action = new AddAuthenticationMessageAction('Message'); const newState = authReducer(initialState, action); @@ -517,7 +520,7 @@ describe('authReducer', () => { blocking: false, loading: false, info: 'Message', - idle: false + idle: false, }; expect(newState).toEqual(state); }); @@ -530,7 +533,7 @@ describe('authReducer', () => { loading: false, error: 'Error', info: 'Message', - idle: false + idle: false, }; const action = new ResetAuthenticationMessagesAction(); const newState = authReducer(initialState, action); @@ -541,7 +544,7 @@ describe('authReducer', () => { loading: false, error: undefined, info: undefined, - idle: false + idle: false, }; expect(newState).toEqual(state); }); @@ -552,7 +555,7 @@ describe('authReducer', () => { loaded: false, blocking: false, loading: false, - idle: false + idle: false, }; const action = new SetRedirectUrlAction('redirect.url'); const newState = authReducer(initialState, action); @@ -562,7 +565,7 @@ describe('authReducer', () => { blocking: false, loading: false, redirectUrl: 'redirect.url', - idle: false + idle: false, }; expect(newState).toEqual(state); }); @@ -574,7 +577,7 @@ describe('authReducer', () => { blocking: false, loading: false, authMethods: [], - idle: false + idle: false, }; const action = new RetrieveAuthMethodsAction(new AuthStatus()); const newState = authReducer(initialState, action); @@ -584,7 +587,7 @@ describe('authReducer', () => { blocking: false, loading: true, authMethods: [], - idle: false + idle: false, }; expect(newState).toEqual(state); }); @@ -596,11 +599,11 @@ describe('authReducer', () => { blocking: true, loading: true, authMethods: [], - idle: false + idle: false, }; - const authMethods = [ - new AuthMethod(AuthMethodType.Password), - new AuthMethod(AuthMethodType.Shibboleth, 'location') + const authMethods: AuthMethod[] = [ + new AuthMethod(AuthMethodType.Password, 0), + new AuthMethod(AuthMethodType.Shibboleth, 1, 'location'), ]; const action = new RetrieveAuthMethodsSuccessAction(authMethods); const newState = authReducer(initialState, action); @@ -610,7 +613,7 @@ describe('authReducer', () => { blocking: false, loading: false, authMethods: authMethods, - idle: false + idle: false, }; expect(newState).toEqual(state); }); @@ -622,7 +625,7 @@ describe('authReducer', () => { blocking: true, loading: true, authMethods: [], - idle: false + idle: false, }; const action = new RetrieveAuthMethodsErrorAction(); @@ -632,8 +635,8 @@ describe('authReducer', () => { loaded: false, blocking: false, loading: false, - authMethods: [new AuthMethod(AuthMethodType.Password)], - idle: false + authMethods: [new AuthMethod(AuthMethodType.Password, 0)], + idle: false, }; expect(newState).toEqual(state); }); @@ -644,7 +647,7 @@ describe('authReducer', () => { loaded: true, blocking: false, loading: false, - idle: false + idle: false, }; const action = new SetUserAsIdleAction(); @@ -654,7 +657,7 @@ describe('authReducer', () => { loaded: true, blocking: false, loading: false, - idle: true + idle: true, }; expect(newState).toEqual(state); }); @@ -665,7 +668,7 @@ describe('authReducer', () => { loaded: true, blocking: false, loading: false, - idle: true + idle: true, }; const action = new UnsetUserAsIdleAction(); @@ -675,7 +678,7 @@ describe('authReducer', () => { loaded: true, blocking: false, loading: false, - idle: false + idle: false, }; expect(newState).toEqual(state); }); diff --git a/src/app/core/auth/auth.reducer.ts b/src/app/core/auth/auth.reducer.ts index ba9c41326a6..8a399710eae 100644 --- a/src/app/core/auth/auth.reducer.ts +++ b/src/app/core/auth/auth.reducer.ts @@ -1,4 +1,5 @@ // import actions +import { StoreActionTypes } from '../../store.actions'; import { AddAuthenticationMessageAction, AuthActions, @@ -10,14 +11,14 @@ import { RedirectWhenTokenExpiredAction, RefreshTokenSuccessAction, RetrieveAuthenticatedEpersonSuccessAction, - RetrieveAuthMethodsSuccessAction, SetAuthCookieStatus, - SetRedirectUrlAction + RetrieveAuthMethodsSuccessAction, + SetAuthCookieStatus, + SetRedirectUrlAction, } from './auth.actions'; -// import models -import { AuthTokenInfo } from './models/auth-token-info.model'; import { AuthMethod } from './models/auth.method'; import { AuthMethodType } from './models/auth.method-type'; -import { StoreActionTypes } from '../../store.actions'; +// import models +import { AuthTokenInfo } from './models/auth-token-info.model'; /** * The auth state. @@ -76,7 +77,7 @@ const initialState: AuthState = { loading: false, authMethods: [], externalAuth: false, - idle: false + idle: false, }; /** @@ -92,13 +93,13 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut return Object.assign({}, state, { error: undefined, loading: true, - info: undefined + info: undefined, }); case AuthActionTypes.AUTHENTICATED: return Object.assign({}, state, { loading: true, - blocking: true + blocking: true, }); case AuthActionTypes.CHECK_AUTHENTICATION_TOKEN: @@ -109,7 +110,7 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut case AuthActionTypes.SET_AUTH_COOKIE_STATUS: return Object.assign({}, state, { - externalAuth: (action as SetAuthCookieStatus).payload + externalAuth: (action as SetAuthCookieStatus).payload, }); case AuthActionTypes.AUTHENTICATED_ERROR: @@ -120,13 +121,13 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut error: (action as AuthenticationErrorAction).payload.message, loaded: true, blocking: false, - loading: false + loading: false, }); case AuthActionTypes.AUTHENTICATED_SUCCESS: return Object.assign({}, state, { authenticated: true, - authToken: (action as AuthenticatedSuccessAction).payload.authToken + authToken: (action as AuthenticatedSuccessAction).payload.authToken, }); case AuthActionTypes.RETRIEVE_AUTHENTICATED_EPERSON_SUCCESS: @@ -136,7 +137,7 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut loading: false, blocking: false, info: undefined, - userId: (action as RetrieveAuthenticatedEpersonSuccessAction).payload + userId: (action as RetrieveAuthenticatedEpersonSuccessAction).payload, }); case AuthActionTypes.AUTHENTICATE_ERROR: @@ -145,7 +146,7 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut authToken: undefined, error: (action as AuthenticationErrorAction).payload.message, blocking: false, - loading: false + loading: false, }); case AuthActionTypes.AUTHENTICATE_SUCCESS: @@ -155,7 +156,7 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut case AuthActionTypes.LOG_OUT_ERROR: return Object.assign({}, state, { authenticated: true, - error: (action as LogOutErrorAction).payload.message + error: (action as LogOutErrorAction).payload.message, }); case AuthActionTypes.REFRESH_TOKEN_ERROR: @@ -168,7 +169,7 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut loading: false, info: undefined, refreshing: false, - userId: undefined + userId: undefined, }); case AuthActionTypes.LOG_OUT_SUCCESS: @@ -181,7 +182,7 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut loading: true, info: undefined, refreshing: false, - userId: undefined + userId: undefined, }); case AuthActionTypes.REDIRECT_AUTHENTICATION_REQUIRED: @@ -193,7 +194,7 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut blocking: false, loading: false, info: (action as RedirectWhenTokenExpiredAction as RedirectWhenAuthenticationIsRequiredAction).payload, - userId: undefined + userId: undefined, }); case AuthActionTypes.REFRESH_TOKEN: @@ -205,7 +206,7 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut return Object.assign({}, state, { authToken: (action as RefreshTokenSuccessAction).payload, refreshing: false, - blocking: false + blocking: false, }); case AuthActionTypes.ADD_MESSAGE: @@ -229,14 +230,14 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut return Object.assign({}, state, { loading: false, blocking: false, - authMethods: (action as RetrieveAuthMethodsSuccessAction).payload + authMethods: (action as RetrieveAuthMethodsSuccessAction).payload, }); case AuthActionTypes.RETRIEVE_AUTH_METHODS_ERROR: return Object.assign({}, state, { loading: false, blocking: false, - authMethods: [new AuthMethod(AuthMethodType.Password)] + authMethods: [new AuthMethod(AuthMethodType.Password, 0)], }); case AuthActionTypes.SET_REDIRECT_URL: diff --git a/src/app/core/auth/auth.service.spec.ts b/src/app/core/auth/auth.service.spec.ts index b38d17aecdb..66c80b9bf5a 100644 --- a/src/app/core/auth/auth.service.spec.ts +++ b/src/app/core/auth/auth.service.spec.ts @@ -1,46 +1,75 @@ -import { inject, TestBed, waitForAsync } from '@angular/core/testing'; import { CommonModule } from '@angular/common'; -import { ActivatedRoute, Router } from '@angular/router'; -import { Store, StoreModule } from '@ngrx/store'; -import { REQUEST } from '@nguniversal/express-engine/tokens'; -import { Observable, of as observableOf } from 'rxjs'; -import { authReducer, AuthState } from './auth.reducer'; -import { NativeWindowRef, NativeWindowService } from '../services/window.service'; -import { AuthService, IMPERSONATING_COOKIE } from './auth.service'; -import { RouterStub } from '../../shared/testing/router.stub'; +import { + inject, + TestBed, + waitForAsync, +} from '@angular/core/testing'; +import { + ActivatedRoute, + Router, +} from '@angular/router'; +import { + Store, + StoreModule, +} from '@ngrx/store'; +import { TranslateService } from '@ngx-translate/core'; +import { cold } from 'jasmine-marbles'; +import { + Observable, + 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'; +import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; import { ActivatedRouteStub } from '../../shared/testing/active-router.stub'; -import { CookieService } from '../services/cookie.service'; import { AuthRequestServiceStub } from '../../shared/testing/auth-request-service.stub'; -import { AuthRequestService } from './auth-request.service'; -import { AuthStatus } from './models/auth-status.model'; -import { AuthTokenInfo } from './models/auth-token-info.model'; -import { EPerson } from '../eperson/models/eperson.model'; +import { authMethodsMock } from '../../shared/testing/auth-service.stub'; import { EPersonMock } from '../../shared/testing/eperson.mock'; -import { AppState } from '../../app.reducer'; -import { ClientCookieService } from '../services/client-cookie.service'; -import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub'; import { routeServiceStub } from '../../shared/testing/route-service.stub'; -import { RouteService } from '../services/route.service'; +import { RouterStub } from '../../shared/testing/router.stub'; +import { + SpecialGroupDataMock, + SpecialGroupDataMock$, +} from '../../shared/testing/special-group.mock'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { RemoteData } from '../data/remote-data'; import { EPersonDataService } from '../eperson/eperson-data.service'; -import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; -import { authMethodsMock } from '../../shared/testing/auth-service.stub'; -import { AuthMethod } from './models/auth.method'; +import { EPerson } from '../eperson/models/eperson.model'; +import { ClientCookieService } from '../services/client-cookie.service'; +import { CookieService } from '../services/cookie.service'; import { HardRedirectService } from '../services/hard-redirect.service'; -import { NotificationsService } from '../../shared/notifications/notifications.service'; -import { TranslateService } from '@ngx-translate/core'; -import { getMockTranslateService } from '../../shared/mocks/translate.service.mock'; -import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub'; -import { SetUserAsIdleAction, UnsetUserAsIdleAction } from './auth.actions'; -import { SpecialGroupDataMock, SpecialGroupDataMock$ } from '../../shared/testing/special-group.mock'; -import { cold } from 'jasmine-marbles'; +import { RouteService } from '../services/route.service'; +import { + NativeWindowRef, + NativeWindowService, +} from '../services/window.service'; +import { + SetUserAsIdleAction, + UnsetUserAsIdleAction, +} from './auth.actions'; +import { + authReducer, + AuthState, +} from './auth.reducer'; +import { + AuthService, + IMPERSONATING_COOKIE, +} from './auth.service'; +import { AuthRequestService } from './auth-request.service'; +import { AuthMethod } from './models/auth.method'; +import { AuthStatus } from './models/auth-status.model'; +import { AuthTokenInfo } from './models/auth-token-info.model'; describe('AuthService test', () => { const mockEpersonDataService: any = { findByHref(href: string): Observable> { return createSuccessfulRemoteDataObject$(EPersonMock); - } + }, }; let mockStore: Store; @@ -62,13 +91,13 @@ describe('AuthService test', () => { uuid: 'test', authenticated: true, okay: true, - specialGroups: SpecialGroupDataMock$ + specialGroups: SpecialGroupDataMock$, }); function init() { mockStore = jasmine.createSpyObj('store', { dispatch: {}, - pipe: observableOf(true) + pipe: observableOf(true), }); window = new NativeWindowRef(); routerStub = new RouterStub(); @@ -80,7 +109,7 @@ describe('AuthService test', () => { loading: false, authToken: token, user: EPersonMock, - idle: false + idle: false, }; unAuthenticatedState = { authenticated: false, @@ -88,7 +117,7 @@ describe('AuthService test', () => { loading: false, authToken: undefined, user: undefined, - idle: false + idle: false, }; idleState = { authenticated: true, @@ -96,12 +125,12 @@ describe('AuthService test', () => { loading: false, authToken: token, user: EPersonMock, - idle: true + idle: true, }; authRequest = new AuthRequestServiceStub(); routeStub = new ActivatedRouteStub(); linkService = { - resolveLinks: {} + resolveLinks: {}, }; hardRedirectService = jasmine.createSpyObj('hardRedirectService', ['redirect']); spyOn(linkService, 'resolveLinks').and.returnValue({ authenticated: true, eperson: observableOf({ payload: {} }) }); @@ -117,11 +146,10 @@ describe('AuthService test', () => { StoreModule.forRoot({ authReducer }, { runtimeChecks: { strictStateImmutability: false, - strictActionImmutability: false - } + strictActionImmutability: false, + }, }), ], - declarations: [], providers: [ { provide: AuthRequestService, useValue: authRequest }, { provide: NativeWindowService, useValue: window }, @@ -135,7 +163,7 @@ describe('AuthService test', () => { { provide: NotificationsService, useValue: NotificationsServiceStub }, { provide: TranslateService, useValue: getMockTranslateService() }, CookieService, - AuthService + AuthService, ], }); authService = TestBed.inject(AuthService); @@ -238,9 +266,9 @@ describe('AuthService test', () => { StoreModule.forRoot({ authReducer }, { runtimeChecks: { strictStateImmutability: false, - strictActionImmutability: false - } - }) + strictActionImmutability: false, + }, + }), ], providers: [ { provide: AuthRequestService, useValue: authRequest }, @@ -249,8 +277,8 @@ describe('AuthService test', () => { { provide: RouteService, useValue: routeServiceStub }, { provide: RemoteDataBuildService, useValue: linkService }, CookieService, - AuthService - ] + AuthService, + ], }).compileComponents(); })); @@ -313,9 +341,9 @@ describe('AuthService test', () => { StoreModule.forRoot({ authReducer }, { runtimeChecks: { strictStateImmutability: false, - strictActionImmutability: false - } - }) + strictActionImmutability: false, + }, + }), ], providers: [ { provide: AuthRequestService, useValue: authRequest }, @@ -325,8 +353,8 @@ describe('AuthService test', () => { { provide: RemoteDataBuildService, useValue: linkService }, ClientCookieService, CookieService, - AuthService - ] + AuthService, + ], }).compileComponents(); })); @@ -338,7 +366,7 @@ describe('AuthService test', () => { loaded: true, loading: false, authToken: expiredToken, - user: EPersonMock + user: EPersonMock, }; store .subscribe((state) => { @@ -528,7 +556,7 @@ describe('AuthService test', () => { it('should call navigateToRedirectUrl with no url', () => { const expectRes = cold('(a|)', { - a: SpecialGroupDataMock + a: SpecialGroupDataMock, }); expect(authService.getSpecialGroupsFromAuthStatus()).toBeObservable(expectRes); }); @@ -543,9 +571,9 @@ describe('AuthService test', () => { StoreModule.forRoot({ authReducer }, { runtimeChecks: { strictStateImmutability: false, - strictActionImmutability: false - } - }) + strictActionImmutability: false, + }, + }), ], providers: [ { provide: AuthRequestService, useValue: authRequest }, @@ -554,8 +582,8 @@ describe('AuthService test', () => { { provide: RouteService, useValue: routeServiceStub }, { provide: RemoteDataBuildService, useValue: linkService }, CookieService, - AuthService - ] + AuthService, + ], }).compileComponents(); })); @@ -583,9 +611,9 @@ describe('AuthService test', () => { StoreModule.forRoot({ authReducer }, { runtimeChecks: { strictStateImmutability: false, - strictActionImmutability: false - } - }) + strictActionImmutability: false, + }, + }), ], providers: [ { provide: AuthRequestService, useValue: authRequest }, @@ -594,8 +622,8 @@ describe('AuthService test', () => { { provide: RouteService, useValue: routeServiceStub }, { provide: RemoteDataBuildService, useValue: linkService }, CookieService, - AuthService - ] + AuthService, + ], }).compileComponents(); })); diff --git a/src/app/core/auth/auth.service.ts b/src/app/core/auth/auth.service.ts index 6604936cde1..cd773b68cfa 100644 --- a/src/app/core/auth/auth.service.ts +++ b/src/app/core/auth/auth.service.ts @@ -1,18 +1,34 @@ -import { Inject, Injectable, Optional } from '@angular/core'; -import { Router } from '@angular/router'; import { HttpHeaders } from '@angular/common/http'; -import { REQUEST, RESPONSE } from '@nguniversal/express-engine/tokens'; - -import { Observable, of as observableOf } from 'rxjs'; -import { filter, map, startWith, switchMap, take } from 'rxjs/operators'; -import { select, Store } from '@ngrx/store'; +import { + Inject, + Injectable, + Optional, +} from '@angular/core'; +import { Router } from '@angular/router'; +import { + select, + Store, +} from '@ngrx/store'; +import { TranslateService } from '@ngx-translate/core'; import { CookieAttributes } from 'js-cookie'; +import { + Observable, + of as observableOf, +} from 'rxjs'; +import { + filter, + map, + startWith, + switchMap, + take, +} from 'rxjs/operators'; -import { EPerson } from '../eperson/models/eperson.model'; -import { AuthRequestService } from './auth-request.service'; -import { HttpOptions } from '../dspace-rest/dspace-rest.service'; -import { AuthStatus } from './models/auth-status.model'; -import { AuthTokenInfo, TOKENITEM } from './models/auth-token-info.model'; +import { environment } from '../../../environments/environment'; +import { + REQUEST, + RESPONSE, +} from '../../../express.tokens'; +import { AppState } from '../../app.reducer'; import { hasNoValue, hasValue, @@ -20,42 +36,58 @@ import { isEmpty, isNotEmpty, isNotNull, - isNotUndefined + isNotUndefined, } from '../../shared/empty.util'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; +import { followLink } from '../../shared/utils/follow-link-config.model'; +import { + buildPaginatedList, + PaginatedList, +} from '../data/paginated-list.model'; +import { RemoteData } from '../data/remote-data'; +import { HttpOptions } from '../dspace-rest/dspace-rest.service'; +import { EPersonDataService } from '../eperson/eperson-data.service'; +import { EPerson } from '../eperson/models/eperson.model'; +import { Group } from '../eperson/models/group.model'; import { CookieService } from '../services/cookie.service'; +import { HardRedirectService } from '../services/hard-redirect.service'; +import { RouteService } from '../services/route.service'; import { - getAuthenticatedUserId, - getAuthenticationToken, getExternalAuthCookieStatus, - getRedirectUrl, - isAuthenticated, - isAuthenticatedLoaded, - isIdle, - isTokenRefreshing -} from './selectors'; -import { AppState } from '../../app.reducer'; + NativeWindowRef, + NativeWindowService, +} from '../services/window.service'; +import { + getAllSucceededRemoteDataPayload, + getFirstCompletedRemoteData, +} from '../shared/operators'; +import { PageInfo } from '../shared/page-info.model'; import { CheckAuthenticationTokenAction, RefreshTokenAction, - ResetAuthenticationMessagesAction, SetAuthCookieStatus, + ResetAuthenticationMessagesAction, + SetAuthCookieStatus, SetRedirectUrlAction, SetUserAsIdleAction, - UnsetUserAsIdleAction + UnsetUserAsIdleAction, } from './auth.actions'; -import { NativeWindowRef, NativeWindowService } from '../services/window.service'; -import { RouteService } from '../services/route.service'; -import { EPersonDataService } from '../eperson/eperson-data.service'; -import { getAllSucceededRemoteDataPayload, getFirstCompletedRemoteData } from '../shared/operators'; +import { AuthRequestService } from './auth-request.service'; import { AuthMethod } from './models/auth.method'; -import { HardRedirectService } from '../services/hard-redirect.service'; -import { RemoteData } from '../data/remote-data'; -import { environment } from '../../../environments/environment'; -import { NotificationsService } from '../../shared/notifications/notifications.service'; -import { TranslateService } from '@ngx-translate/core'; -import { buildPaginatedList, PaginatedList } from '../data/paginated-list.model'; -import { Group } from '../eperson/models/group.model'; -import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; -import { PageInfo } from '../shared/page-info.model'; -import { followLink } from '../../shared/utils/follow-link-config.model'; +import { AuthStatus } from './models/auth-status.model'; +import { + AuthTokenInfo, + TOKENITEM, +} from './models/auth-token-info.model'; +import { + getAuthenticatedUserId, + getAuthenticationToken, + getExternalAuthCookieStatus, + getRedirectUrl, + isAuthenticated, + isAuthenticatedLoaded, + isIdle, + isTokenRefreshing, +} from './selectors'; export const LOGIN_ROUTE = '/login'; export const LOGOUT_ROUTE = '/logout'; @@ -65,7 +97,7 @@ export const IMPERSONATING_COOKIE = 'dsImpersonatingEPerson'; /** * The auth service. */ -@Injectable() +@Injectable({ providedIn: 'root' }) export class AuthService { /** @@ -90,13 +122,13 @@ export class AuthService { protected store: Store, protected hardRedirectService: HardRedirectService, private notificationService: NotificationsService, - private translateService: TranslateService + private translateService: TranslateService, ) { this.store.pipe( // when this service is constructed the store is not fully initialized yet filter((state: any) => state?.core?.auth !== undefined), select(isAuthenticated), - startWith(false) + startWith(false), ).subscribe((authenticated: boolean) => this._authenticated = authenticated); } @@ -119,7 +151,7 @@ export class AuthService { if (hasValue(rd.payload) && rd.payload.authenticated) { return rd.payload; } else { - throw (new Error('Invalid email or password')); + throw (new Error('auth.errors.invalid-user')); } })); @@ -136,7 +168,7 @@ export class AuthService { options.headers = headers; options.withCredentials = true; return this.authRequestService.getRequest('status', options).pipe( - map((rd: RemoteData) => Object.assign(new AuthStatus(), rd.payload)) + map((rd: RemoteData) => Object.assign(new AuthStatus(), rd.payload)), ); } @@ -202,7 +234,7 @@ export class AuthService { */ public retrieveAuthenticatedUserByHref(userHref: string): Observable { return this.epersonService.findByHref(userHref).pipe( - getAllSucceededRemoteDataPayload() + getAllSucceededRemoteDataPayload(), ); } @@ -212,7 +244,7 @@ export class AuthService { */ public retrieveAuthenticatedUserById(userId: string): Observable { return this.epersonService.findById(userId).pipe( - getAllSucceededRemoteDataPayload() + getAllSucceededRemoteDataPayload(), ); } @@ -225,7 +257,7 @@ export class AuthService { select(getAuthenticatedUserId), hasValueOperator(), switchMap((id: string) => this.epersonService.findById(id)), - getAllSucceededRemoteDataPayload() + getAllSucceededRemoteDataPayload(), ); } @@ -248,7 +280,7 @@ export class AuthService { } else { return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(),[])); } - }) + }), ); } @@ -260,15 +292,14 @@ export class AuthService { select(getAuthenticationToken), take(1), map((authTokenInfo: AuthTokenInfo) => { - let token: AuthTokenInfo; // Retrieve authentication token info and check if is valid - token = isNotEmpty(authTokenInfo) ? authTokenInfo : this.storage.get(TOKENITEM); + const token = isNotEmpty(authTokenInfo) ? authTokenInfo : this.storage.get(TOKENITEM); if (isNotEmpty(token) && token.hasOwnProperty('accessToken') && isNotEmpty(token.accessToken) && !this.isTokenExpired(token)) { return token; } else { throw false; } - }) + }), ); } @@ -412,7 +443,7 @@ export class AuthService { const token = this.getToken(); return token.expires - (60 * 5 * 1000) < Date.now(); } - }) + }), ); } @@ -437,7 +468,7 @@ export class AuthService { // Set the cookie expire date const expires = new Date(expireDate); - const options: CookieAttributes = {expires: expires}; + const options: CookieAttributes = { expires: expires }; // Save cookie with the token return this.storage.set(TOKENITEM, token, options); @@ -516,7 +547,7 @@ export class AuthService { } else { return this.storage.get(REDIRECT_COOKIE); } - }) + }), ); } @@ -529,7 +560,7 @@ export class AuthService { // Set the cookie expire date const expires = new Date(expireDate); - const options: CookieAttributes = {expires: expires}; + const options: CookieAttributes = { expires: expires }; this.storage.set(REDIRECT_COOKIE, url, options); this.store.dispatch(new SetRedirectUrlAction(isNotUndefined(url) ? url : '')); } @@ -609,7 +640,7 @@ export class AuthService { */ getShortlivedToken(): Observable { return this.isAuthenticated().pipe( - switchMap((authenticated) => authenticated ? this.authRequestService.getShortlivedToken() : observableOf(null)) + switchMap((authenticated) => authenticated ? this.authRequestService.getShortlivedToken() : observableOf(null)), ); } diff --git a/src/app/core/auth/authenticated.guard.ts b/src/app/core/auth/authenticated.guard.ts index 1ab1d2e0a51..eba6dc89f9e 100644 --- a/src/app/core/auth/authenticated.guard.ts +++ b/src/app/core/auth/authenticated.guard.ts @@ -1,65 +1,64 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, - CanActivate, + CanActivateChildFn, + CanActivateFn, Router, RouterStateSnapshot, - UrlTree + UrlTree, } from '@angular/router'; - +import { + select, + Store, +} from '@ngrx/store'; import { Observable } from 'rxjs'; -import { map, find, switchMap } from 'rxjs/operators'; -import { select, Store } from '@ngrx/store'; +import { + find, + map, + switchMap, +} from 'rxjs/operators'; -import { isAuthenticated, isAuthenticationLoading } from './selectors'; -import { AuthService, LOGIN_ROUTE } from './auth.service'; -import { CoreState } from '../core-state.model'; +import { AppState } from '../../app.reducer'; +import { + AuthService, + LOGIN_ROUTE, +} from './auth.service'; +import { + isAuthenticated, + isAuthenticationLoading, +} from './selectors'; /** * 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 { - - /** - * @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); - } +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]); + } + }), + ); +}; - 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/browser-auth-request.service.spec.ts b/src/app/core/auth/browser-auth-request.service.spec.ts index b41d981bcf6..9649255b236 100644 --- a/src/app/core/auth/browser-auth-request.service.spec.ts +++ b/src/app/core/auth/browser-auth-request.service.spec.ts @@ -1,8 +1,9 @@ -import { AuthRequestService } from './auth-request.service'; -import { RequestService } from '../data/request.service'; -import { BrowserAuthRequestService } from './browser-auth-request.service'; import { Observable } from 'rxjs'; + import { PostRequest } from '../data/request.models'; +import { RequestService } from '../data/request.service'; +import { AuthRequestService } from './auth-request.service'; +import { BrowserAuthRequestService } from './browser-auth-request.service'; describe(`BrowserAuthRequestService`, () => { let href: string; @@ -12,7 +13,7 @@ describe(`BrowserAuthRequestService`, () => { beforeEach(() => { href = 'https://rest.api/auth/shortlivedtokens'; requestService = jasmine.createSpyObj('requestService', { - 'generateRequestId': '8bb0582d-5013-4337-af9c-763beb25aae2' + 'generateRequestId': '8bb0582d-5013-4337-af9c-763beb25aae2', }); service = new BrowserAuthRequestService(null, requestService, null); }); diff --git a/src/app/core/auth/browser-auth-request.service.ts b/src/app/core/auth/browser-auth-request.service.ts index 485e2ef9c4a..d708cd8982a 100644 --- a/src/app/core/auth/browser-auth-request.service.ts +++ b/src/app/core/auth/browser-auth-request.service.ts @@ -1,10 +1,14 @@ import { Injectable } from '@angular/core'; -import { AuthRequestService } from './auth-request.service'; +import { + Observable, + of as observableOf, +} from 'rxjs'; + +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { PostRequest } from '../data/request.models'; -import { HALEndpointService } from '../shared/hal-endpoint.service'; import { RequestService } from '../data/request.service'; -import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; -import { Observable, of as observableOf } from 'rxjs'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { AuthRequestService } from './auth-request.service'; /** * Client side version of the service to send authentication requests @@ -15,7 +19,7 @@ export class BrowserAuthRequestService extends AuthRequestService { constructor( halService: HALEndpointService, requestService: RequestService, - rdbService: RemoteDataBuildService + rdbService: RemoteDataBuildService, ) { super(halService, requestService, rdbService); } diff --git a/src/app/core/auth/models/auth-status.model.ts b/src/app/core/auth/models/auth-status.model.ts index d18b1ccf9a6..f25e5a48928 100644 --- a/src/app/core/auth/models/auth-status.model.ts +++ b/src/app/core/auth/models/auth-status.model.ts @@ -1,7 +1,17 @@ -import { autoserialize, deserialize, deserializeAs } from 'cerialize'; +import { + autoserialize, + deserialize, + deserializeAs, +} from 'cerialize'; import { Observable } from 'rxjs'; -import { link, typedObject } from '../../cache/builders/build-decorators'; + +import { + link, + typedObject, +} from '../../cache/builders/build-decorators'; +import { CacheableObject } from '../../cache/cacheable-object.model'; import { IDToUUIDSerializer } from '../../cache/id-to-uuid-serializer'; +import { PaginatedList } from '../../data/paginated-list.model'; import { RemoteData } from '../../data/remote-data'; import { EPerson } from '../../eperson/models/eperson.model'; import { EPERSON } from '../../eperson/models/eperson.resource-type'; @@ -10,12 +20,10 @@ import { GROUP } from '../../eperson/models/group.resource-type'; import { HALLink } from '../../shared/hal-link.model'; import { ResourceType } from '../../shared/resource-type'; import { excludeFromEquals } from '../../utilities/equals.decorators'; +import { AuthMethod } from './auth.method'; import { AuthError } from './auth-error.model'; import { AUTH_STATUS } from './auth-status.resource-type'; import { AuthTokenInfo } from './auth-token-info.model'; -import { AuthMethod } from './auth.method'; -import { CacheableObject } from '../../cache/cacheable-object.model'; -import { PaginatedList } from '../../data/paginated-list.model'; /** * Object that represents the authenticated status of a user @@ -43,7 +51,7 @@ export class AuthStatus implements CacheableObject { * It is based on the ID, so it will be the same for each refresh. */ @deserializeAs(new IDToUUIDSerializer('auth-status'), 'id') - uuid: string; + uuid: string; /** * True if REST API is up and running, should never return false diff --git a/src/app/core/auth/models/auth.method.ts b/src/app/core/auth/models/auth.method.ts index 0579ae0cd1b..b84e7a308af 100644 --- a/src/app/core/auth/models/auth.method.ts +++ b/src/app/core/auth/models/auth.method.ts @@ -2,11 +2,12 @@ import { AuthMethodType } from './auth.method-type'; export class AuthMethod { authMethodType: AuthMethodType; + position: number; location?: string; - // isStandalonePage? = true; + constructor(authMethodName: string, position: number, location?: string) { + this.position = position; - constructor(authMethodName: string, location?: string) { switch (authMethodName) { case 'ip': { this.authMethodType = AuthMethodType.Ip; 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 3786bd8e6a0..5e8587d02dd 100644 --- a/src/app/core/auth/models/short-lived-token.model.ts +++ b/src/app/core/auth/models/short-lived-token.model.ts @@ -1,10 +1,15 @@ +import { + autoserialize, + autoserializeAs, + deserialize, +} from 'cerialize'; + import { typedObject } from '../../cache/builders/build-decorators'; -import { excludeFromEquals } from '../../utilities/equals.decorators'; -import { autoserialize, autoserializeAs, deserialize } from 'cerialize'; +import { CacheableObject } from '../../cache/cacheable-object.model'; +import { HALLink } from '../../shared/hal-link.model'; import { ResourceType } from '../../shared/resource-type'; +import { excludeFromEquals } from '../../utilities/equals.decorators'; import { SHORT_LIVED_TOKEN } from './short-lived-token.resource-type'; -import { HALLink } from '../../shared/hal-link.model'; -import { CacheableObject } from '../../cache/cacheable-object.model'; /** * A short-lived token that can be used to authenticate a rest request diff --git a/src/app/core/auth/selectors.ts b/src/app/core/auth/selectors.ts index aba739edf67..63603776263 100644 --- a/src/app/core/auth/selectors.ts +++ b/src/app/core/auth/selectors.ts @@ -1,5 +1,7 @@ import { createSelector } from '@ngrx/store'; +import { coreSelector } from '../core.selectors'; +import { CoreState } from '../core-state.model'; /** * Every reducer module's default export is the reducer function itself. In * addition, each module should export a type or interface that describes @@ -7,8 +9,6 @@ import { createSelector } from '@ngrx/store'; * notation packages up all of the exports into a single object. */ import { AuthState } from './auth.reducer'; -import { CoreState } from '../core-state.model'; -import { coreSelector } from '../core.selectors'; /** * Returns the user state. diff --git a/src/app/core/auth/server-auth-request.service.spec.ts b/src/app/core/auth/server-auth-request.service.spec.ts index 5b0221e5df4..b119f6b3b76 100644 --- a/src/app/core/auth/server-auth-request.service.spec.ts +++ b/src/app/core/auth/server-auth-request.service.spec.ts @@ -1,14 +1,22 @@ -import { AuthRequestService } from './auth-request.service'; +import { + HttpClient, + HttpHeaders, + HttpResponse, +} from '@angular/common/http'; +import { + Observable, + of as observableOf, +} from 'rxjs'; + +import { PostRequest } from '../data/request.models'; import { RequestService } from '../data/request.service'; -import { ServerAuthRequestService } from './server-auth-request.service'; -import { HttpClient, HttpHeaders, HttpResponse } from '@angular/common/http'; -import { Observable, of as observableOf } from 'rxjs'; import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { PostRequest } from '../data/request.models'; import { XSRF_REQUEST_HEADER, - XSRF_RESPONSE_HEADER + XSRF_RESPONSE_HEADER, } from '../xsrf/xsrf.constants'; +import { AuthRequestService } from './auth-request.service'; +import { ServerAuthRequestService } from './server-auth-request.service'; describe(`ServerAuthRequestService`, () => { let href: string; @@ -22,20 +30,20 @@ describe(`ServerAuthRequestService`, () => { beforeEach(() => { href = 'https://rest.api/auth/shortlivedtokens'; requestService = jasmine.createSpyObj('requestService', { - 'generateRequestId': '8bb0582d-5013-4337-af9c-763beb25aae2' + 'generateRequestId': '8bb0582d-5013-4337-af9c-763beb25aae2', }); let headers = new HttpHeaders(); headers = headers.set(XSRF_RESPONSE_HEADER, mockToken); httpResponse = { body: { bar: false }, headers: headers, - statusText: '200' + statusText: '200', } as HttpResponse; httpClient = jasmine.createSpyObj('httpClient', { get: observableOf(httpResponse), }); halService = jasmine.createSpyObj('halService', { - 'getRootHref': '/api' + 'getRootHref': '/api', }); service = new ServerAuthRequestService(halService, requestService, null, httpClient); }); diff --git a/src/app/core/auth/server-auth-request.service.ts b/src/app/core/auth/server-auth-request.service.ts index 058322acce0..5f1828c71c5 100644 --- a/src/app/core/auth/server-auth-request.service.ts +++ b/src/app/core/auth/server-auth-request.service.ts @@ -1,21 +1,22 @@ -import { Injectable } from '@angular/core'; -import { AuthRequestService } from './auth-request.service'; -import { PostRequest } from '../data/request.models'; -import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { RequestService } from '../data/request.service'; -import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { - HttpHeaders, HttpClient, - HttpResponse + HttpHeaders, + HttpResponse, } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; + +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { PostRequest } from '../data/request.models'; +import { RequestService } from '../data/request.service'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; import { + DSPACE_XSRF_COOKIE, XSRF_REQUEST_HEADER, XSRF_RESPONSE_HEADER, - DSPACE_XSRF_COOKIE } from '../xsrf/xsrf.constants'; -import { map } from 'rxjs/operators'; -import { Observable } from 'rxjs'; +import { AuthRequestService } from './auth-request.service'; /** * Server side version of the service to send authentication requests @@ -45,11 +46,11 @@ export class ServerAuthRequestService extends AuthRequestService { map((response: HttpResponse) => response.headers.get(XSRF_RESPONSE_HEADER)), // Use that token to create an HttpHeaders object map((xsrfToken: string) => new HttpHeaders() - .set('Content-Type', 'application/json; charset=utf-8') - // set the token as the XSRF header - .set(XSRF_REQUEST_HEADER, xsrfToken) - // and as the DSPACE-XSRF-COOKIE - .set('Cookie', `${DSPACE_XSRF_COOKIE}=${xsrfToken}`)), + .set('Content-Type', 'application/json; charset=utf-8') + // set the token as the XSRF header + .set(XSRF_REQUEST_HEADER, xsrfToken) + // and as the DSPACE-XSRF-COOKIE + .set('Cookie', `${DSPACE_XSRF_COOKIE}=${xsrfToken}`)), map((headers: HttpHeaders) => // Create a new PostRequest using those headers and the given href new PostRequest( @@ -59,8 +60,8 @@ export class ServerAuthRequestService extends AuthRequestService { { headers: headers, }, - ) - ) + ), + ), ); } diff --git a/src/app/core/auth/server-auth.service.ts b/src/app/core/auth/server-auth.service.ts index fc8ab18bfb6..f51215abada 100644 --- a/src/app/core/auth/server-auth.service.ts +++ b/src/app/core/auth/server-auth.service.ts @@ -1,15 +1,17 @@ -import { Injectable } from '@angular/core'; import { HttpHeaders } from '@angular/common/http'; - +import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; -import { hasValue, isNotEmpty } from '../../shared/empty.util'; +import { + hasValue, + isNotEmpty, +} from '../../shared/empty.util'; +import { RemoteData } from '../data/remote-data'; import { HttpOptions } from '../dspace-rest/dspace-rest.service'; import { AuthService } from './auth.service'; import { AuthStatus } from './models/auth-status.model'; import { AuthTokenInfo } from './models/auth-token-info.model'; -import { RemoteData } from '../data/remote-data'; /** * The auth service. @@ -57,7 +59,7 @@ export class ServerAuthService extends AuthService { options.headers = headers; options.withCredentials = true; return this.authRequestService.getRequest('status', options).pipe( - map((rd: RemoteData) => Object.assign(new AuthStatus(), rd.payload)) + map((rd: RemoteData) => Object.assign(new AuthStatus(), rd.payload)), ); } } diff --git a/src/app/core/auth/token-response-parsing.service.spec.ts b/src/app/core/auth/token-response-parsing.service.spec.ts index a440325560a..f4244e98b2c 100644 --- a/src/app/core/auth/token-response-parsing.service.spec.ts +++ b/src/app/core/auth/token-response-parsing.service.spec.ts @@ -1,6 +1,6 @@ -import { TokenResponseParsingService } from './token-response-parsing.service'; -import { RawRestResponse } from '../dspace-rest/raw-rest-response.model'; import { TokenResponse } from '../cache/response.models'; +import { RawRestResponse } from '../dspace-rest/raw-rest-response.model'; +import { TokenResponseParsingService } from './token-response-parsing.service'; describe('TokenResponseParsingService', () => { let service: TokenResponseParsingService; @@ -13,10 +13,10 @@ describe('TokenResponseParsingService', () => { it('should return a TokenResponse containing the token', () => { const data = { payload: { - token: 'valid-token' + token: 'valid-token', }, statusCode: 200, - statusText: 'OK' + statusText: 'OK', } as RawRestResponse; const expected = new TokenResponse(data.payload.token, true, 200, 'OK'); expect(service.parse(undefined, data)).toEqual(expected); @@ -26,7 +26,7 @@ describe('TokenResponseParsingService', () => { const data = { payload: {}, statusCode: 200, - statusText: 'OK' + statusText: 'OK', } as RawRestResponse; const expected = new TokenResponse(null, false, 200, 'OK'); expect(service.parse(undefined, data)).toEqual(expected); @@ -36,7 +36,7 @@ describe('TokenResponseParsingService', () => { const data = { payload: {}, statusCode: 400, - statusText: 'BAD REQUEST' + statusText: 'BAD REQUEST', } as RawRestResponse; const expected = new TokenResponse(null, false, 400, 'BAD REQUEST'); expect(service.parse(undefined, data)).toEqual(expected); diff --git a/src/app/core/auth/token-response-parsing.service.ts b/src/app/core/auth/token-response-parsing.service.ts index 1ba7a16b14e..03a45521e93 100644 --- a/src/app/core/auth/token-response-parsing.service.ts +++ b/src/app/core/auth/token-response-parsing.service.ts @@ -1,11 +1,15 @@ -import { ResponseParsingService } from '../data/parsing.service'; -import { RawRestResponse } from '../dspace-rest/raw-rest-response.model'; -import { RestResponse, TokenResponse } from '../cache/response.models'; -import { isNotEmpty } from '../../shared/empty.util'; import { Injectable } from '@angular/core'; + +import { isNotEmpty } from '../../shared/empty.util'; +import { + RestResponse, + TokenResponse, +} from '../cache/response.models'; +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 b2ddade682c..5628fe65837 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 { Bitstream } from '../shared/bitstream.model'; import { BitstreamDataService } from '../data/bitstream-data.service'; -import { BITSTREAM_PAGE_LINKS_TO_FOLLOW } from '../../bitstream-page/bitstream-page.resolver'; -import { DSOBreadcrumbResolver } from './dso-breadcrumb.resolver'; +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); - } - - /** - * 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; - } +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>; +}; -} diff --git a/src/app/core/breadcrumbs/bitstream-breadcrumbs.service.ts b/src/app/core/breadcrumbs/bitstream-breadcrumbs.service.ts index 333886ed3d1..11a3a743505 100644 --- a/src/app/core/breadcrumbs/bitstream-breadcrumbs.service.ts +++ b/src/app/core/breadcrumbs/bitstream-breadcrumbs.service.ts @@ -1,35 +1,46 @@ import { Injectable } from '@angular/core'; +import { + Observable, + of as observableOf, +} from 'rxjs'; +import { + map, + switchMap, +} from 'rxjs/operators'; -import { Observable, of as observableOf } from 'rxjs'; -import { map, switchMap } from 'rxjs/operators'; - +import { getDSORoute } from '../../app-routing-paths'; +import { BITSTREAM_PAGE_LINKS_TO_FOLLOW } from '../../bitstream-page/bitstream-page.resolver'; import { Breadcrumb } from '../../breadcrumbs/breadcrumb/breadcrumb.model'; -import { DSONameService } from './dso-name.service'; -import { ChildHALResource } from '../shared/child-hal-resource.model'; +import { + hasValue, + isNotEmpty, +} from '../../shared/empty.util'; import { LinkService } from '../cache/builders/link.service'; -import { DSpaceObject } from '../shared/dspace-object.model'; -import { RemoteData } from '../data/remote-data'; -import { hasValue, isNotEmpty } from '../../shared/empty.util'; -import { getDSORoute } from '../../app-routing-paths'; -import { DSOBreadcrumbsService } from './dso-breadcrumbs.service'; import { BitstreamDataService } from '../data/bitstream-data.service'; -import { getFirstCompletedRemoteData, getRemoteDataPayload } from '../shared/operators'; +import { RemoteData } from '../data/remote-data'; import { Bitstream } from '../shared/bitstream.model'; import { Bundle } from '../shared/bundle.model'; +import { ChildHALResource } from '../shared/child-hal-resource.model'; +import { DSpaceObject } from '../shared/dspace-object.model'; import { Item } from '../shared/item.model'; -import { BITSTREAM_PAGE_LINKS_TO_FOLLOW } from '../../bitstream-page/bitstream-page.resolver'; +import { + getFirstCompletedRemoteData, + getRemoteDataPayload, +} from '../shared/operators'; +import { DSOBreadcrumbsService } from './dso-breadcrumbs.service'; +import { DSONameService } from './dso-name.service'; /** * Service to calculate DSpaceObject breadcrumbs for a single part of the route */ @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class BitstreamBreadcrumbsService extends DSOBreadcrumbsService { constructor( protected bitstreamService: BitstreamDataService, protected linkService: LinkService, - protected dsoNameService: DSONameService + protected dsoNameService: DSONameService, ) { super(linkService, dsoNameService); } @@ -53,7 +64,7 @@ export class BitstreamBreadcrumbsService extends DSOBreadcrumbsService { return observableOf([]); }), - map((breadcrumbs: Breadcrumb[]) => [...breadcrumbs, crumb]) + map((breadcrumbs: Breadcrumb[]) => [...breadcrumbs, crumb]), ); } @@ -74,12 +85,12 @@ export class BitstreamBreadcrumbsService extends DSOBreadcrumbsService { } else { return observableOf(undefined); } - }) + }), ); } else { return observableOf(undefined); } - }) + }), ); } } diff --git a/src/app/core/breadcrumbs/breadcrumbsProviderService.ts b/src/app/core/breadcrumbs/breadcrumbsProviderService.ts index 4f5dd0a5838..83d9a2caaa9 100644 --- a/src/app/core/breadcrumbs/breadcrumbsProviderService.ts +++ b/src/app/core/breadcrumbs/breadcrumbsProviderService.ts @@ -1,6 +1,7 @@ -import { Breadcrumb } from '../../breadcrumbs/breadcrumb/breadcrumb.model'; import { Observable } from 'rxjs'; +import { Breadcrumb } from '../../breadcrumbs/breadcrumb/breadcrumb.model'; + /** * Service to calculate breadcrumbs for a single part of the route */ diff --git a/src/app/core/breadcrumbs/collection-breadcrumb.resolver.ts b/src/app/core/breadcrumbs/collection-breadcrumb.resolver.ts index 46c49add064..7df656a9610 100644 --- a/src/app/core/breadcrumbs/collection-breadcrumb.resolver.ts +++ b/src/app/core/breadcrumbs/collection-breadcrumb.resolver.ts @@ -1,28 +1,36 @@ -import { Injectable } from '@angular/core'; -import { DSOBreadcrumbsService } from './dso-breadcrumbs.service'; -import { DSOBreadcrumbResolver } from './dso-breadcrumb.resolver'; -import { Collection } from '../shared/collection.model'; -import { CollectionDataService } from '../data/collection-data.service'; -import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; +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 309927771d5..0c37b5ca4f2 100644 --- a/src/app/core/breadcrumbs/community-breadcrumb.resolver.ts +++ b/src/app/core/breadcrumbs/community-breadcrumb.resolver.ts @@ -1,28 +1,50 @@ -import { Injectable } from '@angular/core'; -import { DSOBreadcrumbsService } from './dso-breadcrumbs.service'; -import { DSOBreadcrumbResolver } from './dso-breadcrumb.resolver'; +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 { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; -import { COMMUNITY_PAGE_LINKS_TO_FOLLOW } from '../../community-page/community-page.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); - } - - /** - * 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; +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>; } -} +}; diff --git a/src/app/core/breadcrumbs/dso-breadcrumb.resolver.spec.ts b/src/app/core/breadcrumbs/dso-breadcrumb.resolver.spec.ts index e35e26e46f9..c40a14a3231 100644 --- a/src/app/core/breadcrumbs/dso-breadcrumb.resolver.spec.ts +++ b/src/app/core/breadcrumbs/dso-breadcrumb.resolver.spec.ts @@ -1,12 +1,12 @@ -import { DSOBreadcrumbResolver } from './dso-breadcrumb.resolver'; -import { Collection } from '../shared/collection.model'; -import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; import { getTestScheduler } from 'jasmine-marbles'; -import { CollectionBreadcrumbResolver } from './collection-breadcrumb.resolver'; + +import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; +import { Collection } from '../shared/collection.model'; +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; @@ -16,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 8be4e5e099a..992627ddfaa 100644 --- a/src/app/core/breadcrumbs/dso-breadcrumb.resolver.ts +++ b/src/app/core/breadcrumbs/dso-breadcrumb.resolver.ts @@ -1,56 +1,69 @@ -import { BreadcrumbConfig } from '../../breadcrumbs/breadcrumb/breadcrumb-config.model'; -import { Injectable } from '@angular/core'; -import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router'; -import { DSOBreadcrumbsService } from './dso-breadcrumbs.service'; -import { getFirstCompletedRemoteData, getRemoteDataPayload } from '../shared/operators'; -import { map } from 'rxjs/operators'; +import { + ActivatedRouteSnapshot, + RouterStateSnapshot, +} from '@angular/router'; import { Observable } from 'rxjs'; -import { DSpaceObject } from '../shared/dspace-object.model'; -import { ChildHALResource } from '../shared/child-hal-resource.model'; -import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; +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 { DSpaceObject } from '../shared/dspace-object.model'; +import { + getFirstCompletedRemoteData, + getRemoteDataPayload, +} from '../shared/operators'; +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-breadcrumbs.service.spec.ts b/src/app/core/breadcrumbs/dso-breadcrumbs.service.spec.ts index 6b89c576d66..3869defa70d 100644 --- a/src/app/core/breadcrumbs/dso-breadcrumbs.service.spec.ts +++ b/src/app/core/breadcrumbs/dso-breadcrumbs.service.spec.ts @@ -1,17 +1,24 @@ -import { TestBed, waitForAsync } from '@angular/core/testing'; -import { DSOBreadcrumbsService } from './dso-breadcrumbs.service'; +import { + TestBed, + waitForAsync, +} from '@angular/core/testing'; +import { getTestScheduler } from 'jasmine-marbles'; +import { of as observableOf } from 'rxjs'; + +import { getDSORoute } from '../../app-routing-paths'; +import { Breadcrumb } from '../../breadcrumbs/breadcrumb/breadcrumb.model'; import { getMockLinkService } from '../../shared/mocks/link-service.mock'; +import { + createSuccessfulRemoteDataObject, + createSuccessfulRemoteDataObject$, +} from '../../shared/remote-data.utils'; import { LinkService } from '../cache/builders/link.service'; -import { Item } from '../shared/item.model'; -import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; -import { DSpaceObject } from '../shared/dspace-object.model'; -import { of as observableOf } from 'rxjs'; -import { Community } from '../shared/community.model'; import { Collection } from '../shared/collection.model'; -import { Breadcrumb } from '../../breadcrumbs/breadcrumb/breadcrumb.model'; -import { getTestScheduler } from 'jasmine-marbles'; +import { Community } from '../shared/community.model'; +import { DSpaceObject } from '../shared/dspace-object.model'; +import { Item } from '../shared/item.model'; +import { DSOBreadcrumbsService } from './dso-breadcrumbs.service'; import { DSONameService } from './dso-name.service'; -import { getDSORoute } from '../../app-routing-paths'; describe('DSOBreadcrumbsService', () => { let service: DSOBreadcrumbsService; @@ -43,46 +50,46 @@ describe('DSOBreadcrumbsService', () => { { type: 'community', metadata: { - 'dc.title': [{ value: 'community' }] + 'dc.title': [{ value: 'community' }], }, uuid: communityUUID, parentCommunity: observableOf(Object.assign(createSuccessfulRemoteDataObject(undefined), { statusCode: 204 })), _links: { parentCommunity: 'site', - self: communityPath + communityUUID - } - } + self: communityPath + communityUUID, + }, + }, ); testCollection = Object.assign(new Collection(), { type: 'collection', metadata: { - 'dc.title': [{ value: 'collection' }] + 'dc.title': [{ value: 'collection' }], }, uuid: collectionUUID, parentCommunity: createSuccessfulRemoteDataObject$(testCommunity), _links: { parentCommunity: communityPath + communityUUID, - self: communityPath + collectionUUID - } - } + self: communityPath + collectionUUID, + }, + }, ); testItem = Object.assign(new Item(), { type: 'item', metadata: { - 'dc.title': [{ value: 'item' }] + 'dc.title': [{ value: 'item' }], }, uuid: itemUUID, owningCollection: createSuccessfulRemoteDataObject$(testCollection), _links: { owningCollection: collectionPath + collectionUUID, - self: itemPath + itemUUID - } - } + self: itemPath + itemUUID, + }, + }, ); dsoNameService = { getName: (dso) => getName(dso) }; @@ -93,8 +100,8 @@ describe('DSOBreadcrumbsService', () => { TestBed.configureTestingModule({ providers: [ { provide: LinkService, useValue: getMockLinkService() }, - { provide: DSONameService, useValue: dsoNameService } - ] + { provide: DSONameService, useValue: dsoNameService }, + ], }).compileComponents(); })); diff --git a/src/app/core/breadcrumbs/dso-breadcrumbs.service.ts b/src/app/core/breadcrumbs/dso-breadcrumbs.service.ts index 9a22cd0e357..7d2e3697e18 100644 --- a/src/app/core/breadcrumbs/dso-breadcrumbs.service.ts +++ b/src/app/core/breadcrumbs/dso-breadcrumbs.service.ts @@ -1,27 +1,35 @@ +import { Injectable } from '@angular/core'; +import { + Observable, + of as observableOf, +} from 'rxjs'; +import { + find, + map, + switchMap, +} from 'rxjs/operators'; + +import { getDSORoute } from '../../app-routing-paths'; import { Breadcrumb } from '../../breadcrumbs/breadcrumb/breadcrumb.model'; -import { BreadcrumbsProviderService } from './breadcrumbsProviderService'; -import { DSONameService } from './dso-name.service'; -import { Observable, of as observableOf } from 'rxjs'; -import { ChildHALResource } from '../shared/child-hal-resource.model'; -import { LinkService } from '../cache/builders/link.service'; -import { DSpaceObject } from '../shared/dspace-object.model'; +import { hasValue } from '../../shared/empty.util'; import { followLink } from '../../shared/utils/follow-link-config.model'; -import { find, map, switchMap } from 'rxjs/operators'; +import { LinkService } from '../cache/builders/link.service'; import { RemoteData } from '../data/remote-data'; -import { hasValue } from '../../shared/empty.util'; -import { Injectable } from '@angular/core'; -import { getDSORoute } from '../../app-routing-paths'; +import { ChildHALResource } from '../shared/child-hal-resource.model'; +import { DSpaceObject } from '../shared/dspace-object.model'; +import { BreadcrumbsProviderService } from './breadcrumbsProviderService'; +import { DSONameService } from './dso-name.service'; /** * Service to calculate DSpaceObject breadcrumbs for a single part of the route */ @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class DSOBreadcrumbsService implements BreadcrumbsProviderService { constructor( protected linkService: LinkService, - protected dsoNameService: DSONameService + protected dsoNameService: DSONameService, ) { } @@ -46,7 +54,7 @@ export class DSOBreadcrumbsService implements BreadcrumbsProviderService [...breadcrumbs, crumb]) + map((breadcrumbs: Breadcrumb[]) => [...breadcrumbs, crumb]), ); } } diff --git a/src/app/core/breadcrumbs/dso-name.service.spec.ts b/src/app/core/breadcrumbs/dso-name.service.spec.ts index 9f2f76599af..5f241b1a6cc 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; @@ -22,7 +26,27 @@ describe(`DSONameService`, () => { }, getRenderTypes(): (string | GenericConstructor)[] { return ['Person', Item, DSpaceObject]; - } + }, + }); + + 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'; @@ -32,7 +56,7 @@ describe(`DSONameService`, () => { }, getRenderTypes(): (string | GenericConstructor)[] { return ['OrgUnit', Item, DSpaceObject]; - } + }, }); mockDSOName = 'Lorem Ipsum'; @@ -42,7 +66,7 @@ describe(`DSONameService`, () => { }, getRenderTypes(): (string | GenericConstructor)[] { return [DSpaceObject]; - } + }, }); service = new DSONameService({ instant: (a) => a } as any); @@ -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/dso-name.service.ts b/src/app/core/breadcrumbs/dso-name.service.ts index ddd97705b01..988141209f4 100644 --- a/src/app/core/breadcrumbs/dso-name.service.ts +++ b/src/app/core/breadcrumbs/dso-name.service.ts @@ -1,7 +1,11 @@ import { Injectable } from '@angular/core'; -import { hasValue, isEmpty } from '../../shared/empty.util'; -import { DSpaceObject } from '../shared/dspace-object.model'; import { TranslateService } from '@ngx-translate/core'; + +import { + hasValue, + isEmpty, +} from '../../shared/empty.util'; +import { DSpaceObject } from '../shared/dspace-object.model'; import { Metadata } from '../shared/metadata.utils'; /** @@ -9,7 +13,7 @@ import { Metadata } from '../shared/metadata.utils'; * on its render types. */ @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class DSONameService { @@ -50,12 +54,12 @@ export class DSONameService { } }, OrgUnit: (dso: DSpaceObject): string => { - return dso.firstMetadataValue('organization.legalName'); + return dso.firstMetadataValue('organization.legalName') || this.translateService.instant('dso.name.untitled'); }, Default: (dso: DSpaceObject): string => { // If object doesn't have dc.title metadata use name property return dso.firstMetadataValue('dc.title') || dso.name || this.translateService.instant('dso.name.untitled'); - } + }, }; /** @@ -106,7 +110,7 @@ export class DSONameService { } return `${familyName}, ${givenName}`; } else if (entityType === 'OrgUnit') { - return this.firstMetadataValue(object, dso, 'organization.legalName'); + return this.firstMetadataValue(object, dso, 'organization.legalName') || this.translateService.instant('dso.name.untitled'); } return this.firstMetadataValue(object, dso, 'dc.title') || dso.name || this.translateService.instant('dso.name.untitled'); } diff --git a/src/app/core/breadcrumbs/i18n-breadcrumb.resolver.spec.ts b/src/app/core/breadcrumbs/i18n-breadcrumb.resolver.spec.ts index 0d1870487ab..a85338c4908 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 { I18nBreadcrumbResolver } from './i18n-breadcrumb.resolver'; import { URLCombiner } from '../url-combiner/url-combiner'; +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; @@ -17,28 +17,28 @@ describe('I18nBreadcrumbResolver', () => { route = { data: { breadcrumbKey: i18nKey }, routeConfig: { - path: segment + path: segment, }, parent: { routeConfig: { - path: parentSegment - } - } as any + path: parentSegment, + }, + } as any, }; 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 b3fadbbaa93..5f5c779211f 100644 --- a/src/app/core/breadcrumbs/i18n-breadcrumb.resolver.ts +++ b/src/app/core/breadcrumbs/i18n-breadcrumb.resolver.ts @@ -1,32 +1,31 @@ +import { inject } from '@angular/core'; +import { + ActivatedRouteSnapshot, + ResolveFn, + RouterStateSnapshot, +} from '@angular/router'; + import { BreadcrumbConfig } from '../../breadcrumbs/breadcrumb/breadcrumb-config.model'; -import { Injectable } from '@angular/core'; -import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router'; -import { I18nBreadcrumbsService } from './i18n-breadcrumbs.service'; import { hasNoValue } from '../../shared/empty.util'; 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) { - } - - /** - * 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 }; +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'); } -} + const fullPath = currentPathFromSnapshot(route); + return { provider: breadcrumbService, key: key, url: fullPath }; +}; diff --git a/src/app/core/breadcrumbs/i18n-breadcrumbs.service.spec.ts b/src/app/core/breadcrumbs/i18n-breadcrumbs.service.spec.ts index ac2f2440370..3fcd911a464 100644 --- a/src/app/core/breadcrumbs/i18n-breadcrumbs.service.spec.ts +++ b/src/app/core/breadcrumbs/i18n-breadcrumbs.service.spec.ts @@ -1,7 +1,14 @@ -import { TestBed, waitForAsync } from '@angular/core/testing'; -import { Breadcrumb } from '../../breadcrumbs/breadcrumb/breadcrumb.model'; +import { + TestBed, + waitForAsync, +} from '@angular/core/testing'; import { getTestScheduler } from 'jasmine-marbles'; -import { BREADCRUMB_MESSAGE_POSTFIX, I18nBreadcrumbsService } from './i18n-breadcrumbs.service'; + +import { Breadcrumb } from '../../breadcrumbs/breadcrumb/breadcrumb.model'; +import { + BREADCRUMB_MESSAGE_POSTFIX, + I18nBreadcrumbsService, +} from './i18n-breadcrumbs.service'; describe('I18nBreadcrumbsService', () => { let service: I18nBreadcrumbsService; diff --git a/src/app/core/breadcrumbs/i18n-breadcrumbs.service.ts b/src/app/core/breadcrumbs/i18n-breadcrumbs.service.ts index 15563bdde8e..5746f6faf26 100644 --- a/src/app/core/breadcrumbs/i18n-breadcrumbs.service.ts +++ b/src/app/core/breadcrumbs/i18n-breadcrumbs.service.ts @@ -1,7 +1,11 @@ +import { Injectable } from '@angular/core'; +import { + Observable, + of as observableOf, +} from 'rxjs'; + import { Breadcrumb } from '../../breadcrumbs/breadcrumb/breadcrumb.model'; import { BreadcrumbsProviderService } from './breadcrumbsProviderService'; -import { Observable, of as observableOf } from 'rxjs'; -import { Injectable } from '@angular/core'; /** * The postfix for i18n breadcrumbs @@ -12,7 +16,7 @@ export const BREADCRUMB_MESSAGE_POSTFIX = '.breadcrumbs'; * Service to calculate i18n breadcrumbs for a single part of the route */ @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class I18nBreadcrumbsService implements BreadcrumbsProviderService { diff --git a/src/app/core/breadcrumbs/item-breadcrumb.resolver.ts b/src/app/core/breadcrumbs/item-breadcrumb.resolver.ts index 3005b6f09ac..ef021123d42 100644 --- a/src/app/core/breadcrumbs/item-breadcrumb.resolver.ts +++ b/src/app/core/breadcrumbs/item-breadcrumb.resolver.ts @@ -1,28 +1,35 @@ -import { Injectable } from '@angular/core'; -import { DSOBreadcrumbsService } from './dso-breadcrumbs.service'; +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 { 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 { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; -import { ITEM_PAGE_LINKS_TO_FOLLOW } from '../../item-page/item.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 new file mode 100644 index 00000000000..a6bbe49ddd9 --- /dev/null +++ b/src/app/core/breadcrumbs/navigation-breadcrumb.resolver.spec.ts @@ -0,0 +1,52 @@ +import { navigationBreadcrumbResolver } from './navigation-breadcrumb.resolver'; + +describe('navigationBreadcrumbResolver', () => { + describe('resolve', () => { + let resolver: any; + let NavigationBreadcrumbService: any; + let i18nKey: string; + let relatedI18nKey: string; + let route: any; + let expectedPath; + let state; + beforeEach(() => { + i18nKey = 'example.key'; + relatedI18nKey = 'related.key'; + route = { + data: { + breadcrumbKey: i18nKey, + relatedRoutes: [ + { + path: '', + data: { breadcrumbKey: relatedI18nKey }, + }, + ], + }, + routeConfig: { + path: 'example', + }, + parent: { + routeConfig: { + path: '', + }, + url: [{ + path: 'base', + }], + } as any, + }; + + state = { + url: '/base/example', + }; + expectedPath = '/base/example:/base'; + NavigationBreadcrumbService = {}; + resolver = navigationBreadcrumbResolver; + }); + + it('should resolve the breadcrumb config', () => { + 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 new file mode 100644 index 00000000000..ac306ee3f5c --- /dev/null +++ b/src/app/core/breadcrumbs/navigation-breadcrumb.resolver.ts @@ -0,0 +1,52 @@ +import { inject } from '@angular/core'; +import { + ActivatedRouteSnapshot, + ResolveFn, + RouterStateSnapshot, +} from '@angular/router'; + +import { BreadcrumbConfig } from '../../breadcrumbs/breadcrumb/breadcrumb-config.model'; +import { NavigationBreadcrumbsService } from './navigation-breadcrumb.service'; + +/** + * 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 + */ +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); + + 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/navigation-breadcrumb.service.ts b/src/app/core/breadcrumbs/navigation-breadcrumb.service.ts new file mode 100644 index 00000000000..2da8b06eab7 --- /dev/null +++ b/src/app/core/breadcrumbs/navigation-breadcrumb.service.ts @@ -0,0 +1,34 @@ +import { Injectable } from '@angular/core'; +import { + Observable, + of as observableOf, +} from 'rxjs'; + +import { Breadcrumb } from '../../breadcrumbs/breadcrumb/breadcrumb.model'; +import { BreadcrumbsProviderService } from './breadcrumbsProviderService'; + +/** + * The postfix for i18n breadcrumbs + */ +export const BREADCRUMB_MESSAGE_POSTFIX = '.breadcrumbs'; + +/** + * Service to calculate i18n breadcrumbs for a single part of the route + */ +@Injectable({ + providedIn: 'root', +}) +export class NavigationBreadcrumbsService implements BreadcrumbsProviderService { + + /** + * Method to calculate the breadcrumbs + * @param key The key used to resolve the breadcrumb + * @param url The url to use as a link for this breadcrumb + */ + getBreadcrumbs(key: string, url: string): Observable { + const keys = key.split(':'); + const urls = url.split(':'); + const breadcrumbs = keys.map((currentKey, index) => new Breadcrumb(currentKey + BREADCRUMB_MESSAGE_POSTFIX, urls[index] )); + return observableOf(breadcrumbs.reverse()); + } +} diff --git a/src/app/core/breadcrumbs/navigation-breadcrumbs.service.spec.ts b/src/app/core/breadcrumbs/navigation-breadcrumbs.service.spec.ts new file mode 100644 index 00000000000..646b967fe5b --- /dev/null +++ b/src/app/core/breadcrumbs/navigation-breadcrumbs.service.spec.ts @@ -0,0 +1,47 @@ +import { + TestBed, + waitForAsync, +} from '@angular/core/testing'; +import { getTestScheduler } from 'jasmine-marbles'; + +import { Breadcrumb } from '../../breadcrumbs/breadcrumb/breadcrumb.model'; +import { BREADCRUMB_MESSAGE_POSTFIX } from './i18n-breadcrumbs.service'; +import { NavigationBreadcrumbsService } from './navigation-breadcrumb.service'; + +describe('NavigationBreadcrumbsService', () => { + let service: NavigationBreadcrumbsService; + let exampleString; + let exampleURL; + let childrenString; + let childrenUrl; + let parentString; + let parentUrl; + + function init() { + exampleString = 'example.string:parent.string'; + exampleURL = 'example.com:parent.com'; + childrenString = 'example.string'; + childrenUrl = 'example.com'; + parentString = 'parent.string'; + parentUrl = 'parent.com'; + } + + beforeEach(waitForAsync(() => { + init(); + TestBed.configureTestingModule({}).compileComponents(); + })); + + beforeEach(() => { + service = new NavigationBreadcrumbsService(); + }); + + describe('getBreadcrumbs', () => { + it('should return an array of breadcrumbs based on strings by adding the postfix', () => { + const breadcrumbs = service.getBreadcrumbs(exampleString, exampleURL); + getTestScheduler().expectObservable(breadcrumbs).toBe('(a|)', { a: [ + new Breadcrumb(childrenString + BREADCRUMB_MESSAGE_POSTFIX, childrenUrl), + new Breadcrumb(parentString + BREADCRUMB_MESSAGE_POSTFIX, parentUrl), + ].reverse() }); + }); + }); +}); diff --git a/src/app/core/breadcrumbs/publication-claim-breadcrumb.resolver.spec.ts b/src/app/core/breadcrumbs/publication-claim-breadcrumb.resolver.spec.ts new file mode 100644 index 00000000000..7c2c34d4790 --- /dev/null +++ b/src/app/core/breadcrumbs/publication-claim-breadcrumb.resolver.spec.ts @@ -0,0 +1,31 @@ +import { publicationClaimBreadcrumbResolver } from './publication-claim-breadcrumb.resolver'; + +describe('publicationClaimBreadcrumbResolver', () => { + describe('resolve', () => { + let resolver: any; + let publicationClaimBreadcrumbService: any; + const fullPath = '/test/publication-claim/openaire:6bee076d-4f2a-4555-a475-04a267769b2a'; + const expectedKey = '6bee076d-4f2a-4555-a475-04a267769b2a'; + const expectedId = 'openaire:6bee076d-4f2a-4555-a475-04a267769b2a'; + let route; + + beforeEach(() => { + route = { + paramMap: { + get: function (param) { + return this[param]; + }, + targetId: expectedId, + }, + }; + publicationClaimBreadcrumbService = {}; + resolver = publicationClaimBreadcrumbResolver; + }); + + it('should resolve the breadcrumb config', () => { + 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 new file mode 100644 index 00000000000..a1b52ce333f --- /dev/null +++ b/src/app/core/breadcrumbs/publication-claim-breadcrumb.resolver.ts @@ -0,0 +1,18 @@ +import { inject } from '@angular/core'; +import { + ActivatedRouteSnapshot, + ResolveFn, + RouterStateSnapshot, +} from '@angular/router'; + +import { BreadcrumbConfig } from '../../breadcrumbs/breadcrumb/breadcrumb-config.model'; +import { PublicationClaimBreadcrumbService } from './publication-claim-breadcrumb.service'; + +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/publication-claim-breadcrumb.service.spec.ts b/src/app/core/breadcrumbs/publication-claim-breadcrumb.service.spec.ts new file mode 100644 index 00000000000..8424b5edda8 --- /dev/null +++ b/src/app/core/breadcrumbs/publication-claim-breadcrumb.service.spec.ts @@ -0,0 +1,55 @@ +import { + TestBed, + waitForAsync, +} from '@angular/core/testing'; +import { getTestScheduler } from 'jasmine-marbles'; +import { of } from 'rxjs'; + +import { Breadcrumb } from '../../breadcrumbs/breadcrumb/breadcrumb.model'; +import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; +import { PublicationClaimBreadcrumbService } from './publication-claim-breadcrumb.service'; + +describe('PublicationClaimBreadcrumbService', () => { + let service: PublicationClaimBreadcrumbService; + let dsoNameService: any = { + getName: (str) => str, + }; + let translateService: any = { + instant: (str) => str, + }; + + let dataService: any = { + findById: (str) => createSuccessfulRemoteDataObject$(str), + }; + + let authorizationService: any = { + isAuthorized: (str) => of(true), + }; + + let exampleKey; + + const ADMIN_PUBLICATION_CLAIMS_PATH = 'admin/notifications/publication-claim'; + const ADMIN_PUBLICATION_CLAIMS_BREADCRUMB_KEY = 'admin.notifications.publicationclaim.page.title'; + + function init() { + exampleKey = 'suggestion.suggestionFor.breadcrumb'; + } + + beforeEach(waitForAsync(() => { + init(); + TestBed.configureTestingModule({}).compileComponents(); + })); + + beforeEach(() => { + service = new PublicationClaimBreadcrumbService(dataService,dsoNameService,translateService, authorizationService); + }); + + describe('getBreadcrumbs', () => { + it('should return a breadcrumb based on a string', () => { + const breadcrumbs = service.getBreadcrumbs(exampleKey); + getTestScheduler().expectObservable(breadcrumbs).toBe('(a|)', { a: [new Breadcrumb(ADMIN_PUBLICATION_CLAIMS_BREADCRUMB_KEY, ADMIN_PUBLICATION_CLAIMS_PATH), + new Breadcrumb(exampleKey, undefined)], + }); + }); + }); +}); diff --git a/src/app/core/breadcrumbs/publication-claim-breadcrumb.service.ts b/src/app/core/breadcrumbs/publication-claim-breadcrumb.service.ts new file mode 100644 index 00000000000..43b7ed5761b --- /dev/null +++ b/src/app/core/breadcrumbs/publication-claim-breadcrumb.service.ts @@ -0,0 +1,48 @@ +import { Injectable } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; +import { + combineLatest, + Observable, +} from 'rxjs'; +import { map } from 'rxjs/operators'; + +import { Breadcrumb } from '../../breadcrumbs/breadcrumb/breadcrumb.model'; +import { AuthorizationDataService } from '../data/feature-authorization/authorization-data.service'; +import { FeatureID } from '../data/feature-authorization/feature-id'; +import { ItemDataService } from '../data/item-data.service'; +import { getFirstCompletedRemoteData } from '../shared/operators'; +import { BreadcrumbsProviderService } from './breadcrumbsProviderService'; +import { DSONameService } from './dso-name.service'; + +/** + * Service to calculate Publication claims breadcrumbs + */ +@Injectable({ + providedIn: 'root', +}) +export class PublicationClaimBreadcrumbService implements BreadcrumbsProviderService { + private ADMIN_PUBLICATION_CLAIMS_PATH = 'admin/notifications/publication-claim'; + private ADMIN_PUBLICATION_CLAIMS_BREADCRUMB_KEY = 'admin.notifications.publicationclaim.page.title'; + + constructor(private dataService: ItemDataService, + private dsoNameService: DSONameService, + private tranlsateService: TranslateService, + protected authorizationService: AuthorizationDataService) { + } + + + /** + * Method to calculate the breadcrumbs + * @param key The key used to resolve the breadcrumb + */ + getBreadcrumbs(key: string): Observable { + return combineLatest([this.dataService.findById(key).pipe(getFirstCompletedRemoteData()),this.authorizationService.isAuthorized(FeatureID.AdministratorOf)]).pipe( + map(([item, isAdmin]) => { + const itemName = this.dsoNameService.getName(item.payload); + return isAdmin ? [new Breadcrumb(this.tranlsateService.instant(this.ADMIN_PUBLICATION_CLAIMS_BREADCRUMB_KEY), this.ADMIN_PUBLICATION_CLAIMS_PATH), + new Breadcrumb(this.tranlsateService.instant('suggestion.suggestionFor.breadcrumb', { name: itemName }), undefined)] : + [new Breadcrumb(this.tranlsateService.instant('suggestion.suggestionFor.breadcrumb', { name: itemName }), undefined)]; + }), + ); + } +} diff --git a/src/app/core/breadcrumbs/quality-assurance-breadcrumb.resolver.spec.ts b/src/app/core/breadcrumbs/quality-assurance-breadcrumb.resolver.spec.ts new file mode 100644 index 00000000000..fe2fe77e7f4 --- /dev/null +++ b/src/app/core/breadcrumbs/quality-assurance-breadcrumb.resolver.spec.ts @@ -0,0 +1,31 @@ +import { qualityAssuranceBreadcrumbResolver } from './quality-assurance-breadcrumb.resolver'; + +describe('qualityAssuranceBreadcrumbResolver', () => { + describe('resolve', () => { + let resolver: any; + let qualityAssuranceBreadcrumbService: any; + let route: any; + const fullPath = '/test/quality-assurance/'; + const expectedKey = 'testSourceId:testTopicId'; + + beforeEach(() => { + route = { + paramMap: { + get: function (param) { + return this[param]; + }, + sourceId: 'testSourceId', + topicId: 'testTopicId', + }, + }; + qualityAssuranceBreadcrumbService = {}; + resolver = qualityAssuranceBreadcrumbResolver; + }); + + it('should resolve the breadcrumb config', () => { + 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 new file mode 100644 index 00000000000..6507a75de66 --- /dev/null +++ b/src/app/core/breadcrumbs/quality-assurance-breadcrumb.resolver.ts @@ -0,0 +1,27 @@ +import { inject } from '@angular/core'; +import { + ActivatedRouteSnapshot, + ResolveFn, + RouterStateSnapshot, +} from '@angular/router'; + +import { BreadcrumbConfig } from '../../breadcrumbs/breadcrumb/breadcrumb-config.model'; +import { QualityAssuranceBreadcrumbService } from './quality-assurance-breadcrumb.service'; + +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; + + 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/breadcrumbs/quality-assurance-breadcrumb.service.spec.ts b/src/app/core/breadcrumbs/quality-assurance-breadcrumb.service.spec.ts new file mode 100644 index 00000000000..f8d30754cad --- /dev/null +++ b/src/app/core/breadcrumbs/quality-assurance-breadcrumb.service.spec.ts @@ -0,0 +1,43 @@ +import { + TestBed, + waitForAsync, +} from '@angular/core/testing'; +import { getTestScheduler } from 'jasmine-marbles'; + +import { Breadcrumb } from '../../breadcrumbs/breadcrumb/breadcrumb.model'; +import { QualityAssuranceBreadcrumbService } from './quality-assurance-breadcrumb.service'; + +describe('QualityAssuranceBreadcrumbService', () => { + let service: QualityAssuranceBreadcrumbService; + let translateService: any = { + instant: (str) => str, + }; + + let exampleString; + let exampleURL; + let exampleQaKey; + + function init() { + exampleString = 'sourceId'; + exampleURL = '/test/quality-assurance/'; + exampleQaKey = 'admin.quality-assurance.breadcrumbs'; + } + + beforeEach(waitForAsync(() => { + init(); + TestBed.configureTestingModule({}).compileComponents(); + })); + + beforeEach(() => { + service = new QualityAssuranceBreadcrumbService(translateService); + }); + + describe('getBreadcrumbs', () => { + it('should return a breadcrumb based on a string', () => { + const breadcrumbs = service.getBreadcrumbs(exampleString, exampleURL); + getTestScheduler().expectObservable(breadcrumbs).toBe('(a|)', { a: [new Breadcrumb(exampleQaKey, exampleURL), + new Breadcrumb(exampleString, exampleURL + exampleString)], + }); + }); + }); +}); diff --git a/src/app/core/breadcrumbs/quality-assurance-breadcrumb.service.ts b/src/app/core/breadcrumbs/quality-assurance-breadcrumb.service.ts new file mode 100644 index 00000000000..580a5e5f8ee --- /dev/null +++ b/src/app/core/breadcrumbs/quality-assurance-breadcrumb.service.ts @@ -0,0 +1,47 @@ +import { Injectable } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; +import { + Observable, + of as observableOf, +} from 'rxjs'; + +import { Breadcrumb } from '../../breadcrumbs/breadcrumb/breadcrumb.model'; +import { BreadcrumbsProviderService } from './breadcrumbsProviderService'; + +/** + * Service to calculate QA breadcrumbs for a single part of the route + */ +@Injectable({ + providedIn: 'root', +}) +export class QualityAssuranceBreadcrumbService implements BreadcrumbsProviderService { + + private QUALITY_ASSURANCE_BREADCRUMB_KEY = 'admin.quality-assurance.breadcrumbs'; + constructor( + private translationService: TranslateService, + ) { + + } + + + /** + * Method to calculate the breadcrumbs + * @param key The key used to resolve the breadcrumb + * @param url The url to use as a link for this breadcrumb + */ + getBreadcrumbs(key: string, url: string): Observable { + const args = key.split(':'); + const sourceId = args[0]; + const topicId = args.length > 2 ? args[args.length - 1] : args[1]; + + if (topicId) { + return observableOf( [new Breadcrumb(this.translationService.instant(this.QUALITY_ASSURANCE_BREADCRUMB_KEY), url), + new Breadcrumb(sourceId, `${url}${sourceId}`), + new Breadcrumb(topicId, undefined)]); + } else { + return observableOf([new Breadcrumb(this.translationService.instant(this.QUALITY_ASSURANCE_BREADCRUMB_KEY), url), + new Breadcrumb(sourceId, `${url}${sourceId}`)]); + } + + } +} diff --git a/src/app/core/browse/browse-definition-data.service.spec.ts b/src/app/core/browse/browse-definition-data.service.spec.ts index f321c2551cd..affa63a5480 100644 --- a/src/app/core/browse/browse-definition-data.service.spec.ts +++ b/src/app/core/browse/browse-definition-data.service.spec.ts @@ -1,11 +1,12 @@ -import { BrowseDefinitionDataService } from './browse-definition-data.service'; -import { followLink } from '../../shared/utils/follow-link-config.model'; import { EMPTY } from 'rxjs'; -import { FindListOptions } from '../data/find-list-options.model'; + +import { getMockObjectCacheService } from '../../shared/mocks/object-cache.service.mock'; import { getMockRemoteDataBuildService } from '../../shared/mocks/remote-data-build.service.mock'; -import { RequestService } from '../data/request.service'; import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub'; -import { getMockObjectCacheService } from '../../shared/mocks/object-cache.service.mock'; +import { followLink } from '../../shared/utils/follow-link-config.model'; +import { FindListOptions } from '../data/find-list-options.model'; +import { RequestService } from '../data/request.service'; +import { BrowseDefinitionDataService } from './browse-definition-data.service'; describe(`BrowseDefinitionDataService`, () => { let requestService: RequestService; @@ -18,7 +19,7 @@ describe(`BrowseDefinitionDataService`, () => { const options = new FindListOptions(); const linksToFollow = [ followLink('entries'), - followLink('items') + followLink('items'), ]; function initTestService() { diff --git a/src/app/core/browse/browse-definition-data.service.ts b/src/app/core/browse/browse-definition-data.service.ts index bc495a51f4f..9c0d0d16c95 100644 --- a/src/app/core/browse/browse-definition-data.service.ts +++ b/src/app/core/browse/browse-definition-data.service.ts @@ -1,33 +1,45 @@ // eslint-disable-next-line max-classes-per-file import { Injectable } from '@angular/core'; -import { BROWSE_DEFINITION } from '../shared/browse-definition.resource-type'; -import { RequestService } from '../data/request.service'; +import { + Observable, + of as observableOf, +} from 'rxjs'; +import { take } from 'rxjs/operators'; + +import { + hasValue, + isNotEmpty, + isNotEmptyOperator, +} from '../../shared/empty.util'; +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 { HALEndpointService } from '../shared/hal-endpoint.service'; -import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; -import { Observable, of as observableOf } from 'rxjs'; -import { RemoteData } from '../data/remote-data'; -import { PaginatedList } from '../data/paginated-list.model'; -import { FindListOptions } from '../data/find-list-options.model'; +import { + FindAllData, + FindAllDataImpl, +} from '../data/base/find-all-data'; import { IdentifiableDataService } from '../data/base/identifiable-data.service'; -import { FindAllData, FindAllDataImpl } from '../data/base/find-all-data'; -import { dataService } from '../data/base/data-service.decorator'; -import { isNotEmpty, isNotEmptyOperator, hasValue } from '../../shared/empty.util'; -import { take } from 'rxjs/operators'; +import { + SearchData, + SearchDataImpl, +} from '../data/base/search-data'; +import { FindListOptions } from '../data/find-list-options.model'; +import { PaginatedList } from '../data/paginated-list.model'; +import { RemoteData } from '../data/remote-data'; import { BrowseDefinitionRestRequest } from '../data/request.models'; -import { RequestParam } from '../cache/models/request-param.model'; -import { SearchData, SearchDataImpl } from '../data/base/search-data'; +import { RequestService } from '../data/request.service'; import { BrowseDefinition } from '../shared/browse-definition.model'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; /** * Create a GET request for the given href, and send it. * Use a GET request specific for BrowseDefinitions. */ export const createAndSendBrowseDefinitionGetRequest = (requestService: RequestService, - responseMsToLive: number, - href$: string | Observable, - useCachedVersionIfAvailable: boolean = true): void => { + responseMsToLive: number, + href$: string | Observable, + useCachedVersionIfAvailable: boolean = true): void => { if (isNotEmpty(href$)) { if (typeof href$ === 'string') { href$ = observableOf(href$); @@ -35,7 +47,7 @@ export const createAndSendBrowseDefinitionGetRequest = (requestService: RequestS href$.pipe( isNotEmptyOperator(), - take(1) + take(1), ).subscribe((href: string) => { const requestId = requestService.generateRequestId(); const request = new BrowseDefinitionRestRequest(requestId, href); @@ -62,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; @@ -150,7 +161,7 @@ export class BrowseDefinitionDataService extends IdentifiableDataService { let scheduler: TestScheduler; let service: BrowseService; let requestService: RequestService; - let rdbService: RemoteDataBuildService; const browsesEndpointURL = 'https://rest.api/browses'; const halService: any = new HALEndpointServiceStub(browsesEndpointURL); @@ -31,26 +39,26 @@ describe('BrowseService', () => { sortOptions: [ { name: 'title', - metadata: 'dc.title' + metadata: 'dc.title', }, { name: 'dateissued', - metadata: 'dc.date.issued' + metadata: 'dc.date.issued', }, { name: 'dateaccessioned', - metadata: 'dc.date.accessioned' - } + metadata: 'dc.date.accessioned', + }, ], defaultSortOrder: 'ASC', type: 'browse', metadataKeys: [ - 'dc.date.issued' + 'dc.date.issued', ], _links: { self: { href: 'https://rest.api/discover/browses/dateissued' }, - items: { href: 'https://rest.api/discover/browses/dateissued/items' } - } + items: { href: 'https://rest.api/discover/browses/dateissued/items' }, + }, }), Object.assign(new ValueListBrowseDefinition(), { id: 'author', @@ -58,28 +66,28 @@ describe('BrowseService', () => { sortOptions: [ { name: 'title', - metadata: 'dc.title' + metadata: 'dc.title', }, { name: 'dateissued', - metadata: 'dc.date.issued' + metadata: 'dc.date.issued', }, { name: 'dateaccessioned', - metadata: 'dc.date.accessioned' - } + metadata: 'dc.date.accessioned', + }, ], defaultSortOrder: 'ASC', type: 'browse', metadataKeys: [ 'dc.contributor.*', - 'dc.creator' + 'dc.creator', ], _links: { self: { href: 'https://rest.api/discover/browses/author' }, entries: { href: 'https://rest.api/discover/browses/author/entries' }, - items: { href: 'https://rest.api/discover/browses/author/items' } - } + items: { href: 'https://rest.api/discover/browses/author/items' }, + }, }), Object.assign(new HierarchicalBrowseDefinition(), { id: 'srsc', @@ -88,14 +96,14 @@ describe('BrowseService', () => { vocabulary: 'srsc', type: 'browse', metadata: [ - 'dc.subject' + 'dc.subject', ], _links: { vocabulary: { 'href': 'https://rest.api/submission/vocabularies/srsc/' }, items: { 'href': 'https://rest.api/discover/browses/srsc/items' }, entries: { 'href': 'https://rest.api/discover/browses/srsc/entries' }, - self: { 'href': 'https://rest.api/discover/browses/srsc' } - } + self: { 'href': 'https://rest.api/discover/browses/srsc' }, + }, }), ]; @@ -104,13 +112,13 @@ describe('BrowseService', () => { const getRequestEntry$ = (successful: boolean) => { return observableOf({ - response: { isSuccessful: successful, payload: browseDefinitions } as any + response: { isSuccessful: successful, payload: browseDefinitions } as any, } as RequestEntry); }; function initTestService() { browseDefinitionDataService = jasmine.createSpyObj('browseDefinitionDataService', { - findAll: createSuccessfulRemoteDataObject$(createPaginatedList(browseDefinitions)) + findAll: createSuccessfulRemoteDataObject$(createPaginatedList(browseDefinitions)), }); hrefOnlyDataService = getMockHrefOnlyDataService(); return new BrowseService( @@ -118,7 +126,6 @@ describe('BrowseService', () => { halService, browseDefinitionDataService, hrefOnlyDataService, - rdbService ); } @@ -130,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', () => { @@ -151,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', () => { @@ -164,7 +167,7 @@ describe('BrowseService', () => { scheduler.flush(); expect(getFirstUsedArgumentOfSpyMethod(hrefOnlyDataService.findListByHref)).toBeObservable(cold('(a|)', { - a: expected + a: expected, })); }); @@ -178,7 +181,7 @@ describe('BrowseService', () => { scheduler.flush(); expect(getFirstUsedArgumentOfSpyMethod(hrefOnlyDataService.findListByHref)).toBeObservable(cold('(a|)', { - a: expected + a: expected, })); }); @@ -193,7 +196,7 @@ describe('BrowseService', () => { scheduler.flush(); expect(getFirstUsedArgumentOfSpyMethod(hrefOnlyDataService.findListByHref)).toBeObservable(cold('(a|)', { - a: expected + a: expected, })); }); }); @@ -204,11 +207,10 @@ describe('BrowseService', () => { describe('if getBrowseDefinitions fires', () => { beforeEach(() => { requestService = getMockRequestService(getRequestEntry$(true)); - rdbService = getMockRemoteDataBuildService(); service = initTestService(); spyOn(service, 'getBrowseDefinitions').and .returnValue(hot('--a-', { - a: createSuccessfulRemoteDataObject(createPaginatedList(browseDefinitions)) + a: createSuccessfulRemoteDataObject(createPaginatedList(browseDefinitions)), })); }); @@ -259,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('----')); @@ -277,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', () => { @@ -290,7 +289,7 @@ describe('BrowseService', () => { scheduler.flush(); expect(getFirstUsedArgumentOfSpyMethod(hrefOnlyDataService.findListByHref)).toBeObservable(cold('(a|)', { - a: expectedURL + a: expectedURL, })); }); diff --git a/src/app/core/browse/browse.service.ts b/src/app/core/browse/browse.service.ts index b210b349494..5fe06a700e5 100644 --- a/src/app/core/browse/browse.service.ts +++ b/src/app/core/browse/browse.service.ts @@ -1,39 +1,57 @@ import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; -import { distinctUntilChanged, map, startWith } from 'rxjs/operators'; -import { hasValue, hasValueOperator, isEmpty, isNotEmpty } from '../../shared/empty.util'; -import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { + distinctUntilChanged, + map, + startWith, +} from 'rxjs/operators'; + +import { environment } from '../../../environments/environment'; +import { + hasValue, + hasValueOperator, + isEmpty, + isNotEmpty, +} from '../../shared/empty.util'; +import { + followLink, + FollowLinkConfig, +} from '../../shared/utils/follow-link-config.model'; +import { SortDirection } from '../cache/models/sort-options.model'; +import { HrefOnlyDataService } from '../data/href-only-data.service'; import { PaginatedList } from '../data/paginated-list.model'; import { RemoteData } from '../data/remote-data'; import { RequestService } from '../data/request.service'; import { BrowseDefinition } from '../shared/browse-definition.model'; -import { FlatBrowseDefinition } from '../shared/flat-browse-definition.model'; import { BrowseEntry } from '../shared/browse-entry.model'; +import { FlatBrowseDefinition } from '../shared/flat-browse-definition.model'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { Item } from '../shared/item.model'; import { getBrowseDefinitionLinks, getFirstOccurrence, - getRemoteDataPayload, getFirstSucceededRemoteData, - getPaginatedListPayload + getPaginatedListPayload, + getRemoteDataPayload, } from '../shared/operators'; import { URLCombiner } from '../url-combiner/url-combiner'; -import { BrowseEntrySearchOptions } from './browse-entry-search-options.model'; -import { HrefOnlyDataService } from '../data/href-only-data.service'; -import { followLink, FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; import { BrowseDefinitionDataService } from './browse-definition-data.service'; -import { SortDirection } from '../cache/models/sort-options.model'; - +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'; @@ -55,7 +73,6 @@ export class BrowseService { protected halService: HALEndpointService, private browseDefinitionDataService: BrowseDefinitionDataService, private hrefOnlyDataService: HrefOnlyDataService, - private rdb: RemoteDataBuildService, ) { } @@ -102,10 +119,10 @@ export class BrowseService { href = new URLCombiner(href, `?${args.join('&')}`).toString(); } return href; - }) + }), ); if (options.fetchThumbnail ) { - return this.hrefOnlyDataService.findListByHref(href$, {}, null, null, ...BROWSE_LINKS_TO_FOLLOW); + return this.hrefOnlyDataService.findListByHref(href$, {}, undefined, undefined, ...getBrowseLinksToFollow()); } return this.hrefOnlyDataService.findListByHref(href$); } @@ -153,7 +170,7 @@ export class BrowseService { }), ); if (options.fetchThumbnail) { - return this.hrefOnlyDataService.findListByHref(href$, {}, null, null, ...BROWSE_LINKS_TO_FOLLOW); + return this.hrefOnlyDataService.findListByHref(href$, {}, undefined, undefined, ...getBrowseLinksToFollow()); } return this.hrefOnlyDataService.findListByHref(href$); } @@ -187,12 +204,12 @@ export class BrowseService { href = new URLCombiner(href, `?${args.join('&')}`).toString(); } return href; - }) + }), ); return this.hrefOnlyDataService.findListByHref(href$).pipe( getFirstSucceededRemoteData(), - getFirstOccurrence() + getFirstOccurrence(), ); } @@ -248,7 +265,7 @@ export class BrowseService { } return isNotEmpty(matchingKeys); - }) + }), ), map((def: BrowseDefinition) => { if (isEmpty(def) || isEmpty(def._links) || isEmpty(def._links[linkPath])) { @@ -258,7 +275,7 @@ export class BrowseService { } }), startWith(undefined), - distinctUntilChanged() + distinctUntilChanged(), ); } diff --git a/src/app/core/cache/builders/build-decorators.spec.ts b/src/app/core/cache/builders/build-decorators.spec.ts index 150a07f0066..53f4cb2f7f3 100644 --- a/src/app/core/cache/builders/build-decorators.spec.ts +++ b/src/app/core/cache/builders/build-decorators.spec.ts @@ -1,7 +1,12 @@ import { HALLink } from '../../shared/hal-link.model'; import { HALResource } from '../../shared/hal-resource.model'; import { ResourceType } from '../../shared/resource-type'; -import { getLinkDefinition, link } from './build-decorators'; +import { + dataService, + getDataServiceFor, + getLinkDefinition, + link, +} from './build-decorators'; class TestHALResource implements HALResource { _links: { @@ -46,5 +51,17 @@ describe('build decorators', () => { expect(result).toBeUndefined(); }); }); + + describe(`set data service`, () => { + it(`should throw error`, () => { + expect(dataService(null)).toThrow(); + }); + + it(`should set properly data service for type`, () => { + const target = new TestHALResource(); + dataService(testType)(target); + expect(getDataServiceFor(testType)).toEqual(target); + }); + }); }); }); diff --git a/src/app/core/cache/builders/build-decorators.ts b/src/app/core/cache/builders/build-decorators.ts index 9e5ebaed854..be3ffc0f4d7 100644 --- a/src/app/core/cache/builders/build-decorators.ts +++ b/src/app/core/cache/builders/build-decorators.ts @@ -1,25 +1,34 @@ -import { hasNoValue, hasValue } from '../../../shared/empty.util'; +import { InjectionToken } from '@angular/core'; +import { + hasNoValue, + hasValue, +} from '../../../shared/empty.util'; import { GenericConstructor } from '../../shared/generic-constructor'; import { HALResource } from '../../shared/hal-resource.model'; import { ResourceType } from '../../shared/resource-type'; +import { CacheableObject } from '../cacheable-object.model'; import { getResourceTypeValueFor } from '../object-cache.reducer'; -import { InjectionToken } from '@angular/core'; import { TypedObject } from '../typed-object.model'; +export const DATA_SERVICE_FACTORY = new InjectionToken<(resourceType: ResourceType) => GenericConstructor>('getDataServiceFor', { + providedIn: 'root', + factory: () => getDataServiceFor, +}); export const LINK_DEFINITION_FACTORY = new InjectionToken<(source: GenericConstructor, linkName: keyof T['_links']) => LinkDefinition>('getLinkDefinition', { providedIn: 'root', factory: () => getLinkDefinition, }); export const LINK_DEFINITION_MAP_FACTORY = new InjectionToken<(source: GenericConstructor) => Map>>('getLinkDefinitions', { providedIn: 'root', - factory: () => getLinkDefinitions + factory: () => getLinkDefinitions, }); const resolvedLinkKey = Symbol('resolvedLink'); const resolvedLinkMap = new Map(); const typeMap = new Map(); +const dataServiceMap = new Map(); const linkMap = new Map(); /** @@ -38,6 +47,39 @@ export function getClassForType(type: string | ResourceType) { return typeMap.get(getResourceTypeValueFor(type)); } +/** + * A class decorator to indicate that this class is a dataservice + * for a given resource type. + * + * "dataservice" in this context means that it has findByHref and + * findAllByHref methods. + * + * @param resourceType the resource type the class is a dataservice for + */ +export function dataService(resourceType: ResourceType): any { + return (target: any) => { + 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) { + return dataServiceMap.get(resourceType.value); +} + /** * A class to represent the data that can be set by the @link decorator */ @@ -65,7 +107,7 @@ export const link = ( resourceType: ResourceType, isList = false, linkName?: keyof T['_links'], - ) => { +) => { return (target: T, propertyName: string) => { let targetMap = linkMap.get(target.constructor); @@ -81,7 +123,7 @@ export const link = ( resourceType, isList, linkName, - propertyName + propertyName, }); linkMap.set(target.constructor, targetMap); diff --git a/src/app/core/cache/builders/link.service.spec.ts b/src/app/core/cache/builders/link.service.spec.ts index 0ddfe058701..d9878913888 100644 --- a/src/app/core/cache/builders/link.service.spec.ts +++ b/src/app/core/cache/builders/link.service.spec.ts @@ -1,15 +1,21 @@ /* eslint-disable max-classes-per-file */ -import { Injectable } from '@angular/core'; import { TestBed } from '@angular/core/testing'; -import { followLink, FollowLinkConfig } from '../../../shared/utils/follow-link-config.model'; +import { + 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'; +import { + LINK_DEFINITION_FACTORY, + LINK_DEFINITION_MAP_FACTORY, +} from './build-decorators'; import { LinkService } from './link.service'; -import { LINK_DEFINITION_FACTORY, LINK_DEFINITION_MAP_FACTORY } from './build-decorators'; -import { isEmpty } from 'rxjs/operators'; -import { FindListOptions } from '../../data/find-list-options.model'; -import { DATA_SERVICE_FACTORY } from '../../data/base/data-service.decorator'; const TEST_MODEL = new ResourceType('testmodel'); let result: any; @@ -31,16 +37,9 @@ class TestModel implements HALResource { successor?: 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; @@ -54,48 +53,54 @@ describe('LinkService', () => { value: 'a test value', _links: { self: { - href: 'http://self.link' + href: 'http://self.link', }, predecessor: { - href: 'http://predecessor.link' + href: 'http://predecessor.link', }, successor: { - href: 'http://successor.link' + href: 'http://successor.link', }, - } + }, }); testDataService = new TestDataService(); spyOn(testDataService, 'findListByHref').and.callThrough(); spyOn(testDataService, 'findByHref').and.callThrough(); 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([ + { + resourceType: TEST_MODEL, + linkName: 'predecessor', + propertyName: 'predecessor', + }, + { + resourceType: TEST_MODEL, + linkName: 'successor', + propertyName: 'successor', + }, + ]), + }, + ], }); service = TestBed.inject(LinkService); }); @@ -103,10 +108,13 @@ 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 linkdefinition concerns a list`, () => { @@ -115,12 +123,15 @@ describe('LinkService', () => { resourceType: TEST_MODEL, linkName: 'predecessor', propertyName: 'predecessor', - isList: true + 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', () => { @@ -132,15 +143,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(); + }); }); }); @@ -157,12 +167,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(); + }, + }); }); }); }); @@ -213,12 +227,12 @@ describe('LinkService', () => { value: 'a test value', _links: { self: { - href: 'http://self.link' + href: 'http://self.link', }, predecessor: { - href: 'http://predecessor.link' - } - } + href: 'http://predecessor.link', + }, + }, }); }); @@ -227,8 +241,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(); + }); }); }); @@ -237,7 +254,7 @@ describe('LinkService', () => { ((service as any).getLinkDefinition as jasmine.Spy).and.returnValue({ resourceType: TEST_MODEL, linkName: 'successor', - propertyName: 'successor' + propertyName: 'successor', }); result = service.resolveLinks(testModel, followLink('successor')); }); diff --git a/src/app/core/cache/builders/link.service.ts b/src/app/core/cache/builders/link.service.ts index afc7ab88e40..6265e89d532 100644 --- a/src/app/core/cache/builders/link.service.ts +++ b/src/app/core/cache/builders/link.service.ts @@ -1,32 +1,48 @@ -import { Inject, Injectable, Injector } from '@angular/core'; -import { hasNoValue, hasValue, isNotEmpty } from '../../../shared/empty.util'; +import { + Inject, + Injectable, + Injector, +} from '@angular/core'; +import { + EMPTY, + Observable, +} from 'rxjs'; +import { + catchError, + switchMap, +} from 'rxjs/operators'; + +import { + 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 { 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 { DATA_SERVICE_FACTORY } from '../../data/base/data-service.decorator'; import { LINK_DEFINITION_FACTORY, LINK_DEFINITION_MAP_FACTORY, LinkDefinition, } from './build-decorators'; -import { RemoteData } from '../../data/remote-data'; -import { EMPTY, Observable } from 'rxjs'; -import { ResourceType } from '../../shared/resource-type'; -import { HALDataService } from '../../data/base/hal-data-service.interface'; -import { PaginatedList } from '../../data/paginated-list.model'; /** * 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>, ) { @@ -55,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)}`); } diff --git a/src/app/core/cache/builders/remote-data-build.service.spec.ts b/src/app/core/cache/builders/remote-data-build.service.spec.ts index d9b856bb777..ec756ce85eb 100644 --- a/src/app/core/cache/builders/remote-data-build.service.spec.ts +++ b/src/app/core/cache/builders/remote-data-build.service.spec.ts @@ -1,26 +1,43 @@ -import { createFailedRemoteDataObject, createPendingRemoteDataObject, createSuccessfulRemoteDataObject } from '../../../shared/remote-data.utils'; -import { buildPaginatedList, PaginatedList } from '../../data/paginated-list.model'; -import { Item } from '../../shared/item.model'; -import { PageInfo } from '../../shared/page-info.model'; -import { RemoteDataBuildService } from './remote-data-build.service'; -import { ObjectCacheService } from '../object-cache.service'; -import { ITEM } from '../../shared/item.resource-type'; +import { + fakeAsync, + tick, +} from '@angular/core/testing'; +import { cold } from 'jasmine-marbles'; +import { + Observable, + of as observableOf, +} from 'rxjs'; +import { take } from 'rxjs/operators'; +import { TestScheduler } from 'rxjs/testing'; + import { getMockLinkService } from '../../../shared/mocks/link-service.mock'; -import { getMockRequestService } from '../../../shared/mocks/request.service.mock'; import { getMockObjectCacheService } from '../../../shared/mocks/object-cache.service.mock'; -import { LinkService } from './link.service'; -import { RequestService } from '../../data/request.service'; -import { UnCacheableObject } from '../../shared/uncacheable-object.model'; +import { getMockRequestService } from '../../../shared/mocks/request.service.mock'; +import { + createFailedRemoteDataObject, + createPendingRemoteDataObject, + createSuccessfulRemoteDataObject, +} from '../../../shared/remote-data.utils'; +import { + followLink, + FollowLinkConfig, +} from '../../../shared/utils/follow-link-config.model'; +import { + buildPaginatedList, + PaginatedList, +} from '../../data/paginated-list.model'; import { RemoteData } from '../../data/remote-data'; -import { Observable, of as observableOf } from 'rxjs'; -import { followLink, FollowLinkConfig } from '../../../shared/utils/follow-link-config.model'; -import { take } from 'rxjs/operators'; -import { HALLink } from '../../shared/hal-link.model'; -import { RequestEntryState } from '../../data/request-entry-state.model'; +import { RequestService } from '../../data/request.service'; import { RequestEntry } from '../../data/request-entry.model'; -import { cold } from 'jasmine-marbles'; -import { TestScheduler } from 'rxjs/testing'; -import { fakeAsync, tick } from '@angular/core/testing'; +import { RequestEntryState } from '../../data/request-entry-state.model'; +import { HALLink } from '../../shared/hal-link.model'; +import { Item } from '../../shared/item.model'; +import { ITEM } from '../../shared/item.resource-type'; +import { PageInfo } from '../../shared/page-info.model'; +import { UnCacheableObject } from '../../shared/uncacheable-object.model'; +import { ObjectCacheService } from '../object-cache.service'; +import { LinkService } from './link.service'; +import { RemoteDataBuildService } from './remote-data-build.service'; describe('RemoteDataBuildService', () => { let service: RemoteDataBuildService; @@ -49,7 +66,7 @@ describe('RemoteDataBuildService', () => { linkService = getMockLinkService(); requestService = getMockRequestService(); unCacheableObject = { - foo: 'bar' + foo: 'bar', }; pageInfo = new PageInfo(); selfLink1 = 'https://rest.api/some/object'; @@ -64,31 +81,31 @@ describe('RemoteDataBuildService', () => { 'dc.title': [ { language: 'en_US', - value: 'Item nr 1' - } - ] + value: 'Item nr 1', + }, + ], }, _links: { self: { - href: selfLink1 - } - } + href: selfLink1, + }, + }, }), Object.assign(new Item(), { metadata: { 'dc.title': [ { language: 'en_US', - value: 'Item nr 2' - } - ] + value: 'Item nr 2', + }, + ], }, _links: { self: { - href: selfLink2 - } - } - }) + href: selfLink2, + }, + }, + }), ]; paginatedList = buildPaginatedList(pageInfo, array); normalizedPaginatedList = buildPaginatedList(pageInfo, array, true); @@ -96,43 +113,43 @@ describe('RemoteDataBuildService', () => { paginatedListRD = createSuccessfulRemoteDataObject(paginatedList); entrySuccessCacheable = { request: { - uuid: '17820127-0ee5-4ed4-b6da-e654bdff8487' + uuid: '17820127-0ee5-4ed4-b6da-e654bdff8487', }, state: RequestEntryState.Success, response: { statusCode: 200, payloadLink: { - href: selfLink1 - } - } + href: selfLink1, + }, + }, } as RequestEntry; entrySuccessUnCacheable = { request: { - uuid: '0aa5ec06-d6a7-4e73-952e-1e0462bd1501' + uuid: '0aa5ec06-d6a7-4e73-952e-1e0462bd1501', }, state: RequestEntryState.Success, response: { statusCode: 200, unCacheableObject, - } + }, } as RequestEntry; entrySuccessNoContent = { request: { - uuid: '780a7295-6102-4a43-9775-80f2a4ff673c' + uuid: '780a7295-6102-4a43-9775-80f2a4ff673c', }, state: RequestEntryState.Success, response: { - statusCode: 204 + statusCode: 204, }, } as RequestEntry; entryError = { request: { - uuid: '1609dcbc-8442-4877-966e-864f151cc40c' + uuid: '1609dcbc-8442-4877-966e-864f151cc40c', }, state: RequestEntryState.Error, response: { statusCode: 500, - } + }, } as RequestEntry; requestEntry$ = observableOf(entrySuccessCacheable); linksToFollow = [ @@ -427,8 +444,8 @@ describe('RemoteDataBuildService', () => { beforeEach(() => { entry = { response: { - payloadLink: { href: 'payload-link' } - } + payloadLink: { href: 'payload-link' }, + }, }; }); @@ -441,8 +458,8 @@ describe('RemoteDataBuildService', () => { beforeEach(() => { entry = { response: { - payloadLink: undefined - } + payloadLink: undefined, + }, }; }); @@ -459,8 +476,8 @@ describe('RemoteDataBuildService', () => { beforeEach(() => { entry = { response: { - unCacheableObject: Object.assign({}) - } + unCacheableObject: Object.assign({}), + }, }; }); @@ -472,7 +489,7 @@ describe('RemoteDataBuildService', () => { describe('when the entry\'s response doesn\'t contain an uncacheable object', () => { beforeEach(() => { entry = { - response: {} + response: {}, }; }); @@ -487,7 +504,7 @@ describe('RemoteDataBuildService', () => { it(`should return a new instance of that type`, () => { const source: any = { type: ITEM, - uuid: 'some-uuid' + uuid: 'some-uuid', }; const result = (service as any).plainObjectToInstance(source); @@ -503,7 +520,7 @@ describe('RemoteDataBuildService', () => { it(`should return a new plain JS object`, () => { const source: any = { type: 'foobar', - uuid: 'some-uuid' + uuid: 'some-uuid', }; const result = (service as any).plainObjectToInstance(source); @@ -528,7 +545,7 @@ describe('RemoteDataBuildService', () => { beforeEach(() => { paginatedLinksToFollow = [ followLink('page', {}, ...linksToFollow), - ...linksToFollow + ...linksToFollow, ]; }); describe(`and the given list doesn't have a page property already`, () => { @@ -843,15 +860,15 @@ describe('RemoteDataBuildService', () => { it('should only emit after the callback is done', () => { testScheduler.run(({ cold: tsCold, expectObservable }) => { buildFromRequestUUIDSpy.and.returnValue( - tsCold('-p----s', RDs) + tsCold('-p----s', RDs), ); callback.and.returnValue( - tsCold(' --t', BOOLEAN) + tsCold(' --t', BOOLEAN), ); const done$ = service.buildFromRequestUUIDAndAwait('some-href', callback); expectObservable(done$).toBe( - ' -p------s', RDs // resulting duration between pending & successful includes the callback + ' -p------s', RDs, // resulting duration between pending & successful includes the callback ); }); }); 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 075bf3ca0ca..36305b4a0c4 100644 --- a/src/app/core/cache/builders/remote-data-build.service.ts +++ b/src/app/core/cache/builders/remote-data-build.service.ts @@ -5,29 +5,52 @@ import { Observable, of as observableOf, } from 'rxjs'; -import { map, switchMap, filter, distinctUntilKeyChanged, startWith } from 'rxjs/operators'; -import { hasValue, isEmpty, isNotEmpty, hasNoValue, isUndefined } from '../../../shared/empty.util'; +import { + distinctUntilKeyChanged, + filter, + map, + startWith, + switchMap, +} from 'rxjs/operators'; + +import { + hasNoValue, + hasValue, + isEmpty, + isNotEmpty, + isUndefined, +} from '../../../shared/empty.util'; import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; -import { FollowLinkConfig, followLink } from '../../../shared/utils/follow-link-config.model'; +import { + followLink, + FollowLinkConfig, +} from '../../../shared/utils/follow-link-config.model'; import { PaginatedList } from '../../data/paginated-list.model'; +import { PAGINATED_LIST } from '../../data/paginated-list.resource-type'; import { RemoteData } from '../../data/remote-data'; import { RequestService } from '../../data/request.service'; -import { ObjectCacheService } from '../object-cache.service'; -import { LinkService } from './link.service'; -import { HALLink } from '../../shared/hal-link.model'; -import { GenericConstructor } from '../../shared/generic-constructor'; -import { getClassForType } from './build-decorators'; -import { HALResource } from '../../shared/hal-resource.model'; -import { PAGINATED_LIST } from '../../data/paginated-list.resource-type'; -import { getUrlWithoutEmbedParams } from '../../index/index.selectors'; -import { getResourceTypeValueFor } from '../object-cache.reducer'; -import { hasSucceeded, isStale, RequestEntryState } from '../../data/request-entry-state.model'; -import { getRequestFromRequestHref, getRequestFromRequestUUID } from '../../shared/request.operators'; import { RequestEntry } from '../../data/request-entry.model'; +import { + hasSucceeded, + isStale, + RequestEntryState, +} from '../../data/request-entry-state.model'; import { ResponseState } from '../../data/response-state.model'; +import { getUrlWithoutEmbedParams } from '../../index/index.selectors'; +import { GenericConstructor } from '../../shared/generic-constructor'; +import { HALLink } from '../../shared/hal-link.model'; +import { HALResource } from '../../shared/hal-resource.model'; import { getFirstCompletedRemoteData } from '../../shared/operators'; +import { + getRequestFromRequestHref, + getRequestFromRequestUUID, +} from '../../shared/request.operators'; +import { getResourceTypeValueFor } from '../object-cache.reducer'; +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, @@ -77,7 +100,7 @@ export class RemoteDataBuildService { } } return [obj]; - }) + }), ); } @@ -151,7 +174,7 @@ export class RemoteDataBuildService { paginatedList.page = page .map((obj: any) => this.plainObjectToInstance(obj)) .map((obj: any) => - this.linkService.resolveLinks(obj, ...pageLink.linksToFollow) + this.linkService.resolveLinks(obj, ...pageLink.linksToFollow), ); if (isNotEmpty(otherLinks)) { return this.linkService.resolveLinks(paginatedList, ...otherLinks); @@ -161,9 +184,10 @@ export class RemoteDataBuildService { } else { // in case the elements of the paginated list were already filled in, because they're UnCacheableObjects paginatedList.page = paginatedList.page + .filter((obj: any) => obj != null) .map((obj: any) => this.plainObjectToInstance(obj)) .map((obj: any) => - this.linkService.resolveLinks(obj, ...pageLink.linksToFollow) + this.linkService.resolveLinks(obj, ...pageLink.linksToFollow), ); if (isNotEmpty(otherLinks)) { return observableOf(this.linkService.resolveLinks(paginatedList, ...otherLinks)); @@ -229,7 +253,7 @@ export class RemoteDataBuildService { } else { return [rd]; } - }) + }), ); } @@ -272,12 +296,13 @@ export class RemoteDataBuildService { return isStale(r2.state) ? r1 : r2; } }), - distinctUntilKeyChanged('lastUpdated') ); const payload$ = this.buildPayload(requestEntry$, href$, ...linksToFollow); - return this.toRemoteDataObservable(requestEntry$, payload$); + return this.toRemoteDataObservable(requestEntry$, payload$).pipe( + distinctUntilKeyChanged('lastUpdated'), + ); } /** @@ -293,12 +318,12 @@ export class RemoteDataBuildService { toRemoteDataObservable(requestEntry$: Observable, payload$: Observable) { return observableCombineLatest([ requestEntry$, - payload$ + payload$, ]).pipe( filter(([entry,payload]: [RequestEntry, T]) => hasValue(entry) && // filter out cases where the state is successful, but the payload isn't yet set - !(hasSucceeded(entry.state) && isUndefined(payload)) + !(hasSucceeded(entry.state) && isUndefined(payload)), ), map(([entry, payload]: [RequestEntry, T]) => { let response = entry.response; @@ -313,9 +338,9 @@ export class RemoteDataBuildService { entry.state, response.errorMessage, payload, - response.statusCode + response.statusCode, ); - }) + }), ); } @@ -406,7 +431,7 @@ export class RemoteDataBuildService { state, errorMessage, payload, - statusCode + statusCode, ); })); } diff --git a/src/app/core/cache/cacheable-object.model.ts b/src/app/core/cache/cacheable-object.model.ts index b7d1609d585..86d041dab77 100644 --- a/src/app/core/cache/cacheable-object.model.ts +++ b/src/app/core/cache/cacheable-object.model.ts @@ -1,6 +1,6 @@ /* tslint:disable:max-classes-per-file */ -import { HALResource } from '../shared/hal-resource.model'; import { HALLink } from '../shared/hal-link.model'; +import { HALResource } from '../shared/hal-resource.model'; import { TypedObject } from './typed-object.model'; /** diff --git a/src/app/core/cache/models/request-param.model.ts b/src/app/core/cache/models/request-param.model.ts index ac21fe0b8a8..b78fa45e337 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/object-cache.actions.ts b/src/app/core/cache/object-cache.actions.ts index c18a20ffd63..5f8f60e1f1f 100644 --- a/src/app/core/cache/object-cache.actions.ts +++ b/src/app/core/cache/object-cache.actions.ts @@ -1,8 +1,8 @@ /* eslint-disable max-classes-per-file */ import { Action } from '@ngrx/store'; +import { Operation } from 'fast-json-patch'; import { type } from '../../shared/ngrx/type'; -import { Operation } from 'fast-json-patch'; import { CacheableObject } from './cacheable-object.model'; /** @@ -15,7 +15,7 @@ export const ObjectCacheActionTypes = { ADD_PATCH: type('dspace/core/cache/object/ADD_PATCH'), APPLY_PATCH: type('dspace/core/cache/object/APPLY_PATCH'), ADD_DEPENDENTS: type('dspace/core/cache/object/ADD_DEPENDENTS'), - REMOVE_DEPENDENTS: type('dspace/core/cache/object/REMOVE_DEPENDENTS') + REMOVE_DEPENDENTS: type('dspace/core/cache/object/REMOVE_DEPENDENTS'), }; /** diff --git a/src/app/core/cache/object-cache.effects.spec.ts b/src/app/core/cache/object-cache.effects.spec.ts index 3a50a5dbc74..66270be4c26 100644 --- a/src/app/core/cache/object-cache.effects.spec.ts +++ b/src/app/core/cache/object-cache.effects.spec.ts @@ -1,10 +1,14 @@ import { TestBed } from '@angular/core/testing'; -import { Observable } from 'rxjs'; import { provideMockActions } from '@ngrx/effects/testing'; -import { cold, hot } from 'jasmine-marbles'; -import { ObjectCacheEffects } from './object-cache.effects'; -import { ResetObjectCacheTimestampsAction } from './object-cache.actions'; +import { + cold, + hot, +} from 'jasmine-marbles'; +import { Observable } from 'rxjs'; + import { StoreActionTypes } from '../../store.actions'; +import { ResetObjectCacheTimestampsAction } from './object-cache.actions'; +import { ObjectCacheEffects } from './object-cache.effects'; describe('ObjectCacheEffects', () => { let cacheEffects: ObjectCacheEffects; diff --git a/src/app/core/cache/object-cache.effects.ts b/src/app/core/cache/object-cache.effects.ts index fa2bf6f6902..0de59a152cb 100644 --- a/src/app/core/cache/object-cache.effects.ts +++ b/src/app/core/cache/object-cache.effects.ts @@ -1,6 +1,10 @@ -import { map } from 'rxjs/operators'; import { Injectable } from '@angular/core'; -import { Actions, createEffect, ofType } from '@ngrx/effects'; +import { + Actions, + createEffect, + ofType, +} from '@ngrx/effects'; +import { map } from 'rxjs/operators'; import { StoreActionTypes } from '../../store.actions'; import { ResetObjectCacheTimestampsAction } from './object-cache.actions'; @@ -16,9 +20,9 @@ export class ObjectCacheEffects { * This assumes that the server cached everything a negligible * time ago, and will likely need to be revisited later */ - fixTimestampsOnRehydrate = createEffect(() => this.actions$ + fixTimestampsOnRehydrate = createEffect(() => this.actions$ .pipe(ofType(StoreActionTypes.REHYDRATE), - map(() => new ResetObjectCacheTimestampsAction(new Date().getTime())) + map(() => new ResetObjectCacheTimestampsAction(new Date().getTime())), )); constructor(private actions$: Actions) { diff --git a/src/app/core/cache/object-cache.reducer.spec.ts b/src/app/core/cache/object-cache.reducer.spec.ts index 919edc8e577..7dda02a0f5d 100644 --- a/src/app/core/cache/object-cache.reducer.spec.ts +++ b/src/app/core/cache/object-cache.reducer.spec.ts @@ -1,6 +1,7 @@ // eslint-disable-next-line import/no-namespace import * as deepFreeze from 'deep-freeze'; import { Operation } from 'fast-json-patch'; + import { Item } from '../shared/item.model'; import { AddDependentsObjectCacheAction, @@ -11,7 +12,6 @@ import { RemoveFromObjectCacheAction, ResetObjectCacheTimestampsAction, } from './object-cache.actions'; - import { objectCacheReducer } from './object-cache.reducer'; class NullAction extends RemoveFromObjectCacheAction { @@ -54,7 +54,7 @@ describe('objectCacheReducer', () => { type: Item.type, self: selfLink2, foo: 'baz', - _links: { self: { href: selfLink2 } } + _links: { self: { href: selfLink2 } }, }, alternativeLinks: [altLink3, altLink4], timeCompleted: new Date().getTime(), @@ -62,8 +62,8 @@ describe('objectCacheReducer', () => { requestUUIDs: [requestUUID2], dependentRequestUUIDs: [requestUUID1], patches: [], - isDirty: false - } + isDirty: false, + }, }; deepFreeze(testState); @@ -126,6 +126,10 @@ describe('objectCacheReducer', () => { deepFreeze(state); objectCacheReducer(state, action); + + // no expect required, deepFreeze will ensure an exception is thrown if the state + // is mutated, and any uncaught exception will cause the test to fail + expect().nothing(); }); it('should remove the specified object from the cache in response to the REMOVE action', () => { @@ -149,6 +153,10 @@ describe('objectCacheReducer', () => { const action = new RemoveFromObjectCacheAction(selfLink1); // testState has already been frozen above objectCacheReducer(testState, action); + + // no expect required, deepFreeze will ensure an exception is thrown if the state + // is mutated, and any uncaught exception will cause the test to fail + expect().nothing(); }); it('should set the timestamp of all objects in the cache in response to a RESET_TIMESTAMPS action', () => { @@ -164,16 +172,24 @@ describe('objectCacheReducer', () => { const action = new ResetObjectCacheTimestampsAction(new Date().getTime()); // testState has already been frozen above objectCacheReducer(testState, action); + + // no expect required, deepFreeze will ensure an exception is thrown if the state + // is mutated, and any uncaught exception will cause the test to fail + expect().nothing(); }); it('should perform the ADD_PATCH action without affecting the previous state', () => { const action = new AddPatchObjectCacheAction(selfLink1, [{ op: 'replace', path: '/name', - value: 'random string' + value: 'random string', }]); // testState has already been frozen above objectCacheReducer(testState, action); + + // no expect required, deepFreeze will ensure an exception is thrown if the state + // is mutated, and any uncaught exception will cause the test to fail + expect().nothing(); }); it('should when the ADD_PATCH action dispatched', () => { diff --git a/src/app/core/cache/object-cache.reducer.ts b/src/app/core/cache/object-cache.reducer.ts index dc3f50db68f..1a780ff0083 100644 --- a/src/app/core/cache/object-cache.reducer.ts +++ b/src/app/core/cache/object-cache.reducer.ts @@ -1,18 +1,26 @@ /* eslint-disable max-classes-per-file */ +import { + applyPatch, + Operation, +} from 'fast-json-patch'; + +import { + hasValue, + isNotEmpty, +} from '../../shared/empty.util'; +import { CacheEntry } from './cache-entry'; +import { CacheableObject } from './cacheable-object.model'; import { AddDependentsObjectCacheAction, AddPatchObjectCacheAction, AddToObjectCacheAction, ApplyPatchObjectCacheAction, ObjectCacheAction, - ObjectCacheActionTypes, RemoveDependentsObjectCacheAction, + ObjectCacheActionTypes, + RemoveDependentsObjectCacheAction, RemoveFromObjectCacheAction, ResetObjectCacheTimestampsAction, } from './object-cache.actions'; -import { hasValue, isNotEmpty } from '../../shared/empty.util'; -import { CacheEntry } from './cache-entry'; -import { applyPatch, Operation } from 'fast-json-patch'; -import { CacheableObject } from './cacheable-object.model'; /** * An interface to represent a JsonPatch @@ -166,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; + } } /** @@ -217,7 +230,7 @@ function resetObjectCacheTimestamps(state: ObjectCacheState, action: ResetObject const newState = Object.create(null); Object.keys(state).forEach((key) => { newState[key] = Object.assign({}, state[key], { - timeCompleted: action.payload + timeCompleted: action.payload, }); }); return newState; @@ -241,7 +254,7 @@ function addPatchObjectCache(state: ObjectCacheState, action: AddPatchObjectCach const patches = newState[uuid].patches; newState[uuid] = Object.assign({}, newState[uuid], { patches: [...patches, { operations } as Patch], - isDirty: true + isDirty: true, }); } return newState; @@ -286,8 +299,8 @@ function addDependentsObjectCacheState(state: ObjectCacheState, action: AddDepen ...new Set([ ...newState[href]?.dependentRequestUUIDs || [], ...action.payload.dependentRequestUUIDs, - ]) - ] + ]), + ], }); } @@ -296,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 @@ -308,7 +321,7 @@ function removeDependentsObjectCacheState(state: ObjectCacheState, action: Remov if (hasValue(newState[href])) { newState[href] = Object.assign({}, newState[href], { - dependentRequestUUIDs: [] + dependentRequestUUIDs: [], }); } diff --git a/src/app/core/cache/object-cache.service.spec.ts b/src/app/core/cache/object-cache.service.spec.ts index 6af797be299..3d27f7252c5 100644 --- a/src/app/core/cache/object-cache.service.spec.ts +++ b/src/app/core/cache/object-cache.service.spec.ts @@ -1,33 +1,44 @@ -import { TestBed, waitForAsync } from '@angular/core/testing'; - -import { cold } from 'jasmine-marbles'; -import { Store, StoreModule } from '@ngrx/store'; -import { MockStore, provideMockStore } from '@ngrx/store/testing'; +import { + TestBed, + waitForAsync, +} from '@angular/core/testing'; +import { + Store, + StoreModule, +} from '@ngrx/store'; +import { + MockStore, + provideMockStore, +} from '@ngrx/store/testing'; import { Operation } from 'fast-json-patch'; -import { empty, of as observableOf } from 'rxjs'; +import { cold } from 'jasmine-marbles'; +import { TestColdObservable } from 'jasmine-marbles/src/test-observables'; +import { + empty, + of as observableOf, +} from 'rxjs'; import { first } from 'rxjs/operators'; +import { TestScheduler } from 'rxjs/testing'; -import { coreReducers} from '../core.reducers'; +import { storeModuleConfig } from '../../app.reducer'; +import { coreReducers } from '../core.reducers'; +import { CoreState } from '../core-state.model'; import { RestRequestMethod } from '../data/rest-request-method'; +import { RemoveFromIndexBySubstringAction } from '../index/index.actions'; +import { IndexName } from '../index/index-name.model'; +import { HALLink } from '../shared/hal-link.model'; import { Item } from '../shared/item.model'; import { AddDependentsObjectCacheAction, - RemoveDependentsObjectCacheAction, AddPatchObjectCacheAction, AddToObjectCacheAction, ApplyPatchObjectCacheAction, + RemoveDependentsObjectCacheAction, RemoveFromObjectCacheAction, } from './object-cache.actions'; import { Patch } from './object-cache.reducer'; import { ObjectCacheService } from './object-cache.service'; import { AddToSSBAction } from './server-sync-buffer.actions'; -import { RemoveFromIndexBySubstringAction } from '../index/index.actions'; -import { HALLink } from '../shared/hal-link.model'; -import { storeModuleConfig } from '../../app.reducer'; -import { TestColdObservable } from 'jasmine-marbles/src/test-observables'; -import { IndexName } from '../index/index-name.model'; -import { CoreState } from '../core-state.model'; -import { TestScheduler } from 'rxjs/testing'; describe('ObjectCacheService', () => { let service: ObjectCacheService; @@ -69,8 +80,8 @@ describe('ObjectCacheService', () => { type: Item.type, _links: { self: { href: selfLink }, - anotherLink: { href: anotherLink } - } + anotherLink: { href: anotherLink }, + }, }; cacheEntry = { data: objectToCache, @@ -96,8 +107,8 @@ describe('ObjectCacheService', () => { 'cache/syncbuffer': {}, 'cache/object-updates': {}, 'data/request': {}, - 'index': {} - } + 'index': {}, + }, }; } @@ -105,12 +116,12 @@ describe('ObjectCacheService', () => { TestBed.configureTestingModule({ imports: [ - StoreModule.forRoot(coreReducers, storeModuleConfig) + StoreModule.forRoot(coreReducers, storeModuleConfig), ], providers: [ provideMockStore({ initialState }), - { provide: ObjectCacheService, useValue: service } - ] + { provide: ObjectCacheService, useValue: service }, + ], }).compileComponents(); })); @@ -120,7 +131,7 @@ describe('ObjectCacheService', () => { mockStore = store as MockStore; mockStore.setState(initialState); linkServiceStub = { - removeResolvedLinks: (a) => a + removeResolvedLinks: (a) => a, }; spyOn(linkServiceStub, 'removeResolvedLinks').and.callThrough(); spyOn(store, 'dispatch'); @@ -209,7 +220,7 @@ describe('ObjectCacheService', () => { describe('getList', () => { it('should return an observable of the array of cached objects with the specified self link and type', () => { const item = Object.assign(new Item(), { - _links: { self: { href: selfLink } } + _links: { self: { href: selfLink } }, }); spyOn(service, 'getObjectByHref').and.returnValue(observableOf(item)); @@ -251,7 +262,7 @@ describe('ObjectCacheService', () => { 'something', 'something-else', 'specific-request', - ] + ], }))); }); @@ -266,7 +277,7 @@ describe('ObjectCacheService', () => { requestUUIDs: [ 'something', 'something-else', - ] + ], }))); }); @@ -292,9 +303,9 @@ describe('ObjectCacheService', () => { const state = Object.assign({}, initialState, { core: Object.assign({}, initialState.core, { 'cache/object': { - [selfLink]: cacheEntry - } - }) + [selfLink]: cacheEntry, + }, + }), }); mockStore.setState(state); const expected: TestColdObservable = cold('a', { a: cacheEntry }); @@ -310,14 +321,14 @@ describe('ObjectCacheService', () => { const state = Object.assign({}, initialState, { core: Object.assign({}, initialState.core, { 'cache/object': { - [selfLink]: cacheEntry + [selfLink]: cacheEntry, }, 'index': { 'object/alt-link-to-self-link': { - [anotherLink]: selfLink - } - } - }) + [anotherLink]: selfLink, + }, + }, + }), }); mockStore.setState(state); (service as any).getByAlternativeLink(anotherLink).subscribe(); @@ -335,8 +346,8 @@ describe('ObjectCacheService', () => { it('isDirty should return true when the patches list in the cache entry is not empty', () => { cacheEntry.patches = [ { - operations: operations - } as Patch + operations: operations, + } as Patch, ]; const result = (service as any).isDirty(cacheEntry); expect(result).toBe(true); @@ -371,9 +382,9 @@ describe('ObjectCacheService', () => { [anotherLink]: selfLink, ['objectWithoutDependentsAlt']: 'objectWithoutDependents', ['objectWithDependentsAlt']: 'objectWithDependents', - } - } - }) + }, + }, + }), }); mockStore.setState(state); }); @@ -421,11 +432,11 @@ describe('ObjectCacheService', () => { testScheduler.run(({ cold: tsCold, flush }) => { const href$ = tsCold('--y-n-n', { y: selfLink, - n: 'NOPE' + n: 'NOPE', }); const dependsOnHref$ = tsCold('-y-n-n', { y: 'objectWithoutDependents', - n: 'NOPE' + n: 'NOPE', }); service.addDependency(href$, dependsOnHref$); diff --git a/src/app/core/cache/object-cache.service.ts b/src/app/core/cache/object-cache.service.ts index 9ca02162108..f645b5a878d 100644 --- a/src/app/core/cache/object-cache.service.ts +++ b/src/app/core/cache/object-cache.service.ts @@ -1,32 +1,68 @@ import { Injectable } from '@angular/core'; -import { createSelector, MemoizedSelector, select, Store } from '@ngrx/store'; -import { applyPatch, Operation } from 'fast-json-patch'; -import { combineLatest as observableCombineLatest, Observable, of as observableOf } from 'rxjs'; - -import { distinctUntilChanged, filter, map, mergeMap, switchMap, take } from 'rxjs/operators'; -import { hasNoValue, hasValue, isEmpty, isNotEmpty } from '../../shared/empty.util'; -import { CoreState } from '../core-state.model'; +import { + createSelector, + MemoizedSelector, + select, + Store, +} from '@ngrx/store'; +import { + applyPatch, + Operation, +} from 'fast-json-patch'; +import { + combineLatest as observableCombineLatest, + Observable, + of as observableOf, +} from 'rxjs'; +import { + distinctUntilChanged, + filter, + map, + mergeMap, + switchMap, + take, +} from 'rxjs/operators'; + +import { + hasNoValue, + hasValue, + isEmpty, + isNotEmpty, +} from '../../shared/empty.util'; import { coreSelector } from '../core.selectors'; +import { CoreState } from '../core-state.model'; import { RestRequestMethod } from '../data/rest-request-method'; -import { selfLinkFromAlternativeLinkSelector, selfLinkFromUuidSelector } from '../index/index.selectors'; +import { RemoveFromIndexBySubstringAction } from '../index/index.actions'; +import { + selfLinkFromAlternativeLinkSelector, + selfLinkFromUuidSelector, +} from '../index/index.selectors'; +import { IndexName } from '../index/index-name.model'; import { GenericConstructor } from '../shared/generic-constructor'; +import { HALLink } from '../shared/hal-link.model'; import { getClassForType } from './builders/build-decorators'; import { LinkService } from './builders/link.service'; -import { AddDependentsObjectCacheAction, AddPatchObjectCacheAction, AddToObjectCacheAction, ApplyPatchObjectCacheAction, RemoveDependentsObjectCacheAction, RemoveFromObjectCacheAction } from './object-cache.actions'; - -import { ObjectCacheEntry, ObjectCacheState } from './object-cache.reducer'; -import { AddToSSBAction } from './server-sync-buffer.actions'; -import { RemoveFromIndexBySubstringAction } from '../index/index.actions'; -import { HALLink } from '../shared/hal-link.model'; import { CacheableObject } from './cacheable-object.model'; -import { IndexName } from '../index/index-name.model'; +import { + AddDependentsObjectCacheAction, + AddPatchObjectCacheAction, + AddToObjectCacheAction, + ApplyPatchObjectCacheAction, + RemoveDependentsObjectCacheAction, + RemoveFromObjectCacheAction, +} from './object-cache.actions'; +import { + ObjectCacheEntry, + ObjectCacheState, +} from './object-cache.reducer'; +import { AddToSSBAction } from './server-sync-buffer.actions'; /** * The base selector function to select the object cache in the store */ const objectCacheSelector = createSelector( coreSelector, - (state: CoreState) => state['cache/object'] + (state: CoreState) => state['cache/object'], ); /** @@ -42,11 +78,11 @@ const entryFromSelfLinkSelector = /** * A service to interact with the object cache */ -@Injectable() +@Injectable({ providedIn: 'root' }) export class ObjectCacheService { constructor( private store: Store, - private linkService: LinkService + private linkService: LinkService, ) { } @@ -63,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)); } @@ -82,12 +120,12 @@ export class ObjectCacheService { const cacheEntry$ = this.getByHref(href); const altLinks$ = cacheEntry$.pipe(map((entry: ObjectCacheEntry) => entry.alternativeLinks), take(1)); const childLinks$ = cacheEntry$.pipe(map((entry: ObjectCacheEntry) => { - return Object - .entries(entry.data._links) - .filter(([key, value]: [string, HALLink]) => key !== 'self') - .map(([key, value]: [string, HALLink]) => value.href); - }), - take(1) + return Object + .entries(entry.data._links) + .filter(([key, value]: [string, HALLink]) => key !== 'self') + .map(([key, value]: [string, HALLink]) => value.href); + }), + take(1), ); this.removeLinksFromAlternativeLinkIndex(altLinks$); this.removeLinksFromAlternativeLinkIndex(childLinks$); @@ -96,8 +134,8 @@ export class ObjectCacheService { private removeLinksFromAlternativeLinkIndex(links$: Observable) { links$.subscribe((links: string[]) => links.forEach((link: string) => { - this.store.dispatch(new RemoveFromIndexBySubstringAction(IndexName.ALTERNATIVE_OBJECT_LINK, link)); - } + this.store.dispatch(new RemoveFromIndexBySubstringAction(IndexName.ALTERNATIVE_OBJECT_LINK, link)); + }, )); } @@ -113,8 +151,8 @@ export class ObjectCacheService { Observable { return this.store.pipe( select(selfLinkFromUuidSelector(uuid)), - mergeMap((selfLink: string) => this.getObjectByHref(selfLink) - ) + mergeMap((selfLink: string) => this.getObjectByHref(selfLink), + ), ); } @@ -129,22 +167,26 @@ export class ObjectCacheService { getObjectByHref(href: string): Observable { return this.getByHref(href).pipe( map((entry: ObjectCacheEntry) => { - if (isNotEmpty(entry.patches)) { - const flatPatch: Operation[] = [].concat(...entry.patches.map((patch) => patch.operations)); - const patchedData = applyPatch(entry.data, flatPatch, undefined, false).newDocument; - return Object.assign({}, entry, { data: patchedData }); - } else { - return entry; - } + if (isNotEmpty(entry.patches)) { + const flatPatch: Operation[] = [].concat(...entry.patches.map((patch) => patch.operations)); + const patchedData = applyPatch(entry.data, flatPatch, undefined, false).newDocument; + return Object.assign({}, entry, { data: patchedData }); + } else { + return entry; } + }, ), 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; - }) + }), ); } @@ -162,13 +204,13 @@ export class ObjectCacheService { this.getBySelfLink(href), ]).pipe( map((results: ObjectCacheEntry[]) => results.find((entry: ObjectCacheEntry) => hasValue(entry))), - filter((entry: ObjectCacheEntry) => hasValue(entry)) + filter((entry: ObjectCacheEntry) => hasValue(entry)), ); } private getBySelfLink(selfLink: string): Observable { return this.store.pipe( - select(entryFromSelfLinkSelector(selfLink)) + select(entryFromSelfLinkSelector(selfLink)), ); } @@ -204,7 +246,7 @@ export class ObjectCacheService { getRequestUUIDByObjectUUID(uuid: string): Observable { return this.store.pipe( select(selfLinkFromUuidSelector(uuid)), - mergeMap((selfLink: string) => this.getRequestUUIDBySelfLink(selfLink)) + mergeMap((selfLink: string) => this.getRequestUUIDBySelfLink(selfLink)), ); } @@ -232,7 +274,7 @@ export class ObjectCacheService { return observableOf([]); } else { return observableCombineLatest( - selfLinks.map((selfLink: string) => this.getObjectByHref(selfLink)) + selfLinks.map((selfLink: string) => this.getObjectByHref(selfLink)), ); } } @@ -252,7 +294,7 @@ export class ObjectCacheService { /* NB: that this is only a solution because the select method is synchronous, see: https://github.com/ngrx/store/issues/296#issuecomment-269032571*/ this.store.pipe( select(selfLinkFromUuidSelector(uuid)), - take(1) + take(1), ).subscribe((selfLink: string) => result = this.hasByHref(selfLink)); return result; @@ -290,9 +332,9 @@ export class ObjectCacheService { hasByHref$(href: string): Observable { return observableCombineLatest( this.getBySelfLink(href), - this.getByAlternativeLink(href) + this.getByAlternativeLink(href), ).pipe( - map((entries: ObjectCacheEntry[]) => entries.some((entry) => hasValue(entry))) + map((entries: ObjectCacheEntry[]) => entries.some((entry) => hasValue(entry))), ); } @@ -358,7 +400,7 @@ export class ObjectCacheService { observableCombineLatest([ href$, dependsOnHref$.pipe( - switchMap(dependsOnHref => this.resolveSelfLink(dependsOnHref)) + switchMap(dependsOnHref => this.resolveSelfLink(dependsOnHref)), ), ]).pipe( switchMap(([href, dependsOnSelfLink]: [string, string]) => { @@ -373,7 +415,7 @@ export class ObjectCacheService { this.getByHref(href).pipe( // only add the latest request to keep dependency index from growing indefinitely map((entry: ObjectCacheEntry) => entry?.requestUUIDs?.[0]), - ) + ), ]); }), take(1), diff --git a/src/app/core/cache/response.models.ts b/src/app/core/cache/response.models.ts index 197bf130fb2..9a09a49bc81 100644 --- a/src/app/core/cache/response.models.ts +++ b/src/app/core/cache/response.models.ts @@ -1,10 +1,10 @@ /* eslint-disable max-classes-per-file */ -import { PageInfo } from '../shared/page-info.model'; import { ConfigObject } from '../config/models/config.model'; +import { RequestError } from '../data/request-error.model'; import { DSpaceObject } from '../shared/dspace-object.model'; import { HALLink } from '../shared/hal-link.model'; +import { PageInfo } from '../shared/page-info.model'; import { UnCacheableObject } from '../shared/uncacheable-object.model'; -import { RequestError } from '../data/request-error.model'; export class RestResponse { public toCache = true; @@ -13,7 +13,7 @@ export class RestResponse { constructor( public isSuccessful: boolean, public statusCode: number, - public statusText: string + public statusText: string, ) { } } @@ -29,7 +29,7 @@ export class DSOSuccessResponse extends RestResponse { public resourceSelfLinks: string[], public statusCode: number, public statusText: string, - public pageInfo?: PageInfo + public pageInfo?: PageInfo, ) { super(true, statusCode, statusText); } @@ -43,7 +43,7 @@ export class EndpointMapSuccessResponse extends RestResponse { constructor( public endpointMap: EndpointMap, public statusCode: number, - public statusText: string + public statusText: string, ) { super(true, statusCode, statusText); } @@ -64,7 +64,7 @@ export class ConfigSuccessResponse extends RestResponse { public configDefinition: ConfigObject, public statusCode: number, public statusText: string, - public pageInfo?: PageInfo + public pageInfo?: PageInfo, ) { super(true, statusCode, statusText); } @@ -78,7 +78,7 @@ export class TokenResponse extends RestResponse { public token: string, public isSuccessful: boolean, public statusCode: number, - public statusText: string + public statusText: string, ) { super(isSuccessful, statusCode, statusText); } @@ -89,7 +89,7 @@ export class PostPatchSuccessResponse extends RestResponse { public dataDefinition: any, public statusCode: number, public statusText: string, - public pageInfo?: PageInfo + public pageInfo?: PageInfo, ) { super(true, statusCode, statusText); } @@ -100,7 +100,7 @@ export class EpersonSuccessResponse extends RestResponse { public epersonDefinition: DSpaceObject[], public statusCode: number, public statusText: string, - public pageInfo?: PageInfo + public pageInfo?: PageInfo, ) { super(true, statusCode, statusText); } @@ -112,7 +112,7 @@ export class MessageResponse extends RestResponse { constructor( public statusCode: number, public statusText: string, - public pageInfo?: PageInfo + public pageInfo?: PageInfo, ) { super(true, statusCode, statusText); } @@ -124,7 +124,7 @@ export class TaskResponse extends RestResponse { constructor( public statusCode: number, public statusText: string, - public pageInfo?: PageInfo + public pageInfo?: PageInfo, ) { super(true, statusCode, statusText); } @@ -135,7 +135,7 @@ export class FilteredDiscoveryQueryResponse extends RestResponse { public filterQuery: string, public statusCode: number, public statusText: string, - public pageInfo?: PageInfo + public pageInfo?: PageInfo, ) { super(true, statusCode, statusText); } diff --git a/src/app/core/cache/server-sync-buffer.effects.spec.ts b/src/app/core/cache/server-sync-buffer.effects.spec.ts index 833c6b580fd..889b3b7454a 100644 --- a/src/app/core/cache/server-sync-buffer.effects.spec.ts +++ b/src/app/core/cache/server-sync-buffer.effects.spec.ts @@ -1,12 +1,22 @@ import { TestBed } from '@angular/core/testing'; - import { provideMockActions } from '@ngrx/effects/testing'; -import { Store, StoreModule } from '@ngrx/store'; -import { cold, hot } from 'jasmine-marbles'; -import { Observable, of as observableOf } from 'rxjs'; +import { + Store, + StoreModule, +} from '@ngrx/store'; +import { + cold, + hot, +} from 'jasmine-marbles'; +import { + Observable, + of as observableOf, +} from 'rxjs'; import { TestScheduler } from 'rxjs/testing'; +import { storeModuleConfig } from '../../app.reducer'; import { getMockRequestService } from '../../shared/mocks/request.service.mock'; +import { NoOpAction } from '../../shared/ngrx/no-op.action'; import { StoreMock } from '../../shared/testing/store.mock'; import { RequestService } from '../data/request.service'; import { RestRequestMethod } from '../data/rest-request-method'; @@ -16,11 +26,9 @@ import { ObjectCacheService } from './object-cache.service'; import { CommitSSBAction, EmptySSBAction, - ServerSyncBufferActionTypes + ServerSyncBufferActionTypes, } from './server-sync-buffer.actions'; import { ServerSyncBufferEffects } from './server-sync-buffer.effects'; -import { storeModuleConfig } from '../../app.reducer'; -import { NoOpAction } from '../../shared/ngrx/no-op.action'; describe('ServerSyncBufferEffects', () => { let ssbEffects: ServerSyncBufferEffects; @@ -32,9 +40,9 @@ describe('ServerSyncBufferEffects', () => { autoSync: { timePerMethod: {}, - defaultTime: 0 - } - } + defaultTime: 0, + }, + }, }; const selfLink = 'https://rest.api/endpoint/1698f1d3-be98-4c51-9fd8-6bfedcbd59b7'; let store; @@ -52,21 +60,21 @@ describe('ServerSyncBufferEffects', () => { provide: ObjectCacheService, useValue: { getObjectBySelfLink: (link) => { const object = Object.assign(new DSpaceObject(), { - _links: { self: { href: link } } + _links: { self: { href: link } }, }); return observableOf(object); }, getByHref: (link) => { const object = Object.assign(new DSpaceObject(), { _links: { - self: { href: link } - } + self: { href: link }, + }, }); return observableOf(object); - } - } + }, + }, }, - { provide: Store, useClass: StoreMock } + { provide: Store, useClass: StoreMock }, // other providers ], }); @@ -88,12 +96,12 @@ describe('ServerSyncBufferEffects', () => { actions = hot('a', { a: { type: ServerSyncBufferActionTypes.ADD, - payload: { href: selfLink, method: RestRequestMethod.PUT } - } + payload: { href: selfLink, method: RestRequestMethod.PUT }, + }, }); expectObservable(ssbEffects.setTimeoutForServerSync).toBe('b', { - b: new CommitSSBAction(RestRequestMethod.PUT) + b: new CommitSSBAction(RestRequestMethod.PUT), }); }); }); @@ -108,8 +116,8 @@ describe('ServerSyncBufferEffects', () => { (state as any).core['cache/syncbuffer'] = { buffer: [{ href: selfLink, - method: RestRequestMethod.PATCH - }] + method: RestRequestMethod.PATCH, + }], }; }); }); @@ -117,13 +125,13 @@ describe('ServerSyncBufferEffects', () => { actions = hot('a', { a: { type: ServerSyncBufferActionTypes.COMMIT, - payload: RestRequestMethod.PATCH - } + payload: RestRequestMethod.PATCH, + }, }); const expected = cold('(bc)', { b: new ApplyPatchObjectCacheAction(selfLink), - c: new EmptySSBAction(RestRequestMethod.PATCH) + c: new EmptySSBAction(RestRequestMethod.PATCH), }); expect(ssbEffects.commitServerSyncBuffer).toBeObservable(expected); @@ -136,7 +144,7 @@ describe('ServerSyncBufferEffects', () => { .subscribe((state) => { (state as any).core = Object({}); (state as any).core['cache/syncbuffer'] = { - buffer: [] + buffer: [], }; }); }); @@ -145,8 +153,8 @@ describe('ServerSyncBufferEffects', () => { actions = hot('a', { a: { type: ServerSyncBufferActionTypes.COMMIT, - payload: { method: RestRequestMethod.PATCH } - } + payload: { method: RestRequestMethod.PATCH }, + }, }); const expected = cold('b', { b: new NoOpAction() }); diff --git a/src/app/core/cache/server-sync-buffer.effects.ts b/src/app/core/cache/server-sync-buffer.effects.ts index 9571d4af5b4..6f346d5bb3f 100644 --- a/src/app/core/cache/server-sync-buffer.effects.ts +++ b/src/app/core/cache/server-sync-buffer.effects.ts @@ -1,27 +1,55 @@ -import { delay, exhaustMap, map, switchMap, take } from 'rxjs/operators'; import { Injectable } from '@angular/core'; -import { Actions, createEffect, ofType } from '@ngrx/effects'; +import { + Actions, + createEffect, + ofType, +} from '@ngrx/effects'; +import { + Action, + createSelector, + MemoizedSelector, + select, + Store, +} from '@ngrx/store'; +import { Operation } from 'fast-json-patch'; +import { + combineLatest as observableCombineLatest, + Observable, + of as observableOf, +} from 'rxjs'; +import { + delay, + exhaustMap, + map, + switchMap, + take, +} from 'rxjs/operators'; + +import { environment } from '../../../environments/environment'; +import { + hasValue, + isNotEmpty, + isNotUndefined, +} from '../../shared/empty.util'; +import { NoOpAction } from '../../shared/ngrx/no-op.action'; import { coreSelector } from '../core.selectors'; +import { CoreState } from '../core-state.model'; +import { PatchRequest } from '../data/request.models'; +import { RequestService } from '../data/request.service'; +import { RestRequestMethod } from '../data/rest-request-method'; +import { ApplyPatchObjectCacheAction } from './object-cache.actions'; +import { ObjectCacheEntry } from './object-cache.reducer'; +import { ObjectCacheService } from './object-cache.service'; import { AddToSSBAction, CommitSSBAction, EmptySSBAction, - ServerSyncBufferActionTypes + ServerSyncBufferActionTypes, } from './server-sync-buffer.actions'; -import { Action, createSelector, MemoizedSelector, select, Store } from '@ngrx/store'; -import { ServerSyncBufferEntry, ServerSyncBufferState } from './server-sync-buffer.reducer'; -import { combineLatest as observableCombineLatest, Observable, of as observableOf } from 'rxjs'; -import { RequestService } from '../data/request.service'; -import { PatchRequest } from '../data/request.models'; -import { ObjectCacheService } from './object-cache.service'; -import { ApplyPatchObjectCacheAction } from './object-cache.actions'; -import { hasValue, isNotEmpty, isNotUndefined } from '../../shared/empty.util'; -import { RestRequestMethod } from '../data/rest-request-method'; -import { environment } from '../../../environments/environment'; -import { ObjectCacheEntry } from './object-cache.reducer'; -import { Operation } from 'fast-json-patch'; -import { NoOpAction } from '../../shared/ngrx/no-op.action'; -import { CoreState } from '../core-state.model'; +import { + ServerSyncBufferEntry, + ServerSyncBufferState, +} from './server-sync-buffer.reducer'; @Injectable() export class ServerSyncBufferEffects { @@ -32,7 +60,7 @@ export class ServerSyncBufferEffects { * Then dispatch a CommitSSBAction * When the delay is running, no new AddToSSBActions are processed in this effect */ - setTimeoutForServerSync = createEffect(() => this.actions$ + setTimeoutForServerSync = createEffect(() => this.actions$ .pipe( ofType(ServerSyncBufferActionTypes.ADD), exhaustMap((action: AddToSSBAction) => { @@ -41,7 +69,7 @@ export class ServerSyncBufferEffects { return observableOf(new CommitSSBAction(action.payload.method)).pipe( delay(timeoutInSeconds * 1000), ); - }) + }), )); /** @@ -50,7 +78,7 @@ export class ServerSyncBufferEffects { * When the list of actions is not empty, also dispatch an EmptySSBAction * When the list is empty dispatch a NO_ACTION placeholder action */ - commitServerSyncBuffer = createEffect(() => this.actions$ + commitServerSyncBuffer = createEffect(() => this.actions$ .pipe( ofType(ServerSyncBufferActionTypes.COMMIT), switchMap((action: CommitSSBAction) => { @@ -78,14 +106,14 @@ export class ServerSyncBufferEffects { /* Add extra action to array, to make sure the ServerSyncBuffer is emptied afterwards */ if (isNotEmpty(actions) && isNotUndefined(actions[0])) { return observableCombineLatest(...actions).pipe( - switchMap((array) => [...array, new EmptySSBAction(action.payload)]) - ); + switchMap((array) => [...array, new EmptySSBAction(action.payload)]), + ); } else { return observableOf(new NoOpAction()); } - }) + }), ); - }) + }), )); /** @@ -96,7 +124,7 @@ export class ServerSyncBufferEffects { */ private applyPatch(href: string): Observable { const patchObject = this.objectCache.getByHref(href).pipe( - take(1) + take(1), ); return patchObject.pipe( @@ -108,7 +136,7 @@ export class ServerSyncBufferEffects { } } return new ApplyPatchObjectCacheAction(href); - }) + }), ); } diff --git a/src/app/core/cache/server-sync-buffer.reducer.spec.ts b/src/app/core/cache/server-sync-buffer.reducer.spec.ts index 51ba010c1e3..d986581ce2e 100644 --- a/src/app/core/cache/server-sync-buffer.reducer.spec.ts +++ b/src/app/core/cache/server-sync-buffer.reducer.spec.ts @@ -1,9 +1,13 @@ // eslint-disable-next-line import/no-namespace import * as deepFreeze from 'deep-freeze'; + +import { RestRequestMethod } from '../data/rest-request-method'; import { RemoveFromObjectCacheAction } from './object-cache.actions'; +import { + AddToSSBAction, + EmptySSBAction, +} from './server-sync-buffer.actions'; import { serverSyncBufferReducer } from './server-sync-buffer.reducer'; -import { RestRequestMethod } from '../data/rest-request-method'; -import { AddToSSBAction, EmptySSBAction } from './server-sync-buffer.actions'; class NullAction extends RemoveFromObjectCacheAction { type = null; @@ -27,8 +31,8 @@ describe('serverSyncBufferReducer', () => { { href: selfLink2, method: RestRequestMethod.GET, - } - ] + }, + ], }; const newSelfLink = 'https://localhost:8080/api/core/items/1ce6b5ae-97e1-4e5a-b4b0-f9029bad10c0'; @@ -52,12 +56,20 @@ describe('serverSyncBufferReducer', () => { const action = new AddToSSBAction(selfLink1, RestRequestMethod.POST); // testState has already been frozen above serverSyncBufferReducer(testState, action); + + // no expect required, deepFreeze will ensure an exception is thrown if the state + // is mutated, and any uncaught exception will cause the test to fail + expect().nothing(); }); it('should perform the EMPTY action without affecting the previous state', () => { const action = new EmptySSBAction(); // testState has already been frozen above serverSyncBufferReducer(testState, action); + + // no expect required, deepFreeze will ensure an exception is thrown if the state + // is mutated, and any uncaught exception will cause the test to fail + expect().nothing(); }); it('should empty the buffer if the EmptySSBAction is dispatched without a payload', () => { @@ -79,7 +91,7 @@ describe('serverSyncBufferReducer', () => { // testState has already been frozen above const newState = serverSyncBufferReducer(testState, action); expect(newState.buffer).toContain({ - href: newSelfLink, method: RestRequestMethod.PUT + href: newSelfLink, method: RestRequestMethod.PUT, }) ; }); diff --git a/src/app/core/cache/server-sync-buffer.reducer.ts b/src/app/core/cache/server-sync-buffer.reducer.ts index 3e8944aa731..f1ae8943151 100644 --- a/src/app/core/cache/server-sync-buffer.reducer.ts +++ b/src/app/core/cache/server-sync-buffer.reducer.ts @@ -1,11 +1,14 @@ -import { hasNoValue, hasValue } from '../../shared/empty.util'; +import { + hasNoValue, + hasValue, +} from '../../shared/empty.util'; +import { RestRequestMethod } from '../data/rest-request-method'; import { AddToSSBAction, EmptySSBAction, ServerSyncBufferAction, - ServerSyncBufferActionTypes + ServerSyncBufferActionTypes, } from './server-sync-buffer.actions'; -import { RestRequestMethod } from '../data/rest-request-method'; /** * An entry in the ServerSyncBufferState @@ -86,9 +89,9 @@ function addToServerSyncQueue(state: ServerSyncBufferState, action: AddToSSBActi * the new state, with a new entry added to the buffer */ function emptyServerSyncQueue(state: ServerSyncBufferState, action: EmptySSBAction): ServerSyncBufferState { - let newBuffer = []; - if (hasValue(action.payload)) { - newBuffer = state.buffer.filter((entry) => entry.method !== action.payload); - } - return Object.assign({}, state, { buffer: newBuffer }); + let newBuffer = []; + if (hasValue(action.payload)) { + newBuffer = state.buffer.filter((entry) => entry.method !== action.payload); + } + return Object.assign({}, state, { buffer: newBuffer }); } 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 new file mode 100644 index 00000000000..706c8f684b9 --- /dev/null +++ b/src/app/core/coar-notify/notify-info/notify-info.guard.spec.ts @@ -0,0 +1,40 @@ +import { of } from 'rxjs'; + +import { notifyInfoGuard } from './notify-info.guard'; + +describe('notifyInfoGuard', () => { + let guard: any; + let notifyInfoServiceSpy: any; + let router: any; + + beforeEach(() => { + notifyInfoServiceSpy = jasmine.createSpyObj('NotifyInfoService', ['isCoarConfigEnabled']); + router = jasmine.createSpyObj('Router', ['parseUrl']); + guard = notifyInfoGuard; + }); + + it('should be created', () => { + 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(null, null, notifyInfoServiceSpy, router).subscribe((result) => { + expect(result).toBe(true); + done(); + }); + }); + + it('should call parseUrl method of Router if COAR config is not enabled', (done) => { + notifyInfoServiceSpy.isCoarConfigEnabled.and.returnValue(of(false)); + router.parseUrl.and.returnValue(of('/404')); + + 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 new file mode 100644 index 00000000000..1025e7b62bc --- /dev/null +++ b/src/app/core/coar-notify/notify-info/notify-info.guard.ts @@ -0,0 +1,23 @@ +import { inject } from '@angular/core'; +import { + ActivatedRouteSnapshot, + CanActivateFn, + Router, + RouterStateSnapshot, + UrlTree, +} from '@angular/router'; +import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; + +import { NotifyInfoService } from './notify-info.service'; + +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 new file mode 100644 index 00000000000..6fa8295be06 --- /dev/null +++ b/src/app/core/coar-notify/notify-info/notify-info.service.spec.ts @@ -0,0 +1,60 @@ +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'; + +describe('NotifyInfoService', () => { + let service: NotifyInfoService; + let configurationDataService: any; + let authorizationDataService: any; + beforeEach(() => { + authorizationDataService = { + isAuthorized: jasmine.createSpy('isAuthorized').and.returnValue(of(true)), + }; + configurationDataService = { + findByPropertyName: jasmine.createSpy('findByPropertyName').and.returnValue(of({})), + }; + TestBed.configureTestingModule({ + providers: [ + NotifyInfoService, + { provide: ConfigurationDataService, useValue: configurationDataService }, + { provide: AuthorizationDataService, useValue: authorizationDataService }, + { provide: ActivatedRoute, useValue: new ActivatedRouteStub() }, + ], + }); + service = TestBed.inject(NotifyInfoService); + authorizationDataService = TestBed.inject(AuthorizationDataService); + configurationDataService = TestBed.inject(ConfigurationDataService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + it('should retrieve and map coar configuration', (done: DoneFn) => { + (configurationDataService.findByPropertyName as jasmine.Spy).and.returnValue(createSuccessfulRemoteDataObject$({ values: ['true'] })); + + service.isCoarConfigEnabled().subscribe((result) => { + expect(result).toBe(true); + done(); + }); + }); + + it('should retrieve and map LDN local inbox URLs', (done: DoneFn) => { + (configurationDataService.findByPropertyName as jasmine.Spy).and.returnValue(createSuccessfulRemoteDataObject$({ values: ['inbox1', 'inbox2'] })); + + service.getCoarLdnLocalInboxUrls().subscribe((result) => { + expect(result).toEqual(['inbox1', 'inbox2']); + done(); + }); + }); + + it('should return the inbox relation link', () => { + expect(service.getInboxRelationLink()).toBe('http://www.w3.org/ns/ldp#inbox'); + }); +}); 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 new file mode 100644 index 00000000000..455c7902ee4 --- /dev/null +++ b/src/app/core/coar-notify/notify-info/notify-info.service.ts @@ -0,0 +1,54 @@ +import { Injectable } from '@angular/core'; +import { + map, + Observable, +} from 'rxjs'; + +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 { getFirstCompletedRemoteData } from '../../shared/operators'; + +/** + * Service to check COAR availability and LDN services information for the COAR Notify functionalities + */ +@Injectable({ + providedIn: 'root', +}) +export class NotifyInfoService { + + /** + * The relation link for the inbox + */ + private _inboxRelationLink = 'http://www.w3.org/ns/ldp#inbox'; + + constructor( + private configService: ConfigurationDataService, + protected authorizationService: AuthorizationDataService, + ) {} + + isCoarConfigEnabled(): Observable { + return this.authorizationService.isAuthorized(FeatureID.CoarNotifyEnabled); + } + + /** + * Get the url of the local inbox from the REST configuration + * @returns the url of the local inbox + */ + getCoarLdnLocalInboxUrls(): Observable { + return this.configService.findByPropertyName('ldn.notify.inbox').pipe( + getFirstCompletedRemoteData(), + map((responseRD: RemoteData) => responseRD.hasSucceeded ? responseRD.payload.values : []), + ); + } + + /** + * Method to get the relation link for the inbox + * @returns the relation link for the inbox + */ + getInboxRelationLink(): string { + return this._inboxRelationLink; + } +} 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 28b4029ea28..8023e58489b 100644 --- a/src/app/core/config/bulk-access-config-data.service.ts +++ b/src/app/core/config/bulk-access-config-data.service.ts @@ -1,17 +1,15 @@ import { Injectable } from '@angular/core'; + import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { ObjectCacheService } from '../cache/object-cache.service'; -import { HALEndpointService } from '../shared/hal-endpoint.service'; import { RequestService } from '../data/request.service'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; import { ConfigDataService } from './config-data.service'; -import { dataService } from '../data/base/data-service.decorator'; -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/config-data.service.spec.ts b/src/app/core/config/config-data.service.spec.ts index 38340d1ad54..a9979f1bb5b 100644 --- a/src/app/core/config/config-data.service.spec.ts +++ b/src/app/core/config/config-data.service.spec.ts @@ -1,15 +1,16 @@ import { getTestScheduler } from 'jasmine-marbles'; import { TestScheduler } from 'rxjs/testing'; + +import { getMockRemoteDataBuildService } from '../../shared/mocks/remote-data-build.service.mock'; import { getMockRequestService } from '../../shared/mocks/request.service.mock'; -import { ConfigDataService } from './config-data.service'; -import { RequestService } from '../data/request.service'; -import { GetRequest } from '../data/request.models'; -import { HALEndpointService } from '../shared/hal-endpoint.service'; import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; -import { getMockRemoteDataBuildService } from '../../shared/mocks/remote-data-build.service.mock'; -import { FindListOptions } from '../data/find-list-options.model'; import { ObjectCacheService } from '../cache/object-cache.service'; +import { FindListOptions } from '../data/find-list-options.model'; +import { GetRequest } from '../data/request.models'; +import { RequestService } from '../data/request.service'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { ConfigDataService } from './config-data.service'; const LINK_NAME = 'test'; const BROWSE = 'search/findByCollection'; diff --git a/src/app/core/config/config-data.service.ts b/src/app/core/config/config-data.service.ts index 58b023e62c8..51ca6559219 100644 --- a/src/app/core/config/config-data.service.ts +++ b/src/app/core/config/config-data.service.ts @@ -1,11 +1,11 @@ import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; -import { ConfigObject } from './models/config.model'; -import { RemoteData } from '../data/remote-data'; import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; -import { getFirstCompletedRemoteData } from '../shared/operators'; import { IdentifiableDataService } from '../data/base/identifiable-data.service'; +import { RemoteData } from '../data/remote-data'; +import { getFirstCompletedRemoteData } from '../shared/operators'; +import { ConfigObject } from './models/config.model'; /** * Abstract data service to retrieve configuration objects from the REST server. 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 d84e14b95db..514c682b4e5 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 @@ -1,8 +1,13 @@ -import { autoserialize, autoserializeAs, inheritSerialization } from 'cerialize'; +import { + autoserialize, + autoserializeAs, + inheritSerialization, +} from 'cerialize'; + import { typedObject } from '../../cache/builders/build-decorators'; -import { excludeFromEquals } from '../../utilities/equals.decorators'; -import { ResourceType } from '../../shared/resource-type'; import { HALLink } from '../../shared/hal-link.model'; +import { ResourceType } from '../../shared/resource-type'; +import { excludeFromEquals } from '../../utilities/equals.decorators'; import { ConfigObject } from './config.model'; import { AccessesConditionOption } from './config-accesses-conditions-options.model'; import { BULK_ACCESS_CONDITION_OPTIONS } from './config-type'; diff --git a/src/app/core/config/models/config-accesses-conditions-options.model.ts b/src/app/core/config/models/config-accesses-conditions-options.model.ts index 244b5019086..64199be0eb7 100644 --- a/src/app/core/config/models/config-accesses-conditions-options.model.ts +++ b/src/app/core/config/models/config-accesses-conditions-options.model.ts @@ -3,43 +3,43 @@ */ export class AccessesConditionOption { - /** + /** * The name for this Access Condition */ - name: string; + name: string; - /** + /** * The groupName for this Access Condition */ - groupName: string; + groupName: string; - /** + /** * A boolean representing if this Access Condition has a start date */ - hasStartDate: boolean; + hasStartDate: boolean; - /** + /** * A boolean representing if this Access Condition has an end date */ - hasEndDate: boolean; + hasEndDate: boolean; - /** + /** * Maximum value of the start date */ - endDateLimit?: string; + endDateLimit?: string; - /** + /** * Maximum value of the end date */ - startDateLimit?: string; + startDateLimit?: string; - /** + /** * Maximum value of the start date */ - maxStartDate?: string; + maxStartDate?: string; - /** + /** * Maximum value of the end date */ - maxEndDate?: string; + maxEndDate?: string; } 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 7db96acf2bd..2e9a9291835 100644 --- a/src/app/core/config/models/config-submission-access.model.ts +++ b/src/app/core/config/models/config-submission-access.model.ts @@ -1,9 +1,14 @@ -import { autoserialize, deserialize, inheritSerialization } from 'cerialize'; +import { + autoserialize, + deserialize, + inheritSerialization, +} from 'cerialize'; + import { typedObject } from '../../cache/builders/build-decorators'; +import { HALLink } from '../../shared/hal-link.model'; import { ConfigObject } from './config.model'; import { AccessesConditionOption } from './config-accesses-conditions-options.model'; import { SUBMISSION_ACCESSES_TYPE } from './config-type'; -import { HALLink } from '../../shared/hal-link.model'; /** * Class for the configuration describing the item accesses condition diff --git a/src/app/core/config/models/config-submission-accesses.model.ts b/src/app/core/config/models/config-submission-accesses.model.ts index 3f8004928db..b3c097cc8ac 100644 --- a/src/app/core/config/models/config-submission-accesses.model.ts +++ b/src/app/core/config/models/config-submission-accesses.model.ts @@ -1,7 +1,8 @@ import { inheritSerialization } from 'cerialize'; + import { typedObject } from '../../cache/builders/build-decorators'; -import { SUBMISSION_ACCESSES_TYPE } from './config-type'; import { SubmissionAccessModel } from './config-submission-access.model'; +import { SUBMISSION_ACCESSES_TYPE } from './config-type'; @typedObject @inheritSerialization(SubmissionAccessModel) 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 b07917e0328..eda4f54340b 100644 --- a/src/app/core/config/models/config-submission-definition.model.ts +++ b/src/app/core/config/models/config-submission-definition.model.ts @@ -1,9 +1,14 @@ -import { autoserialize, deserialize, inheritSerialization } from 'cerialize'; +import { + autoserialize, + deserialize, + inheritSerialization, +} from 'cerialize'; + import { typedObject } from '../../cache/builders/build-decorators'; import { PaginatedList } from '../../data/paginated-list.model'; import { HALLink } from '../../shared/hal-link.model'; -import { SubmissionSectionModel } from './config-submission-section.model'; import { ConfigObject } from './config.model'; +import { SubmissionSectionModel } from './config-submission-section.model'; import { SUBMISSION_DEFINITION_TYPE } from './config-type'; /** diff --git a/src/app/core/config/models/config-submission-definitions.model.ts b/src/app/core/config/models/config-submission-definitions.model.ts index 08f1ef17bb0..790334da9bd 100644 --- a/src/app/core/config/models/config-submission-definitions.model.ts +++ b/src/app/core/config/models/config-submission-definitions.model.ts @@ -1,4 +1,5 @@ import { inheritSerialization } from 'cerialize'; + import { typedObject } from '../../cache/builders/build-decorators'; import { SubmissionDefinitionModel } from './config-submission-definition.model'; import { SUBMISSION_DEFINITIONS_TYPE } from './config-type'; 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 90f94882bd6..c524a839160 100644 --- a/src/app/core/config/models/config-submission-form.model.ts +++ b/src/app/core/config/models/config-submission-form.model.ts @@ -1,7 +1,11 @@ -import { autoserialize, inheritSerialization } from 'cerialize'; +import { + autoserialize, + inheritSerialization, +} from 'cerialize'; + +import { FormFieldModel } from '../../../shared/form/builder/models/form-field.model'; import { typedObject } from '../../cache/builders/build-decorators'; import { ConfigObject } from './config.model'; -import { FormFieldModel } from '../../../shared/form/builder/models/form-field.model'; import { SUBMISSION_FORM_TYPE } from './config-type'; /** diff --git a/src/app/core/config/models/config-submission-forms.model.ts b/src/app/core/config/models/config-submission-forms.model.ts index 506905d88cb..4cf71d85d9a 100644 --- a/src/app/core/config/models/config-submission-forms.model.ts +++ b/src/app/core/config/models/config-submission-forms.model.ts @@ -1,4 +1,5 @@ import { inheritSerialization } from 'cerialize'; + import { typedObject } from '../../cache/builders/build-decorators'; import { SubmissionFormModel } from './config-submission-form.model'; import { SUBMISSION_FORMS_TYPE } from './config-type'; 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 bdc884dfa46..feb4fc9cb07 100644 --- a/src/app/core/config/models/config-submission-section.model.ts +++ b/src/app/core/config/models/config-submission-section.model.ts @@ -1,4 +1,9 @@ -import { autoserialize, deserialize, inheritSerialization } from 'cerialize'; +import { + autoserialize, + deserialize, + inheritSerialization, +} from 'cerialize'; + import { SectionsType } from '../../../submission/sections/sections-type'; import { typedObject } from '../../cache/builders/build-decorators'; import { HALLink } from '../../shared/hal-link.model'; diff --git a/src/app/core/config/models/config-submission-sections.model.ts b/src/app/core/config/models/config-submission-sections.model.ts index 423ea99b1e1..86894b6e44b 100644 --- a/src/app/core/config/models/config-submission-sections.model.ts +++ b/src/app/core/config/models/config-submission-sections.model.ts @@ -1,4 +1,5 @@ import { inheritSerialization } from 'cerialize'; + import { typedObject } from '../../cache/builders/build-decorators'; import { SubmissionSectionModel } from './config-submission-section.model'; import { SUBMISSION_SECTIONS_TYPE } from './config-type'; 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 f6897da2e38..cabc84d0f55 100644 --- a/src/app/core/config/models/config-submission-upload.model.ts +++ b/src/app/core/config/models/config-submission-upload.model.ts @@ -1,12 +1,23 @@ -import { autoserialize, inheritSerialization, deserialize } from 'cerialize'; -import { typedObject, link } from '../../cache/builders/build-decorators'; +import { + autoserialize, + deserialize, + inheritSerialization, +} from 'cerialize'; +import { Observable } from 'rxjs'; + +import { + link, + typedObject, +} from '../../cache/builders/build-decorators'; +import { RemoteData } from '../../data/remote-data'; +import { HALLink } from '../../shared/hal-link.model'; import { ConfigObject } from './config.model'; import { AccessConditionOption } from './config-access-condition-option.model'; import { SubmissionFormsModel } from './config-submission-forms.model'; -import { SUBMISSION_UPLOAD_TYPE, SUBMISSION_FORMS_TYPE } from './config-type'; -import { HALLink } from '../../shared/hal-link.model'; -import { RemoteData } from '../../data/remote-data'; -import { Observable } from 'rxjs'; +import { + SUBMISSION_FORMS_TYPE, + SUBMISSION_UPLOAD_TYPE, +} from './config-type'; @typedObject @inheritSerialization(ConfigObject) diff --git a/src/app/core/config/models/config-submission-uploads.model.ts b/src/app/core/config/models/config-submission-uploads.model.ts index 8fb7dc66b9c..235cdd31a1e 100644 --- a/src/app/core/config/models/config-submission-uploads.model.ts +++ b/src/app/core/config/models/config-submission-uploads.model.ts @@ -1,7 +1,8 @@ import { inheritSerialization } from 'cerialize'; + import { typedObject } from '../../cache/builders/build-decorators'; -import { SUBMISSION_UPLOADS_TYPE } from './config-type'; import { SubmissionUploadModel } from './config-submission-upload.model'; +import { SUBMISSION_UPLOADS_TYPE } from './config-type'; @typedObject @inheritSerialization(SubmissionUploadModel) diff --git a/src/app/core/config/models/config.model.ts b/src/app/core/config/models/config.model.ts index 170aa334edb..74d090b89db 100644 --- a/src/app/core/config/models/config.model.ts +++ b/src/app/core/config/models/config.model.ts @@ -1,8 +1,12 @@ -import { autoserialize, deserialize } from 'cerialize'; +import { + autoserialize, + deserialize, +} from 'cerialize'; + +import { CacheableObject } from '../../cache/cacheable-object.model'; import { HALLink } from '../../shared/hal-link.model'; import { ResourceType } from '../../shared/resource-type'; import { excludeFromEquals } from '../../utilities/equals.decorators'; -import { CacheableObject } from '../../cache/cacheable-object.model'; export abstract class ConfigObject implements CacheableObject { 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 d2da0fce422..bc7a7e66d3c 100644 --- a/src/app/core/config/submission-accesses-config-data.service.ts +++ b/src/app/core/config/submission-accesses-config-data.service.ts @@ -1,22 +1,20 @@ import { Injectable } from '@angular/core'; -import { ConfigDataService } from './config-data.service'; +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 { RemoteData } from '../data/remote-data'; import { RequestService } from '../data/request.service'; import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { ObjectCacheService } from '../cache/object-cache.service'; -import { SUBMISSION_ACCESSES_TYPE } from './models/config-type'; -import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { ConfigDataService } from './config-data.service'; import { ConfigObject } from './models/config.model'; import { SubmissionAccessesModel } from './models/config-submission-accesses.model'; -import { RemoteData } from '../data/remote-data'; -import { Observable } from 'rxjs'; -import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; -import { dataService } from '../data/base/data-service.decorator'; /** * 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 f4c0690685b..fe1234defb3 100644 --- a/src/app/core/config/submission-forms-config-data.service.ts +++ b/src/app/core/config/submission-forms-config-data.service.ts @@ -1,22 +1,20 @@ import { Injectable } from '@angular/core'; -import { ConfigDataService } from './config-data.service'; -import { RequestService } from '../data/request.service'; -import { HALEndpointService } from '../shared/hal-endpoint.service'; +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 { 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 { SUBMISSION_FORMS_TYPE } from './models/config-type'; import { SubmissionFormsModel } from './models/config-submission-forms.model'; -import { RemoteData } from '../data/remote-data'; -import { Observable } from 'rxjs'; -import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; -import { dataService } from '../data/base/data-service.decorator'; /** * 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 8f838352a9e..10d749080e4 100644 --- a/src/app/core/config/submission-uploads-config-data.service.ts +++ b/src/app/core/config/submission-uploads-config-data.service.ts @@ -1,22 +1,20 @@ import { Injectable } from '@angular/core'; -import { ConfigDataService } from './config-data.service'; +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 { RemoteData } from '../data/remote-data'; import { RequestService } from '../data/request.service'; import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { ObjectCacheService } from '../cache/object-cache.service'; -import { SUBMISSION_UPLOADS_TYPE } from './models/config-type'; -import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { ConfigDataService } from './config-data.service'; import { ConfigObject } from './models/config.model'; import { SubmissionUploadsModel } from './models/config-submission-uploads.model'; -import { RemoteData } from '../data/remote-data'; -import { Observable } from 'rxjs'; -import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; -import { dataService } from '../data/base/data-service.decorator'; /** * 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-state.model.ts b/src/app/core/core-state.model.ts index b8211fdb555..2128901754b 100644 --- a/src/app/core/core-state.model.ts +++ b/src/app/core/core-state.model.ts @@ -1,16 +1,14 @@ -import { - BitstreamFormatRegistryState -} from '../admin/admin-registries/bitstream-formats/bitstream-format.reducers'; +import { BitstreamFormatRegistryState } from '../admin/admin-registries/bitstream-formats/bitstream-format.reducers'; +import { AuthState } from './auth/auth.reducer'; import { ObjectCacheState } from './cache/object-cache.reducer'; import { ServerSyncBufferState } from './cache/server-sync-buffer.reducer'; import { ObjectUpdatesState } from './data/object-updates/object-updates.reducer'; +import { RequestState } from './data/request-state.model'; import { HistoryState } from './history/history.reducer'; import { MetaIndexState } from './index/index.reducer'; -import { AuthState } from './auth/auth.reducer'; import { JsonPatchOperationsState } from './json-patch/json-patch-operations.reducer'; import { MetaTagState } from './metadata/meta-tag.reducer'; import { RouteState } from './services/route.reducer'; -import { RequestState } from './data/request-state.model'; /** * The core sub-state in the NgRx store diff --git a/src/app/core/core.effects.ts b/src/app/core/core.effects.ts index b569df290d5..5af2fe580a1 100644 --- a/src/app/core/core.effects.ts +++ b/src/app/core/core.effects.ts @@ -1,12 +1,13 @@ -import { ObjectCacheEffects } from './cache/object-cache.effects'; -import { UUIDIndexEffects } from './index/index.effects'; -import { RequestEffects } from './data/request.effects'; +import { MenuEffects } from '../shared/menu/menu.effects'; import { AuthEffects } from './auth/auth.effects'; -import { JsonPatchOperationsEffects } from './json-patch/json-patch-operations.effects'; +import { ObjectCacheEffects } from './cache/object-cache.effects'; import { ServerSyncBufferEffects } from './cache/server-sync-buffer.effects'; import { ObjectUpdatesEffects } from './data/object-updates/object-updates.effects'; -import { RouteEffects } from './services/route.effects'; +import { RequestEffects } from './data/request.effects'; +import { UUIDIndexEffects } from './index/index.effects'; +import { JsonPatchOperationsEffects } from './json-patch/json-patch-operations.effects'; import { RouterEffects } from './router/router.effects'; +import { RouteEffects } from './services/route.effects'; export const coreEffects = [ RequestEffects, @@ -18,4 +19,5 @@ export const coreEffects = [ ObjectUpdatesEffects, RouteEffects, RouterEffects, + MenuEffects, ]; diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts deleted file mode 100644 index dbca773375a..00000000000 --- a/src/app/core/core.module.ts +++ /dev/null @@ -1,416 +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 { MyDSpaceGuard } from '../my-dspace-page/my-dspace.guard'; - -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 { 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 { AuthenticatedGuard } from './auth/authenticated.guard'; -import { AuthStatus } from './auth/models/auth-status.model'; -import { BrowseService } from './browse/browse.service'; -import { RemoteDataBuildService } from './cache/builders/remote-data-build.service'; -import { ObjectCacheService } from './cache/object-cache.service'; -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 { BitstreamFormatDataService } from './data/bitstream-format-data.service'; -import { CollectionDataService } from './data/collection-data.service'; -import { CommunityDataService } from './data/community-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 { FilteredDiscoveryPageResponseParsingService } from './data/filtered-discovery-page-response-parsing.service'; -import { ItemDataService } from './data/item-data.service'; -import { LookupRelationService } from './data/lookup-relation.service'; -import { MyDSpaceResponseParsingService } from './data/mydspace-response-parsing.service'; -import { ObjectUpdatesService } from './data/object-updates/object-updates.service'; -import { RelationshipTypeDataService } from './data/relationship-type-data.service'; -import { RelationshipDataService } from './data/relationship-data.service'; -import { ResourcePolicyDataService } from './resource-policy/resource-policy-data.service'; -import { SearchResponseParsingService } from './data/search-response-parsing.service'; -import { SiteDataService } from './data/site-data.service'; -import { DspaceRestService } from './dspace-rest/dspace-rest.service'; -import { EPersonDataService } from './eperson/eperson-data.service'; -import { EPerson } from './eperson/models/eperson.model'; -import { Group } from './eperson/models/group.model'; -import { JsonPatchOperationsBuilder } from './json-patch/builder/json-patch-operations-builder'; -import { MetadataField } from './metadata/metadata-field.model'; -import { MetadataSchema } from './metadata/metadata-schema.model'; -import { MetadataService } from './metadata/metadata.service'; -import { RegistryService } from './registry/registry.service'; -import { RoleService } from './roles/role.service'; -import { FeedbackDataService } from './feedback/feedback-data.service'; - -import { ServerResponseService } from './services/server-response.service'; -import { NativeWindowFactory, NativeWindowService } from './services/window.service'; -import { BitstreamFormat } from './shared/bitstream-format.model'; -import { Bitstream } from './shared/bitstream.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 { DSpaceObject } from './shared/dspace-object.model'; -import { ExternalSourceEntry } from './shared/external-source-entry.model'; -import { ExternalSource } from './shared/external-source.model'; -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 { Relationship } from './shared/item-relationships/relationship.model'; -import { Item } from './shared/item.model'; -import { License } from './shared/license.model'; -import { ResourcePolicy } from './resource-policy/models/resource-policy.model'; -import { SearchConfigurationService } from './shared/search/search-configuration.service'; -import { SearchFilterService } from './shared/search/search-filter.service'; -import { SearchService } from './shared/search/search.service'; -import { Site } from './shared/site.model'; -import { UUIDService } from './shared/uuid.service'; -import { WorkflowItem } from './submission/models/workflowitem.model'; -import { WorkspaceItem } from './submission/models/workspaceitem.model'; -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 { WorkflowItemDataService } from './submission/workflowitem-data.service'; -import { WorkspaceitemDataService } from './submission/workspaceitem-data.service'; -import { ClaimedTaskDataService } from './tasks/claimed-task-data.service'; -import { ClaimedTask } from './tasks/models/claimed-task-object.model'; -import { PoolTask } from './tasks/models/pool-task-object.model'; -import { TaskObject } from './tasks/models/task-object.model'; -import { PoolTaskDataService } from './tasks/pool-task-data.service'; -import { TaskResponseParsingService } from './tasks/task-response-parsing.service'; -import { ArrayMoveChangeAnalyzer } from './data/array-move-change-analyzer.service'; -import { BitstreamDataService } from './data/bitstream-data.service'; -import { environment } from '../../environments/environment'; -import { storeModuleConfig } from '../app.reducer'; -import { VersionDataService } from './data/version-data.service'; -import { VersionHistoryDataService } from './data/version-history-data.service'; -import { Version } from './shared/version.model'; -import { VersionHistory } from './shared/version-history.model'; -import { Script } from '../process-page/scripts/script.model'; -import { Process } from '../process-page/processes/process.model'; -import { ProcessDataService } from './data/processes/process-data.service'; -import { ScriptDataService } from './data/processes/script-data.service'; -import { WorkflowActionDataService } from './data/workflow-action-data.service'; -import { WorkflowAction } from './tasks/models/workflow-action-object.model'; -import { ItemTemplateDataService } from './data/item-template-data.service'; -import { TemplateItem } from './shared/template-item.model'; -import { Feature } from './shared/feature.model'; -import { Authorization } from './shared/authorization.model'; -import { FeatureDataService } from './data/feature-authorization/feature-data.service'; -import { AuthorizationDataService } from './data/feature-authorization/authorization-data.service'; -import { - SiteAdministratorGuard -} from './data/feature-authorization/feature-authorization-guard/site-administrator.guard'; -import { Registration } from './shared/registration.model'; -import { MetadataSchemaDataService } from './data/metadata-schema-data.service'; -import { MetadataFieldDataService } from './data/metadata-field-data.service'; -import { TokenResponseParsingService } from './auth/token-response-parsing.service'; -import { SubmissionCcLicenseDataService } from './submission/submission-cc-license-data.service'; -import { SubmissionCcLicence } from './submission/models/submission-cc-license.model'; -import { SubmissionCcLicenceUrl } from './submission/models/submission-cc-license-url.model'; -import { SubmissionCcLicenseUrlDataService } from './submission/submission-cc-license-url-data.service'; -import { VocabularyEntry } from './submission/vocabularies/models/vocabulary-entry.model'; -import { Vocabulary } from './submission/vocabularies/models/vocabulary.model'; -import { VocabularyEntryDetail } from './submission/vocabularies/models/vocabulary-entry-detail.model'; -import { VocabularyService } from './submission/vocabularies/vocabulary.service'; -import { ConfigurationDataService } from './data/configuration-data.service'; -import { ConfigurationProperty } from './shared/configuration-property.model'; -import { ReloadGuard } from './reload/reload.guard'; -import { EndUserAgreementCurrentUserGuard } from './end-user-agreement/end-user-agreement-current-user.guard'; -import { EndUserAgreementCookieGuard } from './end-user-agreement/end-user-agreement-cookie.guard'; -import { EndUserAgreementService } from './end-user-agreement/end-user-agreement.service'; -import { SiteRegisterGuard } from './data/feature-authorization/feature-authorization-guard/site-register.guard'; -import { ShortLivedToken } from './auth/models/short-lived-token.model'; -import { UsageReport } from './statistics/models/usage-report.model'; -import { RootDataService } from './data/root-data.service'; -import { Root } from './data/root.model'; -import { SearchConfig } from './shared/search/search-filters/search-config.model'; -import { SequenceService } from './shared/sequence.service'; -import { CoreState } from './core-state.model'; -import { GroupDataService } from './eperson/group-data.service'; -import { SubmissionAccessesModel } from './config/models/config-submission-accesses.model'; -import { RatingAdvancedWorkflowInfo } from './tasks/models/rating-advanced-workflow-info.model'; -import { AdvancedWorkflowInfo } from './tasks/models/advanced-workflow-info.model'; -import { SelectReviewerAdvancedWorkflowInfo } from './tasks/models/select-reviewer-advanced-workflow-info.model'; -import { AccessStatusObject } from '../shared/object-collection/shared/badges/access-status-badge/access-status.model'; -import { AccessStatusDataService } from './data/access-status-data.service'; -import { LinkHeadService } from './services/link-head.service'; -import { ResearcherProfileDataService } from './profile/researcher-profile-data.service'; -import { ProfileClaimService } from '../profile-page/profile-claim/profile-claim.service'; -import { ResearcherProfile } from './profile/model/researcher-profile.model'; -import { OrcidQueueDataService } from './orcid/orcid-queue-data.service'; -import { OrcidHistoryDataService } from './orcid/orcid-history-data.service'; -import { OrcidQueue } from './orcid/model/orcid-queue.model'; -import { OrcidHistory } from './orcid/model/orcid-history.model'; -import { OrcidAuthService } from './orcid/orcid-auth.service'; -import { VocabularyDataService } from './submission/vocabularies/vocabulary.data.service'; -import { VocabularyEntryDetailsDataService } from './submission/vocabularies/vocabulary-entry-details.data.service'; -import { IdentifierData } from '../shared/object-list/identifier-data/identifier-data.model'; -import { Subscription } from '../shared/subscriptions/models/subscription.model'; -import { SupervisionOrderDataService } from './supervision-order/supervision-order-data.service'; -import { ItemRequest } from './shared/item-request.model'; -import { HierarchicalBrowseDefinition } from './shared/hierarchical-browse-definition.model'; -import { FlatBrowseDefinition } from './shared/flat-browse-definition.model'; -import { ValueListBrowseDefinition } from './shared/value-list-browse-definition.model'; -import { NonHierarchicalBrowseDefinition } from './shared/non-hierarchical-browse-definition'; -import { BulkAccessConditionOptions } from './config/models/bulk-access-condition-options.model'; - -/** - * 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, - 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 -]; - -/** - * 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, - Root, - SearchConfig, - SubmissionAccessesModel, - AccessStatusObject, - ResearcherProfile, - OrcidQueue, - OrcidHistory, - AccessStatusObject, - IdentifierData, - Subscription, - ItemRequest, - BulkAccessConditionOptions - ]; - -@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/core.reducers.ts b/src/app/core/core.reducers.ts index c0165c53848..fda1e05df05 100644 --- a/src/app/core/core.reducers.ts +++ b/src/app/core/core.reducers.ts @@ -1,19 +1,17 @@ -import { ActionReducerMap, } from '@ngrx/store'; +import { ActionReducerMap } from '@ngrx/store'; -import { objectCacheReducer } from './cache/object-cache.reducer'; -import { indexReducer } from './index/index.reducer'; -import { requestReducer } from './data/request.reducer'; +import { bitstreamFormatReducer } from '../admin/admin-registries/bitstream-formats/bitstream-format.reducers'; import { authReducer } from './auth/auth.reducer'; -import { jsonPatchOperationsReducer } from './json-patch/json-patch-operations.reducer'; +import { objectCacheReducer } from './cache/object-cache.reducer'; import { serverSyncBufferReducer } from './cache/server-sync-buffer.reducer'; +import { CoreState } from './core-state.model'; import { objectUpdatesReducer } from './data/object-updates/object-updates.reducer'; -import { routeReducer } from './services/route.reducer'; -import { - bitstreamFormatReducer -} from '../admin/admin-registries/bitstream-formats/bitstream-format.reducers'; +import { requestReducer } from './data/request.reducer'; import { historyReducer } from './history/history.reducer'; +import { indexReducer } from './index/index.reducer'; +import { jsonPatchOperationsReducer } from './json-patch/json-patch-operations.reducer'; import { metaTagReducer } from './metadata/meta-tag.reducer'; -import { CoreState } from './core-state.model'; +import { routeReducer } from './services/route.reducer'; export const coreReducers: ActionReducerMap = { 'bitstreamFormats': bitstreamFormatReducer, @@ -26,5 +24,5 @@ export const coreReducers: ActionReducerMap = { 'auth': authReducer, 'json/patch': jsonPatchOperationsReducer, 'metaTag': metaTagReducer, - 'route': routeReducer + 'route': routeReducer, }; diff --git a/src/app/core/core.selectors.ts b/src/app/core/core.selectors.ts index 77c7974de2c..899afb9be98 100644 --- a/src/app/core/core.selectors.ts +++ b/src/app/core/core.selectors.ts @@ -1,4 +1,5 @@ import { createFeatureSelector } from '@ngrx/store'; + import { CoreState } from './core-state.model'; /** diff --git a/src/app/core/data-services-map.ts b/src/app/core/data-services-map.ts new file mode 100644 index 00000000000..c9ebbc5ffc3 --- /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 18b8cb5d65d..ed587c26d2f 100644 --- a/src/app/core/data/access-status-data.service.spec.ts +++ b/src/app/core/data/access-status-data.service.spec.ts @@ -1,17 +1,21 @@ -import { RequestService } from './request.service'; +import { + fakeAsync, + tick, +} from '@angular/core/testing'; +import { Observable } from 'rxjs'; + +import { hasNoValue } from '../../shared/empty.util'; import { getMockRequestService } from '../../shared/mocks/request.service.mock'; +import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub'; import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub'; -import { fakeAsync, tick } from '@angular/core/testing'; -import { GetRequest } from './request.models'; -import { ObjectCacheService } from '../cache/object-cache.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; -import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; -import { Observable } from 'rxjs'; -import { RemoteData } from './remote-data'; -import { hasNoValue } from '../../shared/empty.util'; -import { AccessStatusDataService } from './access-status-data.service'; +import { ObjectCacheService } from '../cache/object-cache.service'; import { Item } from '../shared/item.model'; +import { AccessStatusDataService } from './access-status-data.service'; +import { RemoteData } from './remote-data'; +import { GetRequest } from './request.models'; +import { RequestService } from './request.service'; const url = 'fake-url'; @@ -29,12 +33,12 @@ describe('AccessStatusDataService', () => { name: 'test-item', _links: { accessStatus: { - href: `https://rest.api/items/${itemId}/accessStatus` + href: `https://rest.api/items/${itemId}/accessStatus`, }, self: { - href: `https://rest.api/items/${itemId}` - } - } + href: `https://rest.api/items/${itemId}`, + }, + }, }); describe('when the requests are successful', () => { @@ -59,20 +63,20 @@ 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', { buildFromRequestUUID: buildResponse$, - buildSingle: buildResponse$ + buildSingle: buildResponse$, }); objectCache = jasmine.createSpyObj('objectCache', { - remove: jasmine.createSpy('remove') + remove: jasmine.createSpy('remove'), }); halService = new HALEndpointServiceStub(url); notificationsService = new NotificationsServiceStub(); diff --git a/src/app/core/data/access-status-data.service.ts b/src/app/core/data/access-status-data.service.ts index e8b77245e87..6d8acb1c8b4 100644 --- a/src/app/core/data/access-status-data.service.ts +++ b/src/app/core/data/access-status-data.service.ts @@ -1,21 +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 { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { ObjectCacheService } from '../cache/object-cache.service'; import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { RequestService } from './request.service'; -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 { Observable } from 'rxjs'; -import { RemoteData } from './remote-data'; 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.spec.ts b/src/app/core/data/array-move-change-analyzer.service.spec.ts index 025791d6dc4..cf11decde4c 100644 --- a/src/app/core/data/array-move-change-analyzer.service.spec.ts +++ b/src/app/core/data/array-move-change-analyzer.service.spec.ts @@ -1,7 +1,8 @@ -import { ArrayMoveChangeAnalyzer } from './array-move-change-analyzer.service'; import { moveItemInArray } from '@angular/cdk/drag-drop'; import { Operation } from 'fast-json-patch'; +import { ArrayMoveChangeAnalyzer } from './array-move-change-analyzer.service'; + /** * Helper class for creating move tests * Define a "from" and "to" index to move objects within the array before comparing @@ -28,7 +29,7 @@ describe('ArrayMoveChangeAnalyzer', () => { '4d7d0798-a8fa-45b8-b4fc-deb2819606c8', 'e56eb99e-2f7c-4bee-9b3f-d3dcc83386b1', '0f608168-cdfc-46b0-92ce-889f7d3ac684', - '546f9f5c-15dc-4eec-86fe-648007ac9e1c' + '546f9f5c-15dc-4eec-86fe-648007ac9e1c', ]; }); @@ -72,7 +73,7 @@ describe('ArrayMoveChangeAnalyzer', () => { '4d7d0798-a8fa-45b8-b4fc-deb2819606c8', undefined, undefined, - '546f9f5c-15dc-4eec-86fe-648007ac9e1c' + '546f9f5c-15dc-4eec-86fe-648007ac9e1c', ]; }); 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 36744e9f96e..bd5fc8dedb3 100644 --- a/src/app/core/data/array-move-change-analyzer.service.ts +++ b/src/app/core/data/array-move-change-analyzer.service.ts @@ -1,12 +1,13 @@ -import { MoveOperation } from 'fast-json-patch'; -import { Injectable } from '@angular/core'; import { moveItemInArray } from '@angular/cdk/drag-drop'; +import { Injectable } from '@angular/core'; +import { MoveOperation } from 'fast-json-patch'; + 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-response-parsing.service.spec.ts b/src/app/core/data/base-response-parsing.service.spec.ts index da9fa7a6432..f6707d3582a 100644 --- a/src/app/core/data/base-response-parsing.service.spec.ts +++ b/src/app/core/data/base-response-parsing.service.spec.ts @@ -1,9 +1,9 @@ /* eslint-disable max-classes-per-file */ -import { BaseResponseParsingService } from './base-response-parsing.service'; +import { CacheableObject } from '../cache/cacheable-object.model'; import { ObjectCacheService } from '../cache/object-cache.service'; -import { GetRequest} from './request.models'; import { DSpaceObject } from '../shared/dspace-object.model'; -import { CacheableObject } from '../cache/cacheable-object.model'; +import { BaseResponseParsingService } from './base-response-parsing.service'; +import { GetRequest } from './request.models'; import { RestRequest } from './rest-request.model'; class TestService extends BaseResponseParsingService { @@ -35,7 +35,7 @@ describe('BaseResponseParsingService', () => { beforeEach(() => { obj = undefined; objectCache = jasmine.createSpyObj('objectCache', { - add: {} + add: {}, }); service = new TestService(objectCache); }); @@ -58,8 +58,8 @@ describe('BaseResponseParsingService', () => { beforeEach(() => { obj = Object.assign(new DSpaceObject(), { _links: { - self: { href: 'obj-selflink' } - } + self: { href: 'obj-selflink' }, + }, }); }); @@ -79,8 +79,8 @@ describe('BaseResponseParsingService', () => { data = { type: 'NotARealType', _links: { - self: { href: 'data-selflink' } - } + self: { href: 'data-selflink' }, + }, }; }); diff --git a/src/app/core/data/base-response-parsing.service.ts b/src/app/core/data/base-response-parsing.service.ts index 18e6623683f..63b4961b313 100644 --- a/src/app/core/data/base-response-parsing.service.ts +++ b/src/app/core/data/base-response-parsing.service.ts @@ -1,14 +1,21 @@ /* eslint-disable max-classes-per-file */ -import { hasNoValue, hasValue, isNotEmpty } from '../../shared/empty.util'; +import { environment } from '../../../environments/environment'; +import { + hasNoValue, + hasValue, + isNotEmpty, +} from '../../shared/empty.util'; +import { getClassForType } from '../cache/builders/build-decorators'; +import { CacheableObject } from '../cache/cacheable-object.model'; +import { ObjectCacheService } from '../cache/object-cache.service'; import { DSpaceSerializer } from '../dspace-rest/dspace.serializer'; import { Serializer } from '../serializer'; -import { PageInfo } from '../shared/page-info.model'; -import { ObjectCacheService } from '../cache/object-cache.service'; import { GenericConstructor } from '../shared/generic-constructor'; -import { PaginatedList, buildPaginatedList } from './paginated-list.model'; -import { getClassForType } from '../cache/builders/build-decorators'; -import { environment } from '../../../environments/environment'; -import { CacheableObject } from '../cache/cacheable-object.model'; +import { PageInfo } from '../shared/page-info.model'; +import { + buildPaginatedList, + PaginatedList, +} from './paginated-list.model'; import { RestRequest } from './rest-request.model'; @@ -64,7 +71,7 @@ export abstract class BaseResponseParsingService { } else if (isRestDataObject(data._embedded[property])) { object[property] = this.retrieveObjectOrUrl(parsedObj); } else if (Array.isArray(parsedObj)) { - object[property] = parsedObj.map((obj) => this.retrieveObjectOrUrl(obj)); + object[property] = parsedObj.map((obj) => this.retrieveObjectOrUrl(obj)); } } }); @@ -96,14 +103,14 @@ export abstract class BaseResponseParsingService { list = this.flattenSingleKeyObject(list); } const page: ObjectDomain[] = this.processArray(list, request); - return buildPaginatedList(pageInfo, page,); + return buildPaginatedList(pageInfo, page); } protected processArray(data: any, request: RestRequest): ObjectDomain[] { let array: ObjectDomain[] = []; data.forEach((datum) => { - array = [...array, this.process(datum, request)]; - } + array = [...array, this.process(datum, request)]; + }, ); return array; } @@ -139,7 +146,7 @@ export abstract class BaseResponseParsingService { let dataJSON: string; if (hasValue(data._embedded)) { dataJSON = JSON.stringify(Object.assign({}, data, { - _embedded: '...' + _embedded: '...', })); } else { dataJSON = JSON.stringify(data); 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 098f075c101..3f44ad5e5ac 100644 --- a/src/app/core/data/base/base-data.service.spec.ts +++ b/src/app/core/data/base/base-data.service.spec.ts @@ -5,22 +5,37 @@ * * http://www.dspace.org/license/ */ -import { RequestService } from '../request.service'; -import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service'; -import { HALEndpointService } from '../../shared/hal-endpoint.service'; -import { ObjectCacheService } from '../../cache/object-cache.service'; -import { FindListOptions } from '../find-list-options.model'; -import { Observable, of as observableOf, combineLatest as observableCombineLatest } from 'rxjs'; +import { + fakeAsync, + tick, +} from '@angular/core/testing'; +import { + combineLatest as observableCombineLatest, + Observable, + of as observableOf, +} from 'rxjs'; +import { TestScheduler } from 'rxjs/testing'; + +import { getMockRemoteDataBuildService } from '../../../shared/mocks/remote-data-build.service.mock'; import { getMockRequestService } from '../../../shared/mocks/request.service.mock'; +import { + createFailedRemoteDataObject$, + createSuccessfulRemoteDataObject$, +} from '../../../shared/remote-data.utils'; import { HALEndpointServiceStub } from '../../../shared/testing/hal-endpoint-service.stub'; -import { getMockRemoteDataBuildService } from '../../../shared/mocks/remote-data-build.service.mock'; +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 { TestScheduler } from 'rxjs/testing'; +import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service'; +import { ObjectCacheEntry } from '../../cache/object-cache.reducer'; +import { ObjectCacheService } from '../../cache/object-cache.service'; +import { HALEndpointService } from '../../shared/hal-endpoint.service'; +import { HALLink } from '../../shared/hal-link.model'; +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 { fakeAsync, tick } from '@angular/core/testing'; import { BaseDataService } from './base-data.service'; -import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; const endpoint = 'https://rest.api/core'; @@ -46,38 +61,22 @@ describe('BaseDataService', () => { let requestService; let halService; let rdbService; - let objectCache; + let objectCache: ObjectCacheServiceStub; let selfLink; let linksToFollow; let testScheduler; - let remoteDataMocks; + let remoteDataMocks: { [responseType: string]: RemoteData }; + let remoteDataPageMocks: { [responseType: string]: RemoteData }; function initTestService(): TestService { requestService = getMockRequestService(); halService = new HALEndpointServiceStub('url') as any; rdbService = getMockRemoteDataBuildService(); - objectCache = { - - addPatch: () => { - /* empty */ - }, - getObjectBySelfLink: () => { - /* empty */ - }, - getByHref: () => { - /* empty */ - }, - addDependency: () => { - /* empty */ - }, - removeDependents: () => { - /* empty */ - }, - } as any; + objectCache = new ObjectCacheServiceStub(); selfLink = 'https://rest.api/endpoint/1698f1d3-be98-4c51-9fd8-6bfedcbd59b7'; linksToFollow = [ followLink('a'), - followLink('b') + followLink('b'), ]; testScheduler = new TestScheduler((actual, expected) => { @@ -88,23 +87,53 @@ describe('BaseDataService', () => { const timeStamp = new Date().getTime(); const msToLive = 15 * 60 * 1000; - const payload = { foo: 'bar' }; + const payload = { + foo: 'bar', + followLink1: {}, + followLink2: {}, + _links: { + self: Object.assign(new HALLink(), { + href: 'self-test-link', + }), + followLink1: Object.assign(new HALLink(), { + href: 'follow-link-1', + }), + followLink2: [ + Object.assign(new HALLink(), { + href: 'follow-link-2-1', + }), + Object.assign(new HALLink(), { + href: 'follow-link-2-2', + }), + ], + }, + }; 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), }; + 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), + }; return new TestService( requestService, rdbService, - objectCache, + objectCache as ObjectCacheService, halService, ); } @@ -303,19 +332,21 @@ describe('BaseDataService', () => { 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', { - a: remoteDataMocks.SuccessStale, - b: remoteDataMocks.RequestPending, - c: remoteDataMocks.ResponsePending, - d: remoteDataMocks.Success, - e: remoteDataMocks.SuccessStale, + spyOn(rdbService, 'buildSingle').and.returnValue(cold('a-b-c-d-e-f-g', { + a: remoteDataMocks.ResponsePendingStale, + b: remoteDataMocks.SuccessStale, + c: remoteDataMocks.ErrorStale, + d: remoteDataMocks.RequestPending, + e: remoteDataMocks.ResponsePending, + f: remoteDataMocks.Success, + g: remoteDataMocks.SuccessStale, })); - const expected = '--b-c-d-e'; + const expected = '------d-e-f-g'; const values = { - b: remoteDataMocks.RequestPending, - c: remoteDataMocks.ResponsePending, - d: remoteDataMocks.Success, - e: remoteDataMocks.SuccessStale, + d: remoteDataMocks.RequestPending, + e: remoteDataMocks.ResponsePending, + f: remoteDataMocks.Success, + g: remoteDataMocks.SuccessStale, }; expectObservable(service.findByHref(selfLink, true, true, ...linksToFollow)).toBe(expected, values); @@ -354,19 +385,21 @@ describe('BaseDataService', () => { 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', { - a: remoteDataMocks.SuccessStale, - b: remoteDataMocks.RequestPending, - c: remoteDataMocks.ResponsePending, - d: remoteDataMocks.Success, - e: remoteDataMocks.SuccessStale, + spyOn(rdbService, 'buildSingle').and.returnValue(cold('a-b-c-d-e-f-g', { + a: remoteDataMocks.ResponsePendingStale, + b: remoteDataMocks.SuccessStale, + c: remoteDataMocks.ErrorStale, + d: remoteDataMocks.RequestPending, + e: remoteDataMocks.ResponsePending, + f: remoteDataMocks.Success, + g: remoteDataMocks.SuccessStale, })); - const expected = '--b-c-d-e'; + const expected = '------d-e-f-g'; const values = { - b: remoteDataMocks.RequestPending, - c: remoteDataMocks.ResponsePending, - d: remoteDataMocks.Success, - e: remoteDataMocks.SuccessStale, + d: remoteDataMocks.RequestPending, + e: remoteDataMocks.ResponsePending, + f: remoteDataMocks.Success, + g: remoteDataMocks.SuccessStale, }; expectObservable(service.findByHref(selfLink, false, true, ...linksToFollow)).toBe(expected, values); @@ -375,6 +408,27 @@ 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', { + a: remoteDataMocks.Success, + b: remoteDataMocks.RequestPending, + c: remoteDataMocks.ResponsePending, + d: remoteDataMocks.Success, + })); + const expected = '--b-c-d'; + const values = { + b: remoteDataMocks.RequestPending, + c: remoteDataMocks.ResponsePending, + d: remoteDataMocks.Success, + }; + + expectObservable(service.findByHref(selfLink, false, false, ...linksToFollow)).toBe(expected, values); + flush(); + expect(objectCache.addDependency).toHaveBeenCalledTimes(3); + }); + }); }); describe(`findListByHref`, () => { @@ -387,8 +441,8 @@ describe('BaseDataService', () => { it(`should call buildHrefFromFindOptions with href and linksToFollow`, () => { testScheduler.run(({ cold }) => { spyOn(service, 'buildHrefFromFindOptions').and.returnValue(selfLink); - spyOn(rdbService, 'buildList').and.returnValue(cold('a', { a: remoteDataMocks.Success })); - spyOn(service as any, 'reRequestStaleRemoteData').and.returnValue(() => cold('a', { a: remoteDataMocks.Success })); + spyOn(rdbService, 'buildList').and.returnValue(cold('a', { a: remoteDataPageMocks.Success })); + spyOn(service as any, 'reRequestStaleRemoteData').and.returnValue(() => cold('a', { a: remoteDataPageMocks.Success })); service.findListByHref(selfLink, findListOptions, true, true, ...linksToFollow); expect(service.buildHrefFromFindOptions).toHaveBeenCalledWith(selfLink, findListOptions, [], ...linksToFollow); @@ -398,8 +452,8 @@ describe('BaseDataService', () => { it(`should call createAndSendGetRequest with the result from buildHrefFromFindOptions and useCachedVersionIfAvailable`, () => { testScheduler.run(({ cold, expectObservable }) => { spyOn(service, 'buildHrefFromFindOptions').and.returnValue('bingo!'); - spyOn(rdbService, 'buildList').and.returnValue(cold('a', { a: remoteDataMocks.Success })); - spyOn(service as any, 'reRequestStaleRemoteData').and.returnValue(() => cold('a', { a: remoteDataMocks.Success })); + spyOn(rdbService, 'buildList').and.returnValue(cold('a', { a: remoteDataPageMocks.Success })); + spyOn(service as any, 'reRequestStaleRemoteData').and.returnValue(() => cold('a', { a: remoteDataPageMocks.Success })); service.findListByHref(selfLink, findListOptions, true, true, ...linksToFollow); expect((service as any).createAndSendGetRequest).toHaveBeenCalledWith(jasmine.anything(), true); @@ -414,8 +468,8 @@ describe('BaseDataService', () => { it(`should call rdbService.buildList with the result from buildHrefFromFindOptions and linksToFollow`, () => { testScheduler.run(({ cold, expectObservable }) => { spyOn(service, 'buildHrefFromFindOptions').and.returnValue('bingo!'); - spyOn(rdbService, 'buildList').and.returnValue(cold('a', { a: remoteDataMocks.Success })); - spyOn(service as any, 'reRequestStaleRemoteData').and.returnValue(() => cold('a', { a: remoteDataMocks.Success })); + spyOn(rdbService, 'buildList').and.returnValue(cold('a', { a: remoteDataPageMocks.Success })); + spyOn(service as any, 'reRequestStaleRemoteData').and.returnValue(() => cold('a', { a: remoteDataPageMocks.Success })); service.findListByHref(selfLink, findListOptions, true, true, ...linksToFollow); expect(rdbService.buildList).toHaveBeenCalledWith(jasmine.anything() as any, ...linksToFollow); @@ -426,12 +480,12 @@ describe('BaseDataService', () => { it(`should call reRequestStaleRemoteData with reRequestOnStale and the exact same findListByHref call as a callback`, () => { testScheduler.run(({ cold, expectObservable }) => { spyOn(service, 'buildHrefFromFindOptions').and.returnValue('bingo!'); - spyOn(rdbService, 'buildList').and.returnValue(cold('a', { a: remoteDataMocks.SuccessStale })); - spyOn(service as any, 'reRequestStaleRemoteData').and.returnValue(() => cold('a', { a: remoteDataMocks.SuccessStale })); + spyOn(rdbService, 'buildList').and.returnValue(cold('a', { a: remoteDataPageMocks.SuccessStale })); + spyOn(service as any, 'reRequestStaleRemoteData').and.returnValue(() => cold('a', { a: remoteDataPageMocks.SuccessStale })); service.findListByHref(selfLink, findListOptions, true, true, ...linksToFollow); expect((service as any).reRequestStaleRemoteData.calls.argsFor(0)[0]).toBeTrue(); - spyOn(service, 'findListByHref').and.returnValue(cold('a', { a: remoteDataMocks.SuccessStale })); + spyOn(service, 'findListByHref').and.returnValue(cold('a', { a: remoteDataPageMocks.SuccessStale })); // prove that the spy we just added hasn't been called yet expect(service.findListByHref).not.toHaveBeenCalled(); // call the callback passed to reRequestStaleRemoteData @@ -446,7 +500,7 @@ describe('BaseDataService', () => { it(`should return a the output from reRequestStaleRemoteData`, () => { testScheduler.run(({ cold, expectObservable }) => { spyOn(service, 'buildHrefFromFindOptions').and.returnValue(selfLink); - spyOn(rdbService, 'buildList').and.returnValue(cold('a', { a: remoteDataMocks.Success })); + spyOn(rdbService, 'buildList').and.returnValue(cold('a', { a: remoteDataPageMocks.Success })); spyOn(service as any, 'reRequestStaleRemoteData').and.returnValue(() => cold('a', { a: 'bingo!' })); const expected = 'a'; const values = { @@ -466,19 +520,19 @@ describe('BaseDataService', () => { it(`should emit a cached completed RemoteData immediately, and keep emitting if it gets rerequested`, () => { testScheduler.run(({ cold, expectObservable }) => { spyOn(rdbService, 'buildList').and.returnValue(cold('a-b-c-d-e', { - a: remoteDataMocks.Success, - b: remoteDataMocks.RequestPending, - c: remoteDataMocks.ResponsePending, - d: remoteDataMocks.Success, - e: remoteDataMocks.SuccessStale, + a: remoteDataPageMocks.Success, + b: remoteDataPageMocks.RequestPending, + c: remoteDataPageMocks.ResponsePending, + d: remoteDataPageMocks.Success, + e: remoteDataPageMocks.SuccessStale, })); const expected = 'a-b-c-d-e'; const values = { - a: remoteDataMocks.Success, - b: remoteDataMocks.RequestPending, - c: remoteDataMocks.ResponsePending, - d: remoteDataMocks.Success, - e: remoteDataMocks.SuccessStale, + a: remoteDataPageMocks.Success, + b: remoteDataPageMocks.RequestPending, + c: remoteDataPageMocks.ResponsePending, + d: remoteDataPageMocks.Success, + e: remoteDataPageMocks.SuccessStale, }; expectObservable(service.findListByHref(selfLink, findListOptions, true, true, ...linksToFollow)).toBe(expected, values); @@ -487,19 +541,21 @@ describe('BaseDataService', () => { 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', { - a: remoteDataMocks.SuccessStale, - b: remoteDataMocks.RequestPending, - c: remoteDataMocks.ResponsePending, - d: remoteDataMocks.Success, - e: remoteDataMocks.SuccessStale, + spyOn(rdbService, 'buildList').and.returnValue(cold('a-b-c-d-e-f-g', { + a: remoteDataPageMocks.ResponsePendingStale, + b: remoteDataPageMocks.SuccessStale, + c: remoteDataPageMocks.ErrorStale, + d: remoteDataPageMocks.RequestPending, + e: remoteDataPageMocks.ResponsePending, + f: remoteDataPageMocks.Success, + g: remoteDataPageMocks.SuccessStale, })); - const expected = '--b-c-d-e'; + const expected = '------d-e-f-g'; const values = { - b: remoteDataMocks.RequestPending, - c: remoteDataMocks.ResponsePending, - d: remoteDataMocks.Success, - e: remoteDataMocks.SuccessStale, + d: remoteDataPageMocks.RequestPending, + e: remoteDataPageMocks.ResponsePending, + f: remoteDataPageMocks.Success, + g: remoteDataPageMocks.SuccessStale, }; expectObservable(service.findListByHref(selfLink, findListOptions, true, true, ...linksToFollow)).toBe(expected, values); @@ -518,18 +574,18 @@ describe('BaseDataService', () => { it(`should not emit a cached completed 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', { - a: remoteDataMocks.Success, - b: remoteDataMocks.RequestPending, - c: remoteDataMocks.ResponsePending, - d: remoteDataMocks.Success, - e: remoteDataMocks.SuccessStale, + a: remoteDataPageMocks.Success, + b: remoteDataPageMocks.RequestPending, + c: remoteDataPageMocks.ResponsePending, + d: remoteDataPageMocks.Success, + e: remoteDataPageMocks.SuccessStale, })); const expected = '--b-c-d-e'; const values = { - b: remoteDataMocks.RequestPending, - c: remoteDataMocks.ResponsePending, - d: remoteDataMocks.Success, - e: remoteDataMocks.SuccessStale, + b: remoteDataPageMocks.RequestPending, + c: remoteDataPageMocks.ResponsePending, + d: remoteDataPageMocks.Success, + e: remoteDataPageMocks.SuccessStale, }; expectObservable(service.findListByHref(selfLink, findListOptions, false, true, ...linksToFollow)).toBe(expected, values); @@ -538,25 +594,49 @@ describe('BaseDataService', () => { 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', { - a: remoteDataMocks.SuccessStale, - b: remoteDataMocks.RequestPending, - c: remoteDataMocks.ResponsePending, - d: remoteDataMocks.Success, - e: remoteDataMocks.SuccessStale, + spyOn(rdbService, 'buildList').and.returnValue(cold('a-b-c-d-e-f-g', { + a: remoteDataPageMocks.ResponsePendingStale, + b: remoteDataPageMocks.SuccessStale, + c: remoteDataPageMocks.ErrorStale, + d: remoteDataPageMocks.RequestPending, + e: remoteDataPageMocks.ResponsePending, + f: remoteDataPageMocks.Success, + g: remoteDataPageMocks.SuccessStale, })); - const expected = '--b-c-d-e'; + const expected = '------d-e-f-g'; const values = { - b: remoteDataMocks.RequestPending, - c: remoteDataMocks.ResponsePending, - d: remoteDataMocks.Success, - e: remoteDataMocks.SuccessStale, + d: remoteDataPageMocks.RequestPending, + e: remoteDataPageMocks.ResponsePending, + f: remoteDataPageMocks.Success, + g: remoteDataPageMocks.SuccessStale, }; + expectObservable(service.findListByHref(selfLink, findListOptions, false, true, ...linksToFollow)).toBe(expected, values); }); }); + it('should link all the followLinks of the cached objects by calling addDependency', () => { + spyOn(objectCache, 'addDependency').and.callThrough(); + testScheduler.run(({ cold, expectObservable, flush }) => { + spyOn(rdbService, 'buildList').and.returnValue(cold('a-b-c-d', { + a: remoteDataPageMocks.SuccessStale, + b: remoteDataPageMocks.RequestPending, + c: remoteDataPageMocks.ResponsePending, + d: remoteDataPageMocks.Success, + })); + const expected = '--b-c-d'; + const values = { + b: remoteDataPageMocks.RequestPending, + c: remoteDataPageMocks.ResponsePending, + d: remoteDataPageMocks.Success, + }; + + expectObservable(service.findListByHref(selfLink, findListOptions, false, false, ...linksToFollow)).toBe(expected, values); + flush(); + expect(objectCache.addDependency).toHaveBeenCalledTimes(3); + }); + }); }); }); @@ -566,8 +646,8 @@ describe('BaseDataService', () => { beforeEach(() => { getByHrefSpy = spyOn(objectCache, 'getByHref').and.returnValue(observableOf({ requestUUIDs: ['request1', 'request2', 'request3'], - dependentRequestUUIDs: ['request4', 'request5'] - })); + dependentRequestUUIDs: ['request4', 'request5'], + } as ObjectCacheEntry)); }); @@ -714,7 +794,7 @@ describe('BaseDataService', () => { (service as any).addDependency( createSuccessfulRemoteDataObject$({ _links: { self: { href: 'object-href' } } }), - observableOf('dependsOnHref') + observableOf('dependsOnHref'), ); expect(addDependencySpy).toHaveBeenCalled(); }); @@ -729,7 +809,7 @@ describe('BaseDataService', () => { (service as any).addDependency( createFailedRemoteDataObject$('something went wrong'), - observableOf('dependsOnHref') + observableOf('dependsOnHref'), ); expect(addDependencySpy).toHaveBeenCalled(); }); diff --git a/src/app/core/data/base/base-data.service.ts b/src/app/core/data/base/base-data.service.ts index edd6d9e2a42..d09ee21ee03 100644 --- a/src/app/core/data/base/base-data.service.ts +++ b/src/app/core/data/base/base-data.service.ts @@ -6,34 +6,53 @@ * http://www.dspace.org/license/ */ -import { AsyncSubject, from as observableFrom, Observable, of as observableOf } from 'rxjs'; -import { map, mergeMap, skipWhile, switchMap, take, tap, toArray } from 'rxjs/operators'; -import { hasValue, isNotEmpty, isNotEmptyOperator } from '../../../shared/empty.util'; +import { + AsyncSubject, + from as observableFrom, + Observable, + of as observableOf, +} from 'rxjs'; +import { + map, + mergeMap, + skipWhile, + switchMap, + take, + tap, + toArray, +} from 'rxjs/operators'; + +import { + hasValue, + isNotEmpty, + isNotEmptyOperator, +} from '../../../shared/empty.util'; import { FollowLinkConfig } from '../../../shared/utils/follow-link-config.model'; 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 { HALEndpointService } from '../../shared/hal-endpoint.service'; +import { HALLink } from '../../shared/hal-link.model'; +import { getFirstCompletedRemoteData } from '../../shared/operators'; import { URLCombiner } from '../../url-combiner/url-combiner'; +import { FindListOptions } from '../find-list-options.model'; +import { PaginatedList } from '../paginated-list.model'; import { RemoteData } from '../remote-data'; import { GetRequest } from '../request.models'; import { RequestService } from '../request.service'; -import { CacheableObject } from '../../cache/cacheable-object.model'; -import { FindListOptions } from '../find-list-options.model'; -import { PaginatedList } from '../paginated-list.model'; -import { ObjectCacheEntry } from '../../cache/object-cache.reducer'; -import { ObjectCacheService } from '../../cache/object-cache.service'; import { HALDataService } from './hal-data-service.interface'; -import { getFirstCompletedRemoteData } from '../../shared/operators'; export const EMBED_SEPARATOR = '%2F'; /** * Common functionality for data services. * Specific functionality that not all services would need - * is implemented in "DataService feature" classes (e.g. {@link CreateData} + * is implemented in "UpdateDataServiceImpl feature" classes (e.g. {@link CreateData} * - * All DataService (or DataService feature) classes must + * All UpdateDataServiceImpl (or UpdateDataServiceImpl feature) classes must * - extend this class (or {@link IdentifiableDataService}) - * - implement any DataService features it requires in order to forward calls to it + * - implement any UpdateDataServiceImpl features it requires in order to forward calls to it * * ``` * export class SomeDataService extends BaseDataService implements CreateData, SearchData { @@ -235,7 +254,7 @@ export class BaseDataService implements HALDataServic if (hasValue(remoteData) && remoteData.isStale) { requestFn(); } - }) + }), ); } else { return source; @@ -268,15 +287,34 @@ export class BaseDataService implements HALDataServic this.createAndSendGetRequest(requestHref$, useCachedVersionIfAvailable); - return this.rdbService.buildSingle(requestHref$, ...linksToFollow).pipe( + const response$: Observable> = this.rdbService.buildSingle(requestHref$, ...linksToFollow).pipe( // This skip ensures that if a stale object is present in the cache when you do a // 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) => useCachedVersionIfAvailable ? rd.isStale : rd.hasCompleted), + skipWhile((rd: RemoteData) => rd.isStale || (!useCachedVersionIfAvailable && rd.hasCompleted)), this.reRequestStaleRemoteData(reRequestOnStale, () => this.findByHref(href$, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow)), ); + return response$.pipe( + // 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') { + // followLink can be either an individual HALLink or a HALLink[] + const followLinksList: HALLink[] = [].concat(remoteDataObject.payload._links[followLinkName]); + for (const individualFollowLink of followLinksList) { + if (hasValue(individualFollowLink?.href)) { + this.addDependency(response$, individualFollowLink.href); + } + } + } + } + } + }), + ); } /** @@ -302,15 +340,38 @@ export class BaseDataService implements HALDataServic this.createAndSendGetRequest(requestHref$, useCachedVersionIfAvailable); - return this.rdbService.buildList(requestHref$, ...linksToFollow).pipe( + const response$: Observable>> = this.rdbService.buildList(requestHref$, ...linksToFollow).pipe( // This skip ensures that if a stale object is present in the cache when you do a // 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>) => useCachedVersionIfAvailable ? rd.isStale : rd.hasCompleted), + skipWhile((rd: RemoteData>) => rd.isStale || (!useCachedVersionIfAvailable && rd.hasCompleted)), this.reRequestStaleRemoteData(reRequestOnStale, () => this.findListByHref(href$, options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow)), ); + return response$.pipe( + // Ensure all followLinks from the cached object are automatically invalidated when invalidating the cached object + tap((remoteDataObject: RemoteData>) => { + 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') { + // followLink can be either an individual HALLink or a HALLink[] + const followLinksList: HALLink[] = [].concat(object._links[followLinkName]); + for (const individualFollowLink of followLinksList) { + if (hasValue(individualFollowLink?.href)) { + this.addDependency(response$, individualFollowLink.href); + } + } + } + } + } + } + } + }), + ); } /** @@ -329,7 +390,7 @@ export class BaseDataService implements HALDataServic href$.pipe( isNotEmptyOperator(), - take(1) + take(1), ).subscribe((href: string) => { const requestId = this.requestService.generateRequestId(); const request = new GetRequest(requestId, href); @@ -373,19 +434,19 @@ export class BaseDataService implements HALDataServic return this.hasCachedResponse(href$).pipe( switchMap((hasCachedResponse) => { if (hasCachedResponse) { - return this.rdbService.buildSingle(href$).pipe( - getFirstCompletedRemoteData(), - map((rd => rd.hasFailed)) - ); + return this.rdbService.buildSingle(href$).pipe( + getFirstCompletedRemoteData(), + map((rd => rd.hasFailed)), + ); } return observableOf(false); - }) + }), ); } /** * Return the links to traverse from the root of the api to the - * endpoint this DataService represents + * endpoint this UpdateDataServiceImpl represents * * e.g. if the api root links to 'foo', and the endpoint at 'foo' * links to 'bar' the linkPath for the BarDataService would be @@ -420,7 +481,7 @@ export class BaseDataService implements HALDataServic } }), ), - dependsOnHref$ + dependsOnHref$, ); } @@ -437,7 +498,7 @@ export class BaseDataService implements HALDataServic switchMap((oce: ObjectCacheEntry) => { return observableFrom([ ...oce.requestUUIDs, - ...oce.dependentRequestUUIDs + ...oce.dependentRequestUUIDs, ]).pipe( mergeMap((requestUUID: string) => this.requestService.setStaleByUUID(requestUUID)), toArray(), diff --git a/src/app/core/data/base/create-data.spec.ts b/src/app/core/data/base/create-data.spec.ts index 0b2e0f39308..1248c5ffe49 100644 --- a/src/app/core/data/base/create-data.spec.ts +++ b/src/app/core/data/base/create-data.spec.ts @@ -5,23 +5,33 @@ * * http://www.dspace.org/license/ */ -import { RequestService } from '../request.service'; +import { + Observable, + of as observableOf, +} from 'rxjs'; + +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 { + createFailedRemoteDataObject, + createSuccessfulRemoteDataObject, +} from '../../../shared/remote-data.utils'; +import { HALEndpointServiceStub } from '../../../shared/testing/hal-endpoint-service.stub'; 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 { DSpaceObject } from '../../shared/dspace-object.model'; import { HALEndpointService } from '../../shared/hal-endpoint.service'; import { FindListOptions } from '../find-list-options.model'; -import { Observable, of as observableOf } from 'rxjs'; -import { CreateData, CreateDataImpl } from './create-data'; -import { NotificationsService } from '../../../shared/notifications/notifications.service'; -import { getMockRequestService } from '../../../shared/mocks/request.service.mock'; -import { HALEndpointServiceStub } from '../../../shared/testing/hal-endpoint-service.stub'; -import { getMockRemoteDataBuildService } from '../../../shared/mocks/remote-data-build.service.mock'; import { RemoteData } from '../remote-data'; +import { RequestService } from '../request.service'; import { RequestEntryState } from '../request-entry-state.model'; -import { createFailedRemoteDataObject, createSuccessfulRemoteDataObject } from '../../../shared/remote-data.utils'; -import { RequestParam } from '../../cache/models/request-param.model'; import { RestRequestMethod } from '../rest-request-method'; -import { DSpaceObject } from '../../shared/dspace-object.model'; +import { + CreateData, + CreateDataImpl, +} from './create-data'; /** * Tests whether calls to `CreateData` methods are correctly patched through in a concrete data service that implements it @@ -149,7 +159,7 @@ describe('CreateDataImpl', () => { describe('create', () => { it('should POST the object to the root endpoint with the given parameters and return the remote data', (done) => { const params = [ - new RequestParam('abc', 123), new RequestParam('def', 456) + new RequestParam('abc', 123), new RequestParam('def', 456), ]; buildFromRequestUUIDSpy.and.returnValue(observableOf(remoteDataMocks.Success)); diff --git a/src/app/core/data/base/create-data.ts b/src/app/core/data/base/create-data.ts index 3ffcd9adf20..13216f8796d 100644 --- a/src/app/core/data/base/create-data.ts +++ b/src/app/core/data/base/create-data.ts @@ -5,22 +5,31 @@ * * http://www.dspace.org/license/ */ +import { Observable } from 'rxjs'; +import { + distinctUntilChanged, + map, + take, + takeWhile, +} from 'rxjs/operators'; + +import { + hasValue, + isNotEmptyOperator, +} from '../../../shared/empty.util'; +import { NotificationOptions } from '../../../shared/notifications/models/notification-options.model'; +import { NotificationsService } from '../../../shared/notifications/notifications.service'; +import { getClassForType } from '../../cache/builders/build-decorators'; +import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service'; import { CacheableObject } from '../../cache/cacheable-object.model'; -import { BaseDataService } from './base-data.service'; import { RequestParam } from '../../cache/models/request-param.model'; -import { Observable } from 'rxjs'; -import { RemoteData } from '../remote-data'; -import { hasValue, isNotEmptyOperator } from '../../../shared/empty.util'; -import { distinctUntilChanged, map, take, takeWhile } from 'rxjs/operators'; +import { ObjectCacheService } from '../../cache/object-cache.service'; import { DSpaceSerializer } from '../../dspace-rest/dspace.serializer'; -import { getClassForType } from '../../cache/builders/build-decorators'; +import { HALEndpointService } from '../../shared/hal-endpoint.service'; +import { RemoteData } from '../remote-data'; import { CreateRequest } from '../request.models'; -import { NotificationOptions } from '../../../shared/notifications/models/notification-options.model'; import { RequestService } from '../request.service'; -import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service'; -import { HALEndpointService } from '../../shared/hal-endpoint.service'; -import { NotificationsService } from '../../../shared/notifications/notifications.service'; -import { ObjectCacheService } from '../../cache/object-cache.service'; +import { BaseDataService } from './base-data.service'; /** * Interface for a data service that can create objects. @@ -37,7 +46,7 @@ export interface CreateData { } /** - * A DataService feature to create objects. + * A UpdateDataServiceImpl feature to create objects. * * Concrete data services can use this feature by implementing {@link CreateData} * and delegating its method to an inner instance of this class. @@ -95,7 +104,7 @@ export class CreateDataImpl extends BaseDataService) => rd.isLoading, true) + takeWhile((rd: RemoteData) => rd.isLoading, true), ).subscribe((rd: RemoteData) => { if (rd.hasFailed) { this.notificationsService.error('Server Error:', rd.errorMessage, new NotificationOptions(-1)); 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 e09c531a569..00000000000 --- a/src/app/core/data/base/data-service.decorator.spec.ts +++ /dev/null @@ -1,56 +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 { ResourceType } from '../../shared/resource-type'; -import { BaseDataService } from './base-data.service'; -import { HALDataService } from './hal-data-service.interface'; -import { dataService, getDataServiceFor } from './data-service.decorator'; -import { v4 as uuidv4 } from 'uuid'; - -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 fbde9bd94f8..00000000000 --- a/src/app/core/data/base/data-service.decorator.ts +++ /dev/null @@ -1,51 +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 { CacheableObject } from '../../cache/cacheable-object.model'; -import { ResourceType } from '../../shared/resource-type'; -import { GenericConstructor } from '../../shared/generic-constructor'; -import { hasNoValue, hasValue } from '../../../shared/empty.util'; -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 a076473b0fe..f3fa72a285a 100644 --- a/src/app/core/data/base/delete-data.spec.ts +++ b/src/app/core/data/base/delete-data.spec.ts @@ -5,24 +5,35 @@ * * http://www.dspace.org/license/ */ -import { constructIdEndpointDefault } from './identifiable-data.service'; -import { RequestService } from '../request.service'; +import { + Observable, + of as observableOf, +} from 'rxjs'; +import { TestScheduler } from 'rxjs/testing'; + +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 { + createFailedRemoteDataObject, + createSuccessfulRemoteDataObject, + createSuccessfulRemoteDataObject$, +} from '../../../shared/remote-data.utils'; +import { HALEndpointServiceStub } from '../../../shared/testing/hal-endpoint-service.stub'; +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 { Observable, of as observableOf } from 'rxjs'; -import { getMockRequestService } from '../../../shared/mocks/request.service.mock'; -import { HALEndpointServiceStub } from '../../../shared/testing/hal-endpoint-service.stub'; -import { getMockRemoteDataBuildService } from '../../../shared/mocks/remote-data-build.service.mock'; -import { followLink } from '../../../shared/utils/follow-link-config.model'; -import { TestScheduler } from 'rxjs/testing'; import { RemoteData } from '../remote-data'; +import { RequestService } from '../request.service'; import { RequestEntryState } from '../request-entry-state.model'; -import { DeleteData, DeleteDataImpl } from './delete-data'; -import { NotificationsService } from '../../../shared/notifications/notifications.service'; -import { createFailedRemoteDataObject, createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; import { RestRequestMethod } from '../rest-request-method'; +import { + DeleteData, + DeleteDataImpl, +} from './delete-data'; +import { constructIdEndpointDefault } from './identifiable-data.service'; /** * Tests whether calls to `DeleteData` methods are correctly patched through in a concrete data service that implements it @@ -34,7 +45,7 @@ export function testDeleteDataImplementation(serviceFactory: () => DeleteData { @@ -105,13 +116,13 @@ describe('DeleteDataImpl', () => { }, getByHref: () => { /* empty */ - } + }, } as any; notificationsService = {} as NotificationsService; selfLink = 'https://rest.api/endpoint/1698f1d3-be98-4c51-9fd8-6bfedcbd59b7'; linksToFollow = [ followLink('a'), - followLink('b') + followLink('b'), ]; testScheduler = new TestScheduler((actual, expected) => { @@ -198,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 807d9d838e9..c47a6af8474 100644 --- a/src/app/core/data/base/delete-data.ts +++ b/src/app/core/data/base/delete-data.ts @@ -5,19 +5,26 @@ * * http://www.dspace.org/license/ */ -import { CacheableObject } from '../../cache/cacheable-object.model'; import { Observable } from 'rxjs'; -import { RemoteData } from '../remote-data'; -import { NoContent } from '../../shared/NoContent.model'; import { switchMap } from 'rxjs/operators'; -import { DeleteRequest } from '../request.models'; -import { hasNoValue, hasValue } from '../../../shared/empty.util'; -import { RequestService } from '../request.service'; -import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service'; -import { HALEndpointService } from '../../shared/hal-endpoint.service'; + +import { + hasNoValue, + hasValue, +} from '../../../shared/empty.util'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; +import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service'; +import { CacheableObject } from '../../cache/cacheable-object.model'; import { ObjectCacheService } from '../../cache/object-cache.service'; -import { ConstructIdEndpoint, IdentifiableDataService } from './identifiable-data.service'; +import { HALEndpointService } from '../../shared/hal-endpoint.service'; +import { NoContent } from '../../shared/NoContent.model'; +import { RemoteData } from '../remote-data'; +import { DeleteRequest } from '../request.models'; +import { RequestService } from '../request.service'; +import { + ConstructIdEndpoint, + IdentifiableDataService, +} from './identifiable-data.service'; export interface DeleteData { /** @@ -68,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/find-all-data.spec.ts b/src/app/core/data/base/find-all-data.spec.ts index 6a73e032d07..f2c48feb76c 100644 --- a/src/app/core/data/base/find-all-data.spec.ts +++ b/src/app/core/data/base/find-all-data.spec.ts @@ -5,24 +5,33 @@ * * http://www.dspace.org/license/ */ -import { FindAllData, FindAllDataImpl } from './find-all-data'; -import { FindListOptions } from '../find-list-options.model'; -import { followLink } from '../../../shared/utils/follow-link-config.model'; -import { getMockRequestService } from '../../../shared/mocks/request.service.mock'; -import { HALEndpointServiceStub } from '../../../shared/testing/hal-endpoint-service.stub'; -import { getMockRemoteDataBuildService } from '../../../shared/mocks/remote-data-build.service.mock'; +import { + Observable, + of as observableOf, +} from 'rxjs'; import { TestScheduler } from 'rxjs/testing'; -import { RemoteData } from '../remote-data'; -import { RequestEntryState } from '../request-entry-state.model'; -import { SortDirection, SortOptions } from '../../cache/models/sort-options.model'; -import { RequestParam } from '../../cache/models/request-param.model'; -import { RequestService } from '../request.service'; +import { getMockRemoteDataBuildService } from '../../../shared/mocks/remote-data-build.service.mock'; +import { getMockRequestService } from '../../../shared/mocks/request.service.mock'; +import { HALEndpointServiceStub } from '../../../shared/testing/hal-endpoint-service.stub'; +import { followLink } from '../../../shared/utils/follow-link-config.model'; import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service'; -import { HALEndpointService } from '../../shared/hal-endpoint.service'; +import { RequestParam } from '../../cache/models/request-param.model'; +import { + SortDirection, + SortOptions, +} from '../../cache/models/sort-options.model'; import { ObjectCacheService } from '../../cache/object-cache.service'; -import { Observable, of as observableOf } from 'rxjs'; +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 { + FindAllData, + FindAllDataImpl, +} from './find-all-data'; /** * Tests whether calls to `FindAllData` methods are correctly patched through in a concrete data service that implements it @@ -143,8 +152,8 @@ describe('FindAllDataImpl', () => { options = {}; (service as any).getFindAllHref(options).subscribe((value) => { - expect(value).toBe(endpoint); - }, + expect(value).toBe(endpoint); + }, ); }); diff --git a/src/app/core/data/base/find-all-data.ts b/src/app/core/data/base/find-all-data.ts index 57884e537e5..0d68689e611 100644 --- a/src/app/core/data/base/find-all-data.ts +++ b/src/app/core/data/base/find-all-data.ts @@ -7,18 +7,23 @@ */ import { Observable } from 'rxjs'; -import { FindListOptions } from '../find-list-options.model'; -import { FollowLinkConfig } from '../../../shared/utils/follow-link-config.model'; -import { RemoteData } from '../remote-data'; -import { PaginatedList } from '../paginated-list.model'; -import { CacheableObject } from '../../cache/cacheable-object.model'; -import { BaseDataService } from './base-data.service'; -import { distinctUntilChanged, filter, map } from 'rxjs/operators'; +import { + distinctUntilChanged, + filter, + map, +} from 'rxjs/operators'; + import { isNotEmpty } from '../../../shared/empty.util'; -import { RequestService } from '../request.service'; +import { FollowLinkConfig } from '../../../shared/utils/follow-link-config.model'; import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service'; +import { CacheableObject } from '../../cache/cacheable-object.model'; import { ObjectCacheService } from '../../cache/object-cache.service'; import { HALEndpointService } from '../../shared/hal-endpoint.service'; +import { FindListOptions } from '../find-list-options.model'; +import { PaginatedList } from '../paginated-list.model'; +import { RemoteData } from '../remote-data'; +import { RequestService } from '../request.service'; +import { BaseDataService } from './base-data.service'; /** * Interface for a data service that list all of its objects. @@ -42,7 +47,7 @@ export interface FindAllData { } /** - * A DataService feature to list all objects. + * A UpdateDataServiceImpl feature to list all objects. * * Concrete data services can use this feature by implementing {@link FindAllData} * and delegating its method to an inner instance of this class. @@ -87,10 +92,9 @@ export class FindAllDataImpl extends BaseDataService< * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved */ getFindAllHref(options: FindListOptions = {}, linkPath?: string, ...linksToFollow: FollowLinkConfig[]): Observable { - let endpoint$: Observable; const args = []; - endpoint$ = this.getBrowseEndpoint(options).pipe( + const endpoint$ = this.getBrowseEndpoint(options).pipe( filter((href: string) => isNotEmpty(href)), map((href: string) => isNotEmpty(linkPath) ? `${href}/${linkPath}` : href), distinctUntilChanged(), diff --git a/src/app/core/data/base/hal-data-service.interface.ts b/src/app/core/data/base/hal-data-service.interface.ts index 6959399760c..1ffdffaa7c5 100644 --- a/src/app/core/data/base/hal-data-service.interface.ts +++ b/src/app/core/data/base/hal-data-service.interface.ts @@ -6,11 +6,12 @@ * http://www.dspace.org/license/ */ import { Observable } from 'rxjs'; + import { FollowLinkConfig } from '../../../shared/utils/follow-link-config.model'; -import { RemoteData } from '../remote-data'; +import { HALResource } from '../../shared/hal-resource.model'; import { FindListOptions } from '../find-list-options.model'; import { PaginatedList } from '../paginated-list.model'; -import { HALResource } from '../../shared/hal-resource.model'; +import { RemoteData } from '../remote-data'; /** * An interface defining the minimum functionality needed for a data service to resolve HAL resources. 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 11af83ff9f6..9281a5c0eb0 100644 --- a/src/app/core/data/base/identifiable-data.service.spec.ts +++ b/src/app/core/data/base/identifiable-data.service.spec.ts @@ -5,23 +5,24 @@ * * http://www.dspace.org/license/ */ -import { FindListOptions } from '../find-list-options.model'; +import { of as observableOf } from 'rxjs'; +import { TestScheduler } from 'rxjs/testing'; + +import { getMockRemoteDataBuildService } from '../../../shared/mocks/remote-data-build.service.mock'; import { getMockRequestService } from '../../../shared/mocks/request.service.mock'; import { HALEndpointServiceStub } from '../../../shared/testing/hal-endpoint-service.stub'; -import { getMockRemoteDataBuildService } from '../../../shared/mocks/remote-data-build.service.mock'; import { followLink } from '../../../shared/utils/follow-link-config.model'; -import { TestScheduler } from 'rxjs/testing'; -import { RemoteData } from '../remote-data'; -import { RequestEntryState } from '../request-entry-state.model'; -import { Observable, of as observableOf } from 'rxjs'; -import { RequestService } from '../request.service'; import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service'; -import { HALEndpointService } from '../../shared/hal-endpoint.service'; import { ObjectCacheService } from '../../cache/object-cache.service'; -import { IdentifiableDataService } from './identifiable-data.service'; +import { HALEndpointService } from '../../shared/hal-endpoint.service'; +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( @@ -30,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); } } @@ -51,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 = { @@ -63,12 +60,12 @@ describe('IdentifiableDataService', () => { }, getByHref: () => { /* empty */ - } + }, } as any; selfLink = 'https://rest.api/endpoint/1698f1d3-be98-4c51-9fd8-6bfedcbd59b7'; linksToFollow = [ followLink('a'), - followLink('b') + followLink('b'), ]; testScheduler = new TestScheduler((actual, expected) => { @@ -132,7 +129,7 @@ describe('IdentifiableDataService', () => { resourceIdMock, followLink('bundles', { shouldEmbed: false }), followLink('owningCollection', { shouldEmbed: false }), - followLink('templateItemOf') + followLink('templateItemOf'), ); expect(result).toEqual(expected); }); @@ -143,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 904f925765c..ba368d21ca1 100644 --- a/src/app/core/data/base/identifiable-data.service.ts +++ b/src/app/core/data/base/identifiable-data.service.ts @@ -5,16 +5,21 @@ * * http://www.dspace.org/license/ */ -import { CacheableObject } from '../../cache/cacheable-object.model'; -import { FollowLinkConfig } from '../../../shared/utils/follow-link-config.model'; import { Observable } from 'rxjs'; -import { map } from 'rxjs/operators'; -import { RemoteData } from '../remote-data'; -import { BaseDataService } from './base-data.service'; -import { RequestService } from '../request.service'; +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'; +import { CacheableObject } from '../../cache/cacheable-object.model'; import { ObjectCacheService } from '../../cache/object-cache.service'; import { HALEndpointService } from '../../shared/hal-endpoint.service'; +import { RemoteData } from '../remote-data'; +import { RequestService } from '../request.service'; +import { BaseDataService } from './base-data.service'; /** * Shorthand type for the method to construct an ID endpoint. @@ -80,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/base/patch-data.spec.ts b/src/app/core/data/base/patch-data.spec.ts index a55b1229b87..03c15199a7c 100644 --- a/src/app/core/data/base/patch-data.spec.ts +++ b/src/app/core/data/base/patch-data.spec.ts @@ -6,28 +6,38 @@ * http://www.dspace.org/license/ */ /* eslint-disable max-classes-per-file */ -import { RequestService } from '../request.service'; +import { + compare, + Operation, +} from 'fast-json-patch'; +import { + Observable, + of as observableOf, +} from 'rxjs'; +import { TestScheduler } from 'rxjs/testing'; + +import { getMockRemoteDataBuildService } from '../../../shared/mocks/remote-data-build.service.mock'; +import { getMockRequestService } from '../../../shared/mocks/request.service.mock'; +import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; +import { HALEndpointServiceStub } from '../../../shared/testing/hal-endpoint-service.stub'; +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 { DSpaceObject } from '../../shared/dspace-object.model'; import { HALEndpointService } from '../../shared/hal-endpoint.service'; +import { Item } from '../../shared/item.model'; +import { ChangeAnalyzer } from '../change-analyzer'; import { FindListOptions } from '../find-list-options.model'; -import { Observable, of as observableOf } from 'rxjs'; -import { getMockRequestService } from '../../../shared/mocks/request.service.mock'; -import { HALEndpointServiceStub } from '../../../shared/testing/hal-endpoint-service.stub'; -import { getMockRemoteDataBuildService } from '../../../shared/mocks/remote-data-build.service.mock'; -import { followLink } from '../../../shared/utils/follow-link-config.model'; -import { TestScheduler } from 'rxjs/testing'; import { RemoteData } from '../remote-data'; -import { RequestEntryState } from '../request-entry-state.model'; -import { PatchData, PatchDataImpl } from './patch-data'; -import { ChangeAnalyzer } from '../change-analyzer'; -import { Item } from '../../shared/item.model'; -import { compare, Operation } from 'fast-json-patch'; import { PatchRequest } from '../request.models'; -import { DSpaceObject } from '../../shared/dspace-object.model'; -import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; -import { constructIdEndpointDefault } from './identifiable-data.service'; +import { RequestService } from '../request.service'; +import { RequestEntryState } from '../request-entry-state.model'; import { RestRequestMethod } from '../rest-request-method'; +import { constructIdEndpointDefault } from './identifiable-data.service'; +import { + PatchData, + PatchDataImpl, +} from './patch-data'; /** * Tests whether calls to `PatchData` methods are correctly patched through in a concrete data service that implements it @@ -182,15 +192,15 @@ describe('PatchDataImpl', () => { _links: { self: { href: 'dso-href', - } - } + }, + }, }; const operations = [ Object.assign({ op: 'move', from: '/1', - path: '/5' - }) as Operation + path: '/5', + }) as Operation, ]; it('should send a PatchRequest', () => { @@ -224,12 +234,12 @@ describe('PatchDataImpl', () => { dso = Object.assign(new DSpaceObject(), { _links: { self: { href: selfLink } }, - metadata: [{ key: 'dc.title', value: name1 }] + metadata: [{ key: 'dc.title', value: name1 }], }); dso2 = Object.assign(new DSpaceObject(), { _links: { self: { href: selfLink } }, - metadata: [{ key: 'dc.title', value: name2 }] + metadata: [{ key: 'dc.title', value: name2 }], }); spyOn(service, 'findByHref').and.returnValue(createSuccessfulRemoteDataObject$(dso)); diff --git a/src/app/core/data/base/patch-data.ts b/src/app/core/data/base/patch-data.ts index e30c394a347..adcf98ef947 100644 --- a/src/app/core/data/base/patch-data.ts +++ b/src/app/core/data/base/patch-data.ts @@ -5,22 +5,36 @@ * * http://www.dspace.org/license/ */ -import { CacheableObject } from '../../cache/cacheable-object.model'; import { Operation } from 'fast-json-patch'; import { Observable } from 'rxjs'; +import { + find, + map, + mergeMap, +} from 'rxjs/operators'; + +import { + hasNoValue, + hasValue, + isNotEmpty, +} from '../../../shared/empty.util'; +import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service'; +import { CacheableObject } from '../../cache/cacheable-object.model'; +import { ObjectCacheService } from '../../cache/object-cache.service'; +import { HALEndpointService } from '../../shared/hal-endpoint.service'; +import { + getFirstSucceededRemoteData, + getRemoteDataPayload, +} from '../../shared/operators'; +import { ChangeAnalyzer } from '../change-analyzer'; import { RemoteData } from '../remote-data'; -import { find, map, mergeMap } from 'rxjs/operators'; -import { hasNoValue, hasValue, isNotEmpty } from '../../../shared/empty.util'; import { PatchRequest } from '../request.models'; -import { getFirstSucceededRemoteData, getRemoteDataPayload } from '../../shared/operators'; -import { ChangeAnalyzer } from '../change-analyzer'; import { RequestService } from '../request.service'; -import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service'; -import { HALEndpointService } from '../../shared/hal-endpoint.service'; -import { ObjectCacheService } from '../../cache/object-cache.service'; import { RestRequestMethod } from '../rest-request-method'; -import { ConstructIdEndpoint, IdentifiableDataService } from './identifiable-data.service'; - +import { + ConstructIdEndpoint, + IdentifiableDataService, +} from './identifiable-data.service'; /** * Interface for a data service that can patch and update objects. @@ -54,7 +68,7 @@ export interface PatchData { } /** - * A DataService feature to patch and update objects. + * A UpdateDataServiceImpl feature to patch and update objects. * * Concrete data services can use this feature by implementing {@link PatchData} * and delegating its method to an inner instance of this class. diff --git a/src/app/core/data/base/put-data.spec.ts b/src/app/core/data/base/put-data.spec.ts index 6287fe91b13..1430bb31061 100644 --- a/src/app/core/data/base/put-data.spec.ts +++ b/src/app/core/data/base/put-data.spec.ts @@ -6,20 +6,27 @@ * http://www.dspace.org/license/ */ -import { RequestService } from '../request.service'; +import { + Observable, + of as observableOf, +} from 'rxjs'; + +import { getMockRemoteDataBuildService } from '../../../shared/mocks/remote-data-build.service.mock'; +import { getMockRequestService } from '../../../shared/mocks/request.service.mock'; +import { HALEndpointServiceStub } from '../../../shared/testing/hal-endpoint-service.stub'; import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service'; import { ObjectCacheService } from '../../cache/object-cache.service'; +import { DSpaceObject } from '../../shared/dspace-object.model'; import { HALEndpointService } from '../../shared/hal-endpoint.service'; import { FindListOptions } from '../find-list-options.model'; -import { Observable, of as observableOf } from 'rxjs'; -import { getMockRequestService } from '../../../shared/mocks/request.service.mock'; -import { HALEndpointServiceStub } from '../../../shared/testing/hal-endpoint-service.stub'; -import { getMockRemoteDataBuildService } from '../../../shared/mocks/remote-data-build.service.mock'; import { RemoteData } from '../remote-data'; +import { RequestService } from '../request.service'; import { RequestEntryState } from '../request-entry-state.model'; -import { PutData, PutDataImpl } from './put-data'; import { RestRequestMethod } from '../rest-request-method'; -import { DSpaceObject } from '../../shared/dspace-object.model'; +import { + PutData, + PutDataImpl, +} from './put-data'; /** * Tests whether calls to `PutData` methods are correctly patched through in a concrete data service that implements it @@ -127,7 +134,7 @@ describe('PutDataImpl', () => { metadata: { // recognized properties will be serialized ['dc.title']: [ { language: 'en', value: 'some object' }, - ] + ], }, data: [ 1, 2, 3, 4 ], // unrecognized properties won't be serialized _links: { self: { href: selfLink } }, @@ -144,7 +151,7 @@ describe('PutDataImpl', () => { method: RestRequestMethod.PUT, body: { // _links are not serialized uuid: obj.uuid, - metadata: obj.metadata + metadata: obj.metadata, }, })); done(); diff --git a/src/app/core/data/base/put-data.ts b/src/app/core/data/base/put-data.ts index bd2a8d29299..e9d2b01eb83 100644 --- a/src/app/core/data/base/put-data.ts +++ b/src/app/core/data/base/put-data.ts @@ -5,18 +5,19 @@ * * http://www.dspace.org/license/ */ -import { CacheableObject } from '../../cache/cacheable-object.model'; -import { BaseDataService } from './base-data.service'; import { Observable } from 'rxjs'; -import { RemoteData } from '../remote-data'; -import { DSpaceSerializer } from '../../dspace-rest/dspace.serializer'; -import { GenericConstructor } from '../../shared/generic-constructor'; -import { PutRequest } from '../request.models'; + import { hasValue } from '../../../shared/empty.util'; -import { RequestService } from '../request.service'; import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service'; +import { CacheableObject } from '../../cache/cacheable-object.model'; import { ObjectCacheService } from '../../cache/object-cache.service'; +import { DSpaceSerializer } from '../../dspace-rest/dspace.serializer'; +import { GenericConstructor } from '../../shared/generic-constructor'; import { HALEndpointService } from '../../shared/hal-endpoint.service'; +import { RemoteData } from '../remote-data'; +import { PutRequest } from '../request.models'; +import { RequestService } from '../request.service'; +import { BaseDataService } from './base-data.service'; /** * Interface for a data service that can send PUT requests. @@ -31,7 +32,7 @@ export interface PutData { } /** - * A DataService feature to send PUT requests. + * A UpdateDataServiceImpl feature to send PUT requests. * * Concrete data services can use this feature by implementing {@link PutData} * and delegating its method to an inner instance of this class. @@ -55,7 +56,7 @@ export class PutDataImpl extends BaseDataService i */ put(object: T): Observable> { const requestId = this.requestService.generateRequestId(); - const serializedObject = new DSpaceSerializer(object.constructor as GenericConstructor<{}>).serialize(object); + const serializedObject = new DSpaceSerializer(object.constructor as GenericConstructor).serialize(object); const request = new PutRequest(requestId, object._links.self.href, serializedObject); if (hasValue(this.responseMsToLive)) { diff --git a/src/app/core/data/base/search-data.spec.ts b/src/app/core/data/base/search-data.spec.ts index 31dddeddfce..af9f87bf2cf 100644 --- a/src/app/core/data/base/search-data.spec.ts +++ b/src/app/core/data/base/search-data.spec.ts @@ -5,12 +5,17 @@ * * http://www.dspace.org/license/ */ -import { constructSearchEndpointDefault, SearchData, SearchDataImpl } from './search-data'; -import { FindListOptions } from '../find-list-options.model'; -import { followLink } from '../../../shared/utils/follow-link-config.model'; import { of as observableOf } from 'rxjs'; -import { getMockRequestService } from '../../../shared/mocks/request.service.mock'; + import { getMockRemoteDataBuildService } from '../../../shared/mocks/remote-data-build.service.mock'; +import { getMockRequestService } from '../../../shared/mocks/request.service.mock'; +import { followLink } from '../../../shared/utils/follow-link-config.model'; +import { FindListOptions } from '../find-list-options.model'; +import { + constructSearchEndpointDefault, + SearchData, + SearchDataImpl, +} from './search-data'; /** * Tests whether calls to `SearchData` methods are correctly patched through in a concrete data service that implements it diff --git a/src/app/core/data/base/search-data.ts b/src/app/core/data/base/search-data.ts index 536d6d6e254..f758affa280 100644 --- a/src/app/core/data/base/search-data.ts +++ b/src/app/core/data/base/search-data.ts @@ -5,19 +5,26 @@ * * http://www.dspace.org/license/ */ -import { CacheableObject } from '../../cache/cacheable-object.model'; -import { BaseDataService } from './base-data.service'; import { Observable } from 'rxjs'; -import { filter, map } from 'rxjs/operators'; -import { hasNoValue, isNotEmpty } from '../../../shared/empty.util'; -import { FindListOptions } from '../find-list-options.model'; +import { + filter, + map, +} from 'rxjs/operators'; + +import { + hasNoValue, + isNotEmpty, +} from '../../../shared/empty.util'; import { FollowLinkConfig } from '../../../shared/utils/follow-link-config.model'; -import { RemoteData } from '../remote-data'; -import { PaginatedList } from '../paginated-list.model'; -import { RequestService } from '../request.service'; import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service'; +import { CacheableObject } from '../../cache/cacheable-object.model'; import { ObjectCacheService } from '../../cache/object-cache.service'; import { HALEndpointService } from '../../shared/hal-endpoint.service'; +import { FindListOptions } from '../find-list-options.model'; +import { PaginatedList } from '../paginated-list.model'; +import { RemoteData } from '../remote-data'; +import { RequestService } from '../request.service'; +import { BaseDataService } from './base-data.service'; /** * Shorthand type for method to construct a search endpoint @@ -51,7 +58,7 @@ export interface SearchData { } /** - * A DataService feature to search for objects. + * A UpdateDataServiceImpl feature to search for objects. * * Concrete data services can use this feature by implementing {@link SearchData} * and delegating its method to an inner instance of this class. @@ -112,10 +119,9 @@ export class SearchDataImpl extends BaseDataService[]): Observable { - let result$: Observable; const args = []; - result$ = this.getSearchEndpoint(searchMethod); + const result$ = this.getSearchEndpoint(searchMethod); return result$.pipe(map((result: string) => this.buildHrefFromFindOptions(result, options, args, ...linksToFollow))); } diff --git a/src/app/core/data/bitstream-data.service.spec.ts b/src/app/core/data/bitstream-data.service.spec.ts index 89178f8dd21..91b5132ce06 100644 --- a/src/app/core/data/bitstream-data.service.spec.ts +++ b/src/app/core/data/bitstream-data.service.spec.ts @@ -1,26 +1,41 @@ import { TestBed } from '@angular/core/testing'; -import { BitstreamDataService } from './bitstream-data.service'; +import { cold } from 'jasmine-marbles'; +import { + Observable, + of as observableOf, +} from 'rxjs'; +import { ItemMock } from 'src/app/shared/mocks/item.mock'; +import { + createFailedRemoteDataObject, + createSuccessfulRemoteDataObject, +} from 'src/app/shared/remote-data.utils'; + +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 { 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 { RequestService } from './request.service'; import { Bitstream } from '../shared/bitstream.model'; -import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { BitstreamFormatDataService } from './bitstream-format-data.service'; -import { Observable, of as observableOf } from 'rxjs'; import { BitstreamFormat } from '../shared/bitstream-format.model'; import { BitstreamFormatSupportLevel } from '../shared/bitstream-format-support-level'; -import { PatchRequest, PutRequest } from './request.models'; -import { getMockRequestService } from '../../shared/mocks/request.service.mock'; -import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub'; -import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; -import { getMockRemoteDataBuildService } from '../../shared/mocks/remote-data-build.service.mock'; -import { testSearchDataImplementation } from './base/search-data.spec'; -import { testPatchDataImplementation } from './base/patch-data.spec'; +import { Bundle } from '../shared/bundle.model'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; import { testDeleteDataImplementation } from './base/delete-data.spec'; +import { testPatchDataImplementation } from './base/patch-data.spec'; +import { testSearchDataImplementation } from './base/search-data.spec'; +import { BitstreamDataService } from './bitstream-data.service'; +import { BitstreamFormatDataService } from './bitstream-format-data.service'; +import { BundleDataService } from './bundle-data.service'; import { DSOChangeAnalyzer } from './dso-change-analyzer.service'; -import { NotificationsService } from '../../shared/notifications/notifications.service'; -import objectContaining = jasmine.objectContaining; import { RemoteData } from './remote-data'; -import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; +import { + PatchRequest, + PutRequest, +} from './request.models'; +import { RequestService } from './request.service'; +import objectContaining = jasmine.objectContaining; describe('BitstreamDataService', () => { let service: BitstreamDataService; @@ -29,39 +44,41 @@ describe('BitstreamDataService', () => { let halService: HALEndpointService; let bitstreamFormatService: BitstreamFormatDataService; let rdbService: RemoteDataBuildService; + let bundleDataService: BundleDataService; const bitstreamFormatHref = 'rest-api/bitstreamformats'; const bitstream1 = Object.assign(new Bitstream(), { id: 'fake-bitstream1', uuid: 'fake-bitstream1', _links: { - self: { href: 'fake-bitstream1-self' } - } + self: { href: 'fake-bitstream1-self' }, + }, }); const bitstream2 = Object.assign(new Bitstream(), { id: 'fake-bitstream2', uuid: 'fake-bitstream2', _links: { - self: { href: 'fake-bitstream2-self' } - } + self: { href: 'fake-bitstream2-self' }, + }, }); const format = Object.assign(new BitstreamFormat(), { id: '2', shortDescription: 'PNG', description: 'Portable Network Graphics', - supportLevel: BitstreamFormatSupportLevel.Known + supportLevel: BitstreamFormatSupportLevel.Known, }); const url = 'fake-bitstream-url'; beforeEach(() => { objectCache = jasmine.createSpyObj('objectCache', { - remove: jasmine.createSpy('remove') + remove: jasmine.createSpy('remove'), }); requestService = getMockRequestService(); halService = Object.assign(new HALEndpointServiceStub(url)); bitstreamFormatService = jasmine.createSpyObj('bistreamFormatService', { - getBrowseEndpoint: observableOf(bitstreamFormatHref) + getBrowseEndpoint: observableOf(bitstreamFormatHref), }); + rdbService = getMockRemoteDataBuildService(); TestBed.configureTestingModule({ @@ -76,6 +93,7 @@ describe('BitstreamDataService', () => { ], }); service = TestBed.inject(BitstreamDataService); + bundleDataService = TestBed.inject(BundleDataService); }); describe('composition', () => { @@ -118,6 +136,32 @@ describe('BitstreamDataService', () => { expect(service.invalidateByHref).toHaveBeenCalledWith('fake-bitstream1-self'); }); + describe('findPrimaryBitstreamByItemAndName', () => { + it('should return primary bitstream', () => { + 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(expected$); + }); + + it('should return null if primary bitstream has not be succeeded ', () => { + 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(expected$); + }); + + it('should return EMPTY if nothing where found', () => { + const expected$ = cold('(|)', {} ); + spyOn(bundleDataService, 'findByItemAndName').and.returnValue(observableOf(createFailedRemoteDataObject())); + expect(service.findPrimaryBitstreamByItemAndName(ItemMock, 'ORIGINAL')).toBeObservable(expected$); + }); + }); + it('should be able to delete multiple bitstreams', () => { service.removeMultiple([bitstream1, bitstream2]); diff --git a/src/app/core/data/bitstream-data.service.ts b/src/app/core/data/bitstream-data.service.ts index bb4ec281665..b8776d25309 100644 --- a/src/app/core/data/bitstream-data.service.ts +++ b/src/app/core/data/bitstream-data.service.ts @@ -1,47 +1,74 @@ import { HttpHeaders } from '@angular/common/http'; import { Injectable } from '@angular/core'; -import { combineLatest as observableCombineLatest, Observable } from 'rxjs'; -import { find, map, switchMap, take } from 'rxjs/operators'; +import { + Operation, + RemoveOperation, +} from 'fast-json-patch'; +import { + combineLatest as observableCombineLatest, + EMPTY, + Observable, +} from 'rxjs'; +import { + find, + map, + switchMap, + take, +} from 'rxjs/operators'; + import { hasValue } from '../../shared/empty.util'; -import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; +import { + followLink, + 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 { 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'; import { Item } from '../shared/item.model'; -import { BundleDataService } from './bundle-data.service'; -import { buildPaginatedList, PaginatedList } from './paginated-list.model'; -import { RemoteData } from './remote-data'; -import { PatchRequest, PutRequest } from './request.models'; -import { RequestService } from './request.service'; -import { BitstreamFormatDataService } from './bitstream-format-data.service'; -import { BitstreamFormat } from '../shared/bitstream-format.model'; -import { HttpOptions } from '../dspace-rest/dspace-rest.service'; -import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; +import { NoContent } from '../shared/NoContent.model'; +import { getFirstCompletedRemoteData } from '../shared/operators'; import { PageInfo } from '../shared/page-info.model'; -import { RequestParam } from '../cache/models/request-param.model'; import { sendRequest } from '../shared/request.operators'; -import { FindListOptions } from './find-list-options.model'; -import { SearchData, SearchDataImpl } from './base/search-data'; -import { PatchData, PatchDataImpl } from './base/patch-data'; +import { + DeleteData, + DeleteDataImpl, +} from './base/delete-data'; +import { IdentifiableDataService } from './base/identifiable-data.service'; +import { + PatchData, + PatchDataImpl, +} from './base/patch-data'; +import { + SearchData, + SearchDataImpl, +} from './base/search-data'; +import { BitstreamFormatDataService } from './bitstream-format-data.service'; +import { BundleDataService } from './bundle-data.service'; import { DSOChangeAnalyzer } from './dso-change-analyzer.service'; +import { FindListOptions } from './find-list-options.model'; +import { + buildPaginatedList, + PaginatedList, +} from './paginated-list.model'; +import { RemoteData } from './remote-data'; +import { + PatchRequest, + PutRequest, +} from './request.models'; +import { RequestService } from './request.service'; import { RestRequestMethod } from './rest-request-method'; -import { DeleteData, DeleteDataImpl } from './base/delete-data'; -import { NotificationsService } from '../../shared/notifications/notifications.service'; -import { NoContent } from '../shared/NoContent.model'; -import { IdentifiableDataService } from './base/identifiable-data.service'; -import { dataService } from './base/data-service.decorator'; -import { Operation, RemoveOperation } from 'fast-json-patch'; /** * 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; @@ -107,7 +134,7 @@ export class BitstreamDataService extends IdentifiableDataService imp } else { return [bundleRD as any]; } - }) + }), ); } @@ -120,10 +147,10 @@ export class BitstreamDataService extends IdentifiableDataService imp const requestId = this.requestService.generateRequestId(); const bitstreamHref$ = this.getBrowseEndpoint().pipe( map((href: string) => `${href}/${bitstream.id}`), - switchMap((href: string) => this.halService.getEndpoint('format', href)) + switchMap((href: string) => this.halService.getEndpoint('format', href)), ); const formatHref$ = this.bitstreamFormatService.getBrowseEndpoint().pipe( - map((href: string) => `${href}/${format.id}`) + map((href: string) => `${href}/${format.id}`), ); observableCombineLatest([bitstreamHref$, formatHref$]).pipe( map(([bitstreamHref, formatHref]) => { @@ -134,7 +161,7 @@ export class BitstreamDataService extends IdentifiableDataService imp return new PutRequest(requestId, bitstreamHref, formatHref, options); }), sendRequest(this.requestService), - take(1) + take(1), ).subscribe(() => { this.requestService.removeByHrefSubstring(bitstream.self + '/format'); }); @@ -177,7 +204,7 @@ export class BitstreamDataService extends IdentifiableDataService imp const hrefObs = this.getSearchByHref( 'byItemHandle', { searchParams }, - ...linksToFollow + ...linksToFollow, ); return this.findByHref( @@ -201,6 +228,37 @@ export class BitstreamDataService extends IdentifiableDataService imp return this.searchData.getSearchByHref(searchMethod, options, ...linksToFollow); } + + /** + * + * Make a request to get primary bitstream + * in all current use cases, and having it simplifies this method + * + * @param item the {@link Item} the {@link Bundle} is a part of + * @param bundleName the name of the {@link Bundle} we want to find + * {@link Bitstream}s for + * @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's + * no valid cached version. Defaults to true + * @param reRequestOnStale Whether or not the request should automatically be re- + * requested after the response becomes stale + * @return {Observable} + * 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( + getFirstCompletedRemoteData(), + switchMap((rd: RemoteData) => { + if (!rd.hasSucceeded) { + return EMPTY; + } + return rd.payload.primaryBitstream.pipe( + getFirstCompletedRemoteData(), + map((rdb: RemoteData) => rdb.hasSucceeded ? rdb.payload : null), + ); + }), + ); + } + /** * Make a new FindListRequest with given search method * diff --git a/src/app/core/data/bitstream-format-data.service.spec.ts b/src/app/core/data/bitstream-format-data.service.spec.ts index 15efebe8c72..234326d453b 100644 --- a/src/app/core/data/bitstream-format-data.service.spec.ts +++ b/src/app/core/data/bitstream-format-data.service.spec.ts @@ -1,21 +1,36 @@ -import { BitstreamFormatDataService } from './bitstream-format-data.service'; -import { RestResponse } from '../cache/response.models'; -import { Observable, of as observableOf } from 'rxjs'; -import { Action, Store } from '@ngrx/store'; -import { ObjectCacheService } from '../cache/object-cache.service'; -import { cold, getTestScheduler, hot } from 'jasmine-marbles'; -import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { NotificationsService } from '../../shared/notifications/notifications.service'; -import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; -import { BitstreamFormat } from '../shared/bitstream-format.model'; import { waitForAsync } from '@angular/core/testing'; -import { BitstreamFormatsRegistryDeselectAction, BitstreamFormatsRegistryDeselectAllAction, BitstreamFormatsRegistrySelectAction } from '../../admin/admin-registries/bitstream-formats/bitstream-format.actions'; +import { + Action, + Store, +} from '@ngrx/store'; +import { + cold, + getTestScheduler, + hot, +} from 'jasmine-marbles'; +import { + Observable, + of as observableOf, +} from 'rxjs'; import { TestScheduler } from 'rxjs/testing'; + +import { + BitstreamFormatsRegistryDeselectAction, + BitstreamFormatsRegistryDeselectAllAction, + BitstreamFormatsRegistrySelectAction, +} from '../../admin/admin-registries/bitstream-formats/bitstream-format.actions'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { ObjectCacheService } from '../cache/object-cache.service'; +import { RestResponse } from '../cache/response.models'; import { CoreState } from '../core-state.model'; -import { RequestEntry } from './request-entry.model'; -import { testFindAllDataImplementation } from './base/find-all-data.spec'; +import { BitstreamFormat } from '../shared/bitstream-format.model'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; import { testDeleteDataImplementation } from './base/delete-data.spec'; +import { testFindAllDataImplementation } from './base/find-all-data.spec'; +import { BitstreamFormatDataService } from './bitstream-format-data.service'; +import { RequestEntry } from './request-entry.model'; describe('BitstreamFormatDataService', () => { let service: BitstreamFormatDataService; @@ -31,19 +46,19 @@ describe('BitstreamFormatDataService', () => { const store = { dispatch(action: Action) { // Do Nothing - } + }, } as Store; const requestUUIDs = ['some', 'uuid']; const objectCache = jasmine.createSpyObj('objectCache', { - getByHref: observableOf({ requestUUIDs }) + getByHref: observableOf({ requestUUIDs }), }) as ObjectCacheService; const halEndpointService = { getEndpoint(linkPath: string): Observable { return cold('a', { a: bitstreamFormatsEndpoint }); - } + }, } as HALEndpointService; const notificationsService = {} as NotificationsService; @@ -83,7 +98,7 @@ describe('BitstreamFormatDataService', () => { getByUUID: cold('a', { a: responseCacheEntry }), setStaleByUUID: observableOf(true), generateRequestId: 'request-id', - removeByHrefSubstring: {} + removeByHrefSubstring: {}, }); service = initTestService(halEndpointService); })); @@ -104,7 +119,7 @@ describe('BitstreamFormatDataService', () => { getByUUID: cold('a', { a: responseCacheEntry }), setStaleByUUID: observableOf(true), generateRequestId: 'request-id', - removeByHrefSubstring: {} + removeByHrefSubstring: {}, }); service = initTestService(halEndpointService); })); @@ -127,7 +142,7 @@ describe('BitstreamFormatDataService', () => { getByUUID: cold('a', { a: responseCacheEntry }), setStaleByUUID: observableOf(true), generateRequestId: 'request-id', - removeByHrefSubstring: {} + removeByHrefSubstring: {}, }); service = initTestService(halEndpointService); })); @@ -149,7 +164,7 @@ describe('BitstreamFormatDataService', () => { getByUUID: cold('a', { a: responseCacheEntry }), setStaleByUUID: observableOf(true), generateRequestId: 'request-id', - removeByHrefSubstring: {} + removeByHrefSubstring: {}, }); service = initTestService(halEndpointService); })); @@ -174,7 +189,7 @@ describe('BitstreamFormatDataService', () => { getByUUID: cold('a', { a: responseCacheEntry }), setStaleByUUID: observableOf(true), generateRequestId: 'request-id', - removeByHrefSubstring: {} + removeByHrefSubstring: {}, }); service = initTestService(halEndpointService); })); @@ -198,12 +213,12 @@ describe('BitstreamFormatDataService', () => { getByUUID: cold('a', { a: responseCacheEntry }), setStaleByUUID: observableOf(true), generateRequestId: 'request-id', - removeByHrefSubstring: {} + removeByHrefSubstring: {}, }); const halService = { getEndpoint(linkPath: string): Observable { return observableOf(bitstreamFormatsEndpoint); - } + }, } as HALEndpointService; service = initTestService(halService); service.clearBitStreamFormatRequests().subscribe(); @@ -222,7 +237,7 @@ describe('BitstreamFormatDataService', () => { getByUUID: cold('a', { a: responseCacheEntry }), setStaleByUUID: observableOf(true), generateRequestId: 'request-id', - removeByHrefSubstring: {} + removeByHrefSubstring: {}, }); service = initTestService(halEndpointService); spyOn(store, 'dispatch'); @@ -245,7 +260,7 @@ describe('BitstreamFormatDataService', () => { getByUUID: cold('a', { a: responseCacheEntry }), setStaleByUUID: observableOf(true), generateRequestId: 'request-id', - removeByHrefSubstring: {} + removeByHrefSubstring: {}, }); service = initTestService(halEndpointService); spyOn(store, 'dispatch'); @@ -268,7 +283,7 @@ describe('BitstreamFormatDataService', () => { getByUUID: cold('a', { a: responseCacheEntry }), setStaleByUUID: observableOf(true), generateRequestId: 'request-id', - removeByHrefSubstring: {} + removeByHrefSubstring: {}, }); service = initTestService(halEndpointService); spyOn(store, 'dispatch'); @@ -289,12 +304,12 @@ describe('BitstreamFormatDataService', () => { getByUUID: hot('a', { a: responseCacheEntry }), setStaleByUUID: observableOf(true), generateRequestId: 'request-id', - removeByHrefSubstring: {} + removeByHrefSubstring: {}, }); const halService = { getEndpoint(linkPath: string): Observable { return observableOf(bitstreamFormatsEndpoint); - } + }, } as HALEndpointService; service = initTestService(halService); })); diff --git a/src/app/core/data/bitstream-format-data.service.ts b/src/app/core/data/bitstream-format-data.service.ts index 01043898158..97b0fa961a9 100644 --- a/src/app/core/data/bitstream-format-data.service.ts +++ b/src/app/core/data/bitstream-format-data.service.ts @@ -1,30 +1,50 @@ import { Injectable } from '@angular/core'; -import { createSelector, select, Store } from '@ngrx/store'; +import { + createSelector, + select, + Store, +} from '@ngrx/store'; import { Observable } from 'rxjs'; -import { distinctUntilChanged, map, tap } from 'rxjs/operators'; -import { BitstreamFormatsRegistryDeselectAction, BitstreamFormatsRegistryDeselectAllAction, BitstreamFormatsRegistrySelectAction } from '../../admin/admin-registries/bitstream-formats/bitstream-format.actions'; +import { + distinctUntilChanged, + map, + tap, +} from 'rxjs/operators'; +import { FollowLinkConfig } from 'src/app/shared/utils/follow-link-config.model'; + +import { + BitstreamFormatsRegistryDeselectAction, + BitstreamFormatsRegistryDeselectAllAction, + BitstreamFormatsRegistrySelectAction, +} from '../../admin/admin-registries/bitstream-formats/bitstream-format.actions'; import { BitstreamFormatRegistryState } from '../../admin/admin-registries/bitstream-formats/bitstream-format.reducers'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { ObjectCacheService } from '../cache/object-cache.service'; import { coreSelector } from '../core.selectors'; -import { BitstreamFormat } from '../shared/bitstream-format.model'; -import { BITSTREAM_FORMAT } from '../shared/bitstream-format.resource-type'; +import { CoreState } from '../core-state.model'; import { Bitstream } from '../shared/bitstream.model'; +import { BitstreamFormat } from '../shared/bitstream-format.model'; import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { RemoteData } from './remote-data'; -import { PostRequest, PutRequest } from './request.models'; -import { RequestService } from './request.service'; +import { NoContent } from '../shared/NoContent.model'; import { sendRequest } from '../shared/request.operators'; -import { CoreState } from '../core-state.model'; +import { + DeleteData, + DeleteDataImpl, +} from './base/delete-data'; +import { + FindAllData, + FindAllDataImpl, +} from './base/find-all-data'; import { IdentifiableDataService } from './base/identifiable-data.service'; -import { DeleteData, DeleteDataImpl } from './base/delete-data'; -import { FindAllData, FindAllDataImpl } from './base/find-all-data'; -import { FollowLinkConfig } from 'src/app/shared/utils/follow-link-config.model'; import { FindListOptions } from './find-list-options.model'; import { PaginatedList } from './paginated-list.model'; -import { NoContent } from '../shared/NoContent.model'; -import { dataService } from './base/data-service.decorator'; +import { RemoteData } from './remote-data'; +import { + PostRequest, + PutRequest, +} from './request.models'; +import { RequestService } from './request.service'; const bitstreamFormatsStateSelector = createSelector( coreSelector, @@ -38,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'; @@ -106,7 +125,7 @@ export class BitstreamFormatDataService extends IdentifiableDataService { return new PostRequest(requestId, endpointURL, bitstreamFormat); }), - sendRequest(this.requestService) + sendRequest(this.requestService), ).subscribe(); return this.rdbService.buildFromRequestUUID(requestId); @@ -117,7 +136,7 @@ export class BitstreamFormatDataService extends IdentifiableDataService { return this.getBrowseEndpoint().pipe( - tap((href: string) => this.requestService.removeByHrefSubstring(href)) + tap((href: string) => this.requestService.removeByHrefSubstring(href)), ); } diff --git a/src/app/core/data/browse-response-parsing.service.spec.ts b/src/app/core/data/browse-response-parsing.service.spec.ts index 9fa7239ef7c..53d2ec20fc8 100644 --- a/src/app/core/data/browse-response-parsing.service.spec.ts +++ b/src/app/core/data/browse-response-parsing.service.spec.ts @@ -1,9 +1,9 @@ import { getMockObjectCacheService } from '../../shared/mocks/object-cache.service.mock'; -import { BrowseResponseParsingService } from './browse-response-parsing.service'; import { ObjectCacheService } from '../cache/object-cache.service'; -import { HIERARCHICAL_BROWSE_DEFINITION } from '../shared/hierarchical-browse-definition.resource-type'; import { FLAT_BROWSE_DEFINITION } from '../shared/flat-browse-definition.resource-type'; +import { HIERARCHICAL_BROWSE_DEFINITION } from '../shared/hierarchical-browse-definition.resource-type'; import { VALUE_LIST_BROWSE_DEFINITION } from '../shared/value-list-browse-definition.resource-type'; +import { BrowseResponseParsingService } from './browse-response-parsing.service'; class TestService extends BrowseResponseParsingService { constructor(protected objectCache: ObjectCacheService) { @@ -26,22 +26,22 @@ describe('BrowseResponseParsingService', () => { describe('', () => { const mockFlatBrowse = { - id: 'title', - browseType: 'flatBrowse', - type: 'browse', - }; + id: 'title', + browseType: 'flatBrowse', + type: 'browse', + }; const mockValueList = { - id: 'author', - browseType: 'valueList', - type: 'browse', - }; + id: 'author', + browseType: 'valueList', + type: 'browse', + }; const mockHierarchicalBrowse = { - id: 'srsc', - browseType: 'hierarchicalBrowse', - type: 'browse', - }; + id: 'srsc', + browseType: 'hierarchicalBrowse', + type: 'browse', + }; it('should deserialize flatBrowses correctly', () => { let deserialized = service.deserialize(mockFlatBrowse); diff --git a/src/app/core/data/browse-response-parsing.service.ts b/src/app/core/data/browse-response-parsing.service.ts index a568cdb6176..e01fa17f1fb 100644 --- a/src/app/core/data/browse-response-parsing.service.ts +++ b/src/app/core/data/browse-response-parsing.service.ts @@ -1,18 +1,17 @@ import { Injectable } from '@angular/core'; -import { ObjectCacheService } from '../cache/object-cache.service'; + import { hasValue } from '../../shared/empty.util'; -import { - HIERARCHICAL_BROWSE_DEFINITION -} from '../shared/hierarchical-browse-definition.resource-type'; -import { FLAT_BROWSE_DEFINITION } from '../shared/flat-browse-definition.resource-type'; -import { HierarchicalBrowseDefinition } from '../shared/hierarchical-browse-definition.model'; -import { FlatBrowseDefinition } from '../shared/flat-browse-definition.model'; -import { DspaceRestResponseParsingService } from './dspace-rest-response-parsing.service'; +import { ObjectCacheService } from '../cache/object-cache.service'; import { Serializer } from '../serializer'; import { BrowseDefinition } from '../shared/browse-definition.model'; import { BROWSE_DEFINITION } from '../shared/browse-definition.resource-type'; +import { FlatBrowseDefinition } from '../shared/flat-browse-definition.model'; +import { FLAT_BROWSE_DEFINITION } from '../shared/flat-browse-definition.resource-type'; +import { HierarchicalBrowseDefinition } from '../shared/hierarchical-browse-definition.model'; +import { HIERARCHICAL_BROWSE_DEFINITION } from '../shared/hierarchical-browse-definition.resource-type'; import { ValueListBrowseDefinition } from '../shared/value-list-browse-definition.model'; import { VALUE_LIST_BROWSE_DEFINITION } from '../shared/value-list-browse-definition.resource-type'; +import { DspaceRestResponseParsingService } from './dspace-rest-response-parsing.service'; /** * A ResponseParsingService used to parse a REST API response to a BrowseDefinition object diff --git a/src/app/core/data/bundle-data.service.spec.ts b/src/app/core/data/bundle-data.service.spec.ts index e3ba438f9bd..b2c8be06af1 100644 --- a/src/app/core/data/bundle-data.service.spec.ts +++ b/src/app/core/data/bundle-data.service.spec.ts @@ -1,19 +1,23 @@ import { HttpClient } from '@angular/common/http'; import { Store } from '@ngrx/store'; -import { compare, Operation } from 'fast-json-patch'; -import { NotificationsService } from '../../shared/notifications/notifications.service'; -import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; -import { Item } from '../shared/item.model'; -import { ChangeAnalyzer } from './change-analyzer'; +import { + compare, + Operation, +} from 'fast-json-patch'; + import { getMockRequestService } from '../../shared/mocks/request.service.mock'; -import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub'; -import { BundleDataService } from './bundle-data.service'; -import { HALLink } from '../shared/hal-link.model'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; +import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub'; import { createPaginatedList } from '../../shared/testing/utils.test'; -import { Bundle } from '../shared/bundle.model'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { CoreState } from '../core-state.model'; +import { Bundle } from '../shared/bundle.model'; +import { HALLink } from '../shared/hal-link.model'; +import { Item } from '../shared/item.model'; import { testPatchDataImplementation } from './base/patch-data.spec'; +import { BundleDataService } from './bundle-data.service'; +import { ChangeAnalyzer } from './change-analyzer'; class DummyChangeAnalyzer implements ChangeAnalyzer { diff(object1: Item, object2: Item): Operation[] { @@ -41,7 +45,7 @@ describe('BundleDataService', () => { bundleHALLink.href = bundleLink; item = new Item(); item._links = { - bundles: bundleHALLink + bundles: bundleHALLink, }; requestService = getMockRequestService(); halService = new HALEndpointServiceStub('url') as any; @@ -56,7 +60,7 @@ describe('BundleDataService', () => { }, getObjectBySelfLink: () => { /* empty */ - } + }, } as any; store = {} as Store; return new BundleDataService( @@ -99,30 +103,30 @@ describe('BundleDataService', () => { metadata: { 'dc.title': [ { - value: 'ORIGINAL' - } - ] - } + value: 'ORIGINAL', + }, + ], + }, }), Object.assign(new Bundle(), { id: 'THUMBNAIL_BUNDLE', metadata: { 'dc.title': [ { - value: 'THUMBNAIL' - } - ] - } + value: 'THUMBNAIL', + }, + ], + }, }), Object.assign(new Bundle(), { id: 'EXTRA_BUNDLE', metadata: { 'dc.title': [ { - value: 'EXTRA' - } - ] - } + value: 'EXTRA', + }, + ], + }, }), ]; spyOn(service, 'findAllByItem').and.returnValue(createSuccessfulRemoteDataObject$(createPaginatedList(bundles))); diff --git a/src/app/core/data/bundle-data.service.ts b/src/app/core/data/bundle-data.service.ts index 19f0e737069..5d552c9bf0f 100644 --- a/src/app/core/data/bundle-data.service.ts +++ b/src/app/core/data/bundle-data.service.ts @@ -1,36 +1,39 @@ import { Injectable } from '@angular/core'; +import { Operation } from 'fast-json-patch'; import { Observable } from 'rxjs'; -import { map, switchMap, take } from 'rxjs/operators'; +import { + map, + switchMap, + take, +} from 'rxjs/operators'; + import { hasValue } from '../../shared/empty.util'; +import { PaginatedSearchOptions } from '../../shared/search/models/paginated-search-options.model'; 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 { 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 { IdentifiableDataService } from './base/identifiable-data.service'; +import { + PatchData, + PatchDataImpl, +} from './base/patch-data'; +import { DSOChangeAnalyzer } from './dso-change-analyzer.service'; +import { FindListOptions } from './find-list-options.model'; import { PaginatedList } from './paginated-list.model'; import { RemoteData } from './remote-data'; import { GetRequest } from './request.models'; import { RequestService } from './request.service'; -import { PaginatedSearchOptions } from '../../shared/search/models/paginated-search-options.model'; -import { Bitstream } from '../shared/bitstream.model'; import { RequestEntryState } from './request-entry-state.model'; -import { FindListOptions } from './find-list-options.model'; -import { IdentifiableDataService } from './base/identifiable-data.service'; -import { PatchData, PatchDataImpl } from './base/patch-data'; -import { DSOChangeAnalyzer } from './dso-change-analyzer.service'; import { RestRequestMethod } from './rest-request-method'; -import { Operation } from 'fast-json-patch'; -import { dataService } from './base/data-service.decorator'; /** * 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'; @@ -91,7 +94,7 @@ export class BundleDataService extends IdentifiableDataService implement RequestEntryState.Success, null, matchingBundle, - 200 + 200, ); } else { return new RemoteData( @@ -101,7 +104,7 @@ export class BundleDataService extends IdentifiableDataService implement RequestEntryState.Error, `The bundle with name ${bundleName} was not found.`, null, - 404 + 404, ); } } else { @@ -119,7 +122,7 @@ export class BundleDataService extends IdentifiableDataService implement getBitstreamsEndpoint(bundleId: string, searchOptions?: PaginatedSearchOptions): Observable { return this.getBrowseEndpoint().pipe( switchMap((href: string) => this.halService.getEndpoint(this.bitstreamsEndpoint, `${href}/${bundleId}`)), - map((href) => searchOptions ? searchOptions.toRestUrl(href) : href) + map((href) => searchOptions ? searchOptions.toRestUrl(href) : href), ); } diff --git a/src/app/core/data/collection-data.service.spec.ts b/src/app/core/data/collection-data.service.spec.ts index 65f8b3ab2cd..f27695c3261 100644 --- a/src/app/core/data/collection-data.service.spec.ts +++ b/src/app/core/data/collection-data.service.spec.ts @@ -1,29 +1,45 @@ -import { CollectionDataService } from './collection-data.service'; -import { RequestService } from './request.service'; +import { + fakeAsync, + tick, +} from '@angular/core/testing'; import { TranslateService } from '@ngx-translate/core'; +import { + cold, + getTestScheduler, + hot, +} from 'jasmine-marbles'; +import { Observable } from 'rxjs'; +import { TestScheduler } from 'rxjs/testing'; + +import { hasNoValue } from '../../shared/empty.util'; import { getMockRequestService } from '../../shared/mocks/request.service.mock'; +import { getMockTranslateService } from '../../shared/mocks/translate.service.mock'; +import { + createFailedRemoteDataObject$, + createSuccessfulRemoteDataObject, + createSuccessfulRemoteDataObject$, +} from '../../shared/remote-data.utils'; import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub'; import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub'; -import { getMockTranslateService } from '../../shared/mocks/translate.service.mock'; -import { fakeAsync, tick } from '@angular/core/testing'; -import { ContentSourceRequest, UpdateContentSourceRequest } from './request.models'; -import { ContentSource } from '../shared/content-source.model'; -import { ObjectCacheService } from '../cache/object-cache.service'; +import { ObjectCacheServiceStub } from '../../shared/testing/object-cache-service.stub'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { ObjectCacheService } from '../cache/object-cache.service'; import { Collection } from '../shared/collection.model'; +import { ContentSource } from '../shared/content-source.model'; import { PageInfo } from '../shared/page-info.model'; -import { buildPaginatedList } from './paginated-list.model'; -import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; -import { cold, getTestScheduler, hot } from 'jasmine-marbles'; -import { TestScheduler } from 'rxjs/testing'; -import { Observable } from 'rxjs'; -import { RemoteData } from './remote-data'; -import { hasNoValue } from '../../shared/empty.util'; import { testCreateDataImplementation } from './base/create-data.spec'; +import { testDeleteDataImplementation } from './base/delete-data.spec'; import { testFindAllDataImplementation } from './base/find-all-data.spec'; -import { testSearchDataImplementation } from './base/search-data.spec'; import { testPatchDataImplementation } from './base/patch-data.spec'; -import { testDeleteDataImplementation } from './base/delete-data.spec'; +import { testSearchDataImplementation } from './base/search-data.spec'; +import { CollectionDataService } from './collection-data.service'; +import { buildPaginatedList } from './paginated-list.model'; +import { RemoteData } from './remote-data'; +import { + ContentSourceRequest, + UpdateContentSourceRequest, +} from './request.models'; +import { RequestService } from './request.service'; const url = 'fake-url'; const collectionId = 'fake-collection-id'; @@ -35,7 +51,7 @@ describe('CollectionDataService', () => { let translate: TranslateService; let notificationsService: any; let rdbService: RemoteDataBuildService; - let objectCache: ObjectCacheService; + let objectCache: ObjectCacheServiceStub; let halService: any; const mockCollection1: Collection = Object.assign(new Collection(), { @@ -43,9 +59,9 @@ describe('CollectionDataService', () => { name: 'test-collection-1', _links: { self: { - href: 'https://rest.api/collections/test-collection-1-1' - } - } + href: 'https://rest.api/collections/test-collection-1-1', + }, + }, }); const mockCollection2: Collection = Object.assign(new Collection(), { @@ -53,9 +69,9 @@ describe('CollectionDataService', () => { name: 'test-collection-2', _links: { self: { - href: 'https://rest.api/collections/test-collection-2-2' - } - } + href: 'https://rest.api/collections/test-collection-2-2', + }, + }, }); const mockCollection3: Collection = Object.assign(new Collection(), { @@ -63,9 +79,9 @@ describe('CollectionDataService', () => { name: 'test-collection-3', _links: { self: { - href: 'https://rest.api/collections/test-collection-3-3' - } - } + href: 'https://rest.api/collections/test-collection-3-3', + }, + }, }); const queryString = 'test-string'; @@ -135,10 +151,10 @@ 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 + a: paginatedListRD, }); expect(result).toBeObservable(expected); }); @@ -150,10 +166,10 @@ 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 + a: paginatedListRD, }); expect(result).toBeObservable(expected); }); @@ -190,29 +206,27 @@ 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', { buildList: hot('a|', { - a: paginatedListRD + a: paginatedListRD, }), buildFromRequestUUID: buildResponse$, - buildSingle: buildResponse$ - }); - objectCache = jasmine.createSpyObj('objectCache', { - remove: jasmine.createSpy('remove') + buildSingle: buildResponse$, }); + objectCache = new ObjectCacheServiceStub(); halService = new HALEndpointServiceStub(url); notificationsService = new NotificationsServiceStub(); translate = getMockTranslateService(); - service = new CollectionDataService(requestService, rdbService, objectCache, halService, null, notificationsService, null, null, translate); + service = new CollectionDataService(requestService, rdbService, objectCache as ObjectCacheService, halService, null, notificationsService, null, null, translate); } }); diff --git a/src/app/core/data/collection-data.service.ts b/src/app/core/data/collection-data.service.ts index 405b35c1f94..b2d5476d21a 100644 --- a/src/app/core/data/collection-data.service.ts +++ b/src/app/core/data/collection-data.service.ts @@ -2,41 +2,51 @@ import { HttpHeaders } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; import { Observable } from 'rxjs'; -import { filter, map, switchMap, take } from 'rxjs/operators'; -import { hasValue, isNotEmpty, isNotEmptyOperator } from '../../shared/empty.util'; -import { NotificationOptions } from '../../shared/notifications/models/notification-options.model'; +import { + filter, + map, + switchMap, + take, +} from 'rxjs/operators'; + +import { + hasValue, + isNotEmpty, + isNotEmptyOperator, +} from '../../shared/empty.util'; import { INotification } from '../../shared/notifications/models/notification.model'; +import { NotificationOptions } from '../../shared/notifications/models/notification-options.model'; import { NotificationsService } from '../../shared/notifications/notifications.service'; 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 { HttpOptions } from '../dspace-rest/dspace-rest.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 { + getAllCompletedRemoteData, + getFirstCompletedRemoteData, +} from '../shared/operators'; +import { BitstreamDataService } from './bitstream-data.service'; import { ComColDataService } from './comcol-data.service'; import { CommunityDataService } from './community-data.service'; import { DSOChangeAnalyzer } from './dso-change-analyzer.service'; +import { FindListOptions } from './find-list-options.model'; import { PaginatedList } from './paginated-list.model'; import { RemoteData } from './remote-data'; import { ContentSourceRequest, - UpdateContentSourceRequest + UpdateContentSourceRequest, } from './request.models'; import { RequestService } from './request.service'; -import { BitstreamDataService } from './bitstream-data.service'; import { RestRequest } from './rest-request.model'; -import { FindListOptions } from './find-list-options.model'; -import { Community } from '../shared/community.model'; -import { dataService } from './base/data-service.decorator'; -@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'; @@ -73,11 +83,12 @@ export class CollectionDataService extends ComColDataService { getAuthorizedCollection(query: string, options: FindListOptions = {}, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]): Observable>> { const searchHref = 'findSubmitAuthorized'; options = Object.assign({}, options, { - searchParams: [new RequestParam('query', query)] + searchParams: [new RequestParam('query', query)], }); return this.searchBy(searchHref, options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow).pipe( - filter((collections: RemoteData>) => !collections.isResponsePending)); + getAllCompletedRemoteData(), + ); } /** @@ -102,12 +113,13 @@ export class CollectionDataService extends ComColDataService { options = Object.assign({}, options, { searchParams: [ new RequestParam('query', query), - new RequestParam('entityType', entityType) - ] + new RequestParam('entityType', entityType), + ], }); return this.searchBy(searchHref, options, true, reRequestOnStale, ...linksToFollow).pipe( - filter((collections: RemoteData>) => !collections.isResponsePending)); + getAllCompletedRemoteData(), + ); } /** @@ -121,17 +133,18 @@ export class CollectionDataService extends ComColDataService { * @return Observable>> * collection list */ - getAuthorizedCollectionByCommunity(communityId: string, query: string, options: FindListOptions = {}, reRequestOnStale = true,): Observable>> { + getAuthorizedCollectionByCommunity(communityId: string, query: string, options: FindListOptions = {}, reRequestOnStale = true): Observable>> { const searchHref = 'findSubmitAuthorizedByCommunity'; options = Object.assign({}, options, { searchParams: [ new RequestParam('uuid', communityId), - new RequestParam('query', query) - ] + new RequestParam('query', query), + ], }); 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 @@ -154,15 +167,16 @@ export class CollectionDataService extends ComColDataService { const searchHref = 'findSubmitAuthorizedByCommunityAndEntityType'; const searchParams = [ new RequestParam('uuid', communityId), - new RequestParam('entityType', entityType) + new RequestParam('entityType', entityType), ]; options = Object.assign({}, options, { - searchParams: searchParams + searchParams: searchParams, }); return this.searchBy(searchHref, options, true, reRequestOnStale, ...linksToFollow).pipe( - filter((collections: RemoteData>) => !collections.isResponsePending)); + getAllCompletedRemoteData(), + ); } /** @@ -177,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), ); } @@ -189,7 +202,7 @@ export class CollectionDataService extends ComColDataService { */ getHarvesterEndpoint(collectionId: string): Observable { return this.halService.getEndpoint(this.linkPath).pipe( - switchMap((href: string) => this.halService.getEndpoint('harvester', `${href}/${collectionId}`)) + switchMap((href: string) => this.halService.getEndpoint('harvester', `${href}/${collectionId}`)), ); } @@ -200,7 +213,7 @@ export class CollectionDataService extends ComColDataService { getContentSource(collectionId: string, useCachedVersionIfAvailable = true): Observable> { const href$ = this.getHarvesterEndpoint(collectionId).pipe( isNotEmptyOperator(), - take(1) + take(1), ); href$.subscribe((href: string) => { @@ -227,7 +240,7 @@ export class CollectionDataService extends ComColDataService { headers = headers.append('Content-Type', 'application/json'); options.headers = headers; return new UpdateContentSourceRequest(requestId, href, JSON.stringify(serializedContentSource), options); - }) + }), ); // Execute the post/put request @@ -255,7 +268,7 @@ export class CollectionDataService extends ComColDataService { return (response as RemoteData).payload; } return response as INotification; - }) + }), ); } diff --git a/src/app/core/data/comcol-data.service.spec.ts b/src/app/core/data/comcol-data.service.spec.ts index 0f9f0fa7402..e1fe48c0762 100644 --- a/src/app/core/data/comcol-data.service.spec.ts +++ b/src/app/core/data/comcol-data.service.spec.ts @@ -1,28 +1,37 @@ import { HttpClient } from '@angular/common/http'; import { Store } from '@ngrx/store'; import { cold } from 'jasmine-marbles'; -import { Observable, of as observableOf } from 'rxjs'; +import { + Observable, + of as observableOf, +} from 'rxjs'; import { TestScheduler } from 'rxjs/testing'; + import { getMockRequestService } from '../../shared/mocks/request.service.mock'; import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { + createFailedRemoteDataObject, + createFailedRemoteDataObject$, + createSuccessfulRemoteDataObject, + createSuccessfulRemoteDataObject$, +} from '../../shared/remote-data.utils'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { ObjectCacheService } from '../cache/object-cache.service'; +import { CoreState } from '../core-state.model'; +import { Bitstream } from '../shared/bitstream.model'; import { Community } from '../shared/community.model'; import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { testCreateDataImplementation } from './base/create-data.spec'; +import { testDeleteDataImplementation } from './base/delete-data.spec'; +import { testFindAllDataImplementation } from './base/find-all-data.spec'; +import { testPatchDataImplementation } from './base/patch-data.spec'; +import { testSearchDataImplementation } from './base/search-data.spec'; +import { BitstreamDataService } from './bitstream-data.service'; import { ComColDataService } from './comcol-data.service'; import { CommunityDataService } from './community-data.service'; import { DSOChangeAnalyzer } from './dso-change-analyzer.service'; -import { RequestService } from './request.service'; -import { createFailedRemoteDataObject, createFailedRemoteDataObject$, createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; -import { BitstreamDataService } from './bitstream-data.service'; -import { CoreState } from '../core-state.model'; import { FindListOptions } from './find-list-options.model'; -import { Bitstream } from '../shared/bitstream.model'; -import { testCreateDataImplementation } from './base/create-data.spec'; -import { testFindAllDataImplementation } from './base/find-all-data.spec'; -import { testSearchDataImplementation } from './base/search-data.spec'; -import { testPatchDataImplementation } from './base/patch-data.spec'; -import { testDeleteDataImplementation } from './base/delete-data.spec'; +import { RequestService } from './request.service'; const LINK_NAME = 'test'; @@ -45,7 +54,7 @@ class TestService extends ComColDataService { protected http: HttpClient, protected bitstreamDataService: BitstreamDataService, protected comparator: DSOChangeAnalyzer, - protected linkPath: string + protected linkPath: string, ) { super('something', requestService, rdbService, objectCache, halService, comparator, notificationsService, bitstreamDataService); } @@ -79,30 +88,30 @@ describe('ComColDataService', () => { const comparator = {} as any; const options = Object.assign(new FindListOptions(), { - scopeID: scopeID + scopeID: scopeID, }); const scopedEndpoint = `${communityEndpoint}/${LINK_NAME}`; const mockHalService = { - getEndpoint: (linkPath) => observableOf(communitiesEndpoint) + getEndpoint: (linkPath) => observableOf(communitiesEndpoint), }; function initRdbService(): RemoteDataBuildService { return jasmine.createSpyObj('rdbService', { - buildSingle : createFailedRemoteDataObject$('Error', 500) + buildSingle : createFailedRemoteDataObject$('Error', 500), }); } function initBitstreamDataService(): BitstreamDataService { return jasmine.createSpyObj('bitstreamDataService', { - deleteByHref: createSuccessfulRemoteDataObject$({}) + deleteByHref: createSuccessfulRemoteDataObject$({}), }); } function initMockCommunityDataService(): CommunityDataService { return jasmine.createSpyObj('cds', { getEndpoint: cold('--a-', { a: communitiesEndpoint }), - getIDHref: communityEndpoint + getIDHref: communityEndpoint, }); } @@ -112,11 +121,11 @@ describe('ComColDataService', () => { d: { _links: { [LINK_NAME]: { - href: scopedEndpoint - } - } - } - }) + href: scopedEndpoint, + }, + }, + }, + }), }); } @@ -132,7 +141,7 @@ describe('ComColDataService', () => { http, bitstreamDataService, comparator, - LINK_NAME + LINK_NAME, ); } @@ -200,12 +209,12 @@ describe('ComColDataService', () => { communityWithParentHref = { _links: { parentCommunity: { - href: 'topLevel/parentCommunity' - } - } + href: 'topLevel/parentCommunity', + }, + }, } as Community; communityWithoutParentHref = { - _links: {} + _links: {}, } as Community; }); @@ -238,9 +247,9 @@ describe('ComColDataService', () => { id: 'a20da287-e174-466a-9926-f66as300d399', metadata: [{ key: 'dc.title', - value: 'parent community' + value: 'parent community', }], - _links: {} + _links: {}, }); }); it('should refresh a specific cached community when the parent link can be resolved', () => { @@ -262,9 +271,9 @@ describe('ComColDataService', () => { dso = { _links: { logo: { - href: 'logo-href' - } - } + href: 'logo-href', + }, + }, }; }); @@ -291,8 +300,8 @@ describe('ComColDataService', () => { _links: { self: { href: 'logo-href', - } - } + }, + }, }); }); diff --git a/src/app/core/data/comcol-data.service.ts b/src/app/core/data/comcol-data.service.ts index abc9046cd0e..de0d1a31570 100644 --- a/src/app/core/data/comcol-data.service.ts +++ b/src/app/core/data/comcol-data.service.ts @@ -1,34 +1,63 @@ -import { distinctUntilChanged, filter, map, switchMap, take } from 'rxjs/operators'; -import { combineLatest as observableCombineLatest, Observable } from 'rxjs'; -import { hasValue, isEmpty, isNotEmpty } from '../../shared/empty.util'; +import { Operation } from 'fast-json-patch'; +import { + combineLatest as observableCombineLatest, + Observable, +} from 'rxjs'; +import { + distinctUntilChanged, + filter, + map, + switchMap, + take, +} from 'rxjs/operators'; + +import { + hasValue, + isEmpty, + isNotEmpty, +} from '../../shared/empty.util'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { createFailedRemoteDataObject$ } from '../../shared/remote-data.utils'; +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 { Community } from '../shared/community.model'; -import { HALLink } from '../shared/hal-link.model'; -import { PaginatedList } from './paginated-list.model'; -import { RemoteData } from './remote-data'; -import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { getFirstCompletedRemoteData } from '../shared/operators'; import { Bitstream } from '../shared/bitstream.model'; import { Collection } from '../shared/collection.model'; -import { BitstreamDataService } from './bitstream-data.service'; +import { Community } from '../shared/community.model'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { HALLink } from '../shared/hal-link.model'; import { NoContent } from '../shared/NoContent.model'; -import { createFailedRemoteDataObject$ } from '../../shared/remote-data.utils'; +import { getFirstCompletedRemoteData } from '../shared/operators'; import { URLCombiner } from '../url-combiner/url-combiner'; -import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; -import { FindListOptions } from './find-list-options.model'; +import { + CreateData, + CreateDataImpl, +} from './base/create-data'; +import { + DeleteData, + DeleteDataImpl, +} from './base/delete-data'; +import { + FindAllData, + FindAllDataImpl, +} from './base/find-all-data'; import { IdentifiableDataService } from './base/identifiable-data.service'; -import { PatchData, PatchDataImpl } from './base/patch-data'; -import { DeleteData, DeleteDataImpl } from './base/delete-data'; -import { FindAllData, FindAllDataImpl } from './base/find-all-data'; -import { SearchData, SearchDataImpl } from './base/search-data'; -import { RestRequestMethod } from './rest-request-method'; -import { CreateData, CreateDataImpl } from './base/create-data'; -import { RequestParam } from '../cache/models/request-param.model'; -import { RequestService } from './request.service'; -import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; -import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { + PatchData, + PatchDataImpl, +} from './base/patch-data'; +import { + SearchData, + SearchDataImpl, +} from './base/search-data'; +import { BitstreamDataService } from './bitstream-data.service'; import { DSOChangeAnalyzer } from './dso-change-analyzer.service'; -import { Operation } from 'fast-json-patch'; +import { FindListOptions } from './find-list-options.model'; +import { PaginatedList } from './paginated-list.model'; +import { RemoteData } from './remote-data'; +import { RequestService } from './request.service'; +import { RestRequestMethod } from './rest-request-method'; export abstract class ComColDataService extends IdentifiableDataService implements CreateData, FindAllData, SearchData, PatchData, DeleteData { private createData: CreateData; @@ -86,7 +115,7 @@ export abstract class ComColDataService extend }), filter((halLink: HALLink) => isNotEmpty(halLink)), map((halLink: HALLink) => halLink.href), - distinctUntilChanged() + distinctUntilChanged(), ); } } @@ -97,7 +126,7 @@ export abstract class ComColDataService extend public findByParent(parentUUID: string, options: FindListOptions = {}, ...linksToFollow: FollowLinkConfig[]): Observable>> { const href$ = this.getFindByParentHref(parentUUID).pipe( - map((href: string) => this.buildHrefFromFindOptions(href, options)) + map((href: string) => this.buildHrefFromFindOptions(href, options)), ); return this.findListByHref(href$, options, true, true, ...linksToFollow); } @@ -110,7 +139,7 @@ export abstract class ComColDataService extend return this.halService.getEndpoint(this.linkPath).pipe( // We can't use HalLinkService to discover the logo link itself, as objects without a logo // don't have the link, and this method is also used in the createLogo method. - map((href: string) => new URLCombiner(href, id, 'logo').toString()) + map((href: string) => new URLCombiner(href, id, 'logo').toString()), ); } @@ -132,7 +161,7 @@ export abstract class ComColDataService extend } else { return this.bitstreamDataService.deleteByHref(logoRd.payload._links.self.href); } - }) + }), ); } else { return createFailedRemoteDataObject$(`The given object doesn't have a logo`, 400); @@ -148,7 +177,7 @@ export abstract class ComColDataService extend this.findByHref(parentCommunityUrl).pipe( getFirstCompletedRemoteData(), ), - this.halService.getEndpoint('communities/search/top').pipe(take(1)) + this.halService.getEndpoint('communities/search/top').pipe(take(1)), ]).subscribe(([rd, topHref]: [RemoteData, string]) => { if (rd.hasSucceeded && isNotEmpty(rd.payload) && isNotEmpty(rd.payload.id)) { this.requestService.setStaleByHrefSubstring(rd.payload.id); diff --git a/src/app/core/data/community-data.service.ts b/src/app/core/data/community-data.service.ts index efb6d50e848..79dedf0c842 100644 --- a/src/app/core/data/community-data.service.ts +++ b/src/app/core/data/community-data.service.ts @@ -1,26 +1,28 @@ import { Injectable } from '@angular/core'; - import { Observable } from 'rxjs'; -import { filter, map, switchMap, take } from 'rxjs/operators'; +import { + filter, + map, + switchMap, + take, +} from 'rxjs/operators'; + +import { isNotEmpty } from '../../shared/empty.util'; import { NotificationsService } from '../../shared/notifications/notifications.service'; +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 { BitstreamDataService } from './bitstream-data.service'; import { ComColDataService } from './comcol-data.service'; import { DSOChangeAnalyzer } from './dso-change-analyzer.service'; +import { FindListOptions } from './find-list-options.model'; import { PaginatedList } from './paginated-list.model'; import { RemoteData } from './remote-data'; import { RequestService } from './request.service'; -import { BitstreamDataService } from './bitstream-data.service'; -import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; -import { isNotEmpty } from '../../shared/empty.util'; -import { FindListOptions } from './find-list-options.model'; -import { dataService } from './base/data-service.decorator'; -@Injectable() -@dataService(COMMUNITY) +@Injectable({ providedIn: 'root' }) export class CommunityDataService extends ComColDataService { protected topLinkPath = 'search/top'; @@ -44,14 +46,14 @@ export class CommunityDataService extends ComColDataService { findTop(options: FindListOptions = {}, ...linksToFollow: FollowLinkConfig[]): Observable>> { return this.getEndpoint().pipe( map(href => `${href}/search/top`), - switchMap(href => this.findListByHref(href, options, true, true, ...linksToFollow)) + switchMap(href => this.findListByHref(href, options, true, true, ...linksToFollow)), ); } protected getFindByParentHref(parentUUID: string): Observable { return this.halService.getEndpoint(this.linkPath).pipe( switchMap((communityEndpointHref: string) => - this.halService.getEndpoint('subcommunities', `${communityEndpointHref}/${parentUUID}`)) + this.halService.getEndpoint('subcommunities', `${communityEndpointHref}/${parentUUID}`)), ); } @@ -59,7 +61,7 @@ export class CommunityDataService extends ComColDataService { return this.getEndpoint().pipe( map((endpoint: string) => this.getIDHref(endpoint, options.scopeID)), filter((href: string) => isNotEmpty(href)), - take(1) + take(1), ); } } diff --git a/src/app/core/data/configuration-data.service.spec.ts b/src/app/core/data/configuration-data.service.spec.ts index 7fe69c16e5c..bccfe45da48 100644 --- a/src/app/core/data/configuration-data.service.spec.ts +++ b/src/app/core/data/configuration-data.service.spec.ts @@ -1,12 +1,16 @@ -import { cold, getTestScheduler } from 'jasmine-marbles'; +import { + cold, + getTestScheduler, +} from 'jasmine-marbles'; import { TestScheduler } from 'rxjs/testing'; + import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { ObjectCacheService } from '../cache/object-cache.service'; +import { ConfigurationProperty } from '../shared/configuration-property.model'; import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { ConfigurationDataService } from './configuration-data.service'; import { GetRequest } from './request.models'; import { RequestService } from './request.service'; -import { ObjectCacheService } from '../cache/object-cache.service'; -import { ConfigurationDataService } from './configuration-data.service'; -import { ConfigurationProperty } from '../shared/configuration-property.model'; describe('ConfigurationDataService', () => { let scheduler: TestScheduler; @@ -18,7 +22,7 @@ describe('ConfigurationDataService', () => { const testObject = { uuid: 'test-property', name: 'test-property', - values: ['value-1', 'value-2'] + values: ['value-1', 'value-2'], } as ConfigurationProperty; const configLink = 'https://rest.api/rest/api/config/properties'; const requestURL = `https://rest.api/rest/api/config/properties/${testObject.name}`; @@ -28,18 +32,18 @@ describe('ConfigurationDataService', () => { scheduler = getTestScheduler(); halService = jasmine.createSpyObj('halService', { - getEndpoint: cold('a', { a: configLink }) + getEndpoint: cold('a', { a: configLink }), }); requestService = jasmine.createSpyObj('requestService', { generateRequestId: requestUUID, - send: true + send: true, }); rdbService = jasmine.createSpyObj('rdbService', { buildSingle: cold('a', { a: { - payload: testObject - } - }) + payload: testObject, + }, + }), }); objectCache = {} as ObjectCacheService; @@ -70,8 +74,8 @@ describe('ConfigurationDataService', () => { const result = service.findByPropertyName(testObject.name); const expected = cold('a', { a: { - payload: testObject - } + payload: testObject, + }, }); expect(result).toBeObservable(expected); }); diff --git a/src/app/core/data/configuration-data.service.ts b/src/app/core/data/configuration-data.service.ts index de044e25e33..bb1bd19ff14 100644 --- a/src/app/core/data/configuration-data.service.ts +++ b/src/app/core/data/configuration-data.service.ts @@ -1,18 +1,15 @@ -/* eslint-disable max-classes-per-file */ import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; + import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { ObjectCacheService } from '../cache/object-cache.service'; +import { ConfigurationProperty } from '../shared/configuration-property.model'; import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { IdentifiableDataService } from './base/identifiable-data.service'; import { RemoteData } from './remote-data'; import { RequestService } from './request.service'; -import { ConfigurationProperty } from '../shared/configuration-property.model'; -import { CONFIG_PROPERTY } from '../shared/config-property.resource-type'; -import { IdentifiableDataService } from './base/identifiable-data.service'; -import { dataService } from './base/data-service.decorator'; -@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 066ccf28c9d..4c0fd789fbf 100644 --- a/src/app/core/data/content-source-response-parsing.service.ts +++ b/src/app/core/data/content-source-response-parsing.service.ts @@ -1,13 +1,14 @@ import { Injectable } from '@angular/core'; + import { ParsedResponse } from '../cache/response.models'; -import { RawRestResponse } from '../dspace-rest/raw-rest-response.model'; import { DSpaceSerializer } from '../dspace-rest/dspace.serializer'; +import { RawRestResponse } from '../dspace-rest/raw-rest-response.model'; import { ContentSource } from '../shared/content-source.model'; 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 992a29e4b84..d6aeca7965b 100644 --- a/src/app/core/data/debug-response-parsing.service.ts +++ b/src/app/core/data/debug-response-parsing.service.ts @@ -1,10 +1,11 @@ import { Injectable } from '@angular/core'; + import { RestResponse } from '../cache/response.models'; 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 70c45bbc2de..fa08af018a2 100644 --- a/src/app/core/data/default-change-analyzer.service.ts +++ b/src/app/core/data/default-change-analyzer.service.ts @@ -1,16 +1,19 @@ import { Injectable } from '@angular/core'; -import { compare } from 'fast-json-patch'; -import { Operation } from 'fast-json-patch'; +import { + compare, + Operation, +} from 'fast-json-patch'; + import { getClassForType } from '../cache/builders/build-decorators'; +import { TypedObject } from '../cache/typed-object.model'; import { DSpaceNotNullSerializer } from '../dspace-rest/dspace-not-null.serializer'; import { ChangeAnalyzer } from './change-analyzer'; -import { TypedObject } from '../cache/typed-object.model'; /** * 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 a621895633b..95e7b5d69f4 100644 --- a/src/app/core/data/dso-change-analyzer.service.ts +++ b/src/app/core/data/dso-change-analyzer.service.ts @@ -1,15 +1,19 @@ -import { compare, Operation } from 'fast-json-patch'; -import { ChangeAnalyzer } from './change-analyzer'; import { Injectable } from '@angular/core'; +import { + compare, + Operation, +} from 'fast-json-patch'; +import cloneDeep from 'lodash/cloneDeep'; + import { DSpaceObject } from '../shared/dspace-object.model'; import { MetadataMap } from '../shared/metadata.models'; -import cloneDeep from 'lodash/cloneDeep'; +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.spec.ts b/src/app/core/data/dso-redirect.service.spec.ts index 2122dc663a1..b6b72583ea6 100644 --- a/src/app/core/data/dso-redirect.service.spec.ts +++ b/src/app/core/data/dso-redirect.service.spec.ts @@ -1,16 +1,25 @@ -import { cold, getTestScheduler } from 'jasmine-marbles'; +import { + cold, + getTestScheduler, +} from 'jasmine-marbles'; import { TestScheduler } from 'rxjs/testing'; + +import { AppConfig } from '../../../config/app-config.interface'; +import { environment } from '../../../environments/environment.test'; +import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils'; 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 { HardRedirectService } from '../services/hard-redirect.service'; import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { DsoRedirectService } from './dso-redirect.service'; -import { GetRequest, IdentifierType } from './request.models'; -import { RequestService } from './request.service'; -import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils'; import { Item } from '../shared/item.model'; import { EMBED_SEPARATOR } from './base/base-data.service'; -import { HardRedirectService } from '../services/hard-redirect.service'; +import { DsoRedirectService } from './dso-redirect.service'; +import { + GetRequest, + IdentifierType, +} from './request.models'; +import { RequestService } from './request.service'; describe('DsoRedirectService', () => { let scheduler: TestScheduler; @@ -33,34 +42,35 @@ describe('DsoRedirectService', () => { scheduler = getTestScheduler(); halService = jasmine.createSpyObj('halService', { - getEndpoint: cold('a', { a: pidLink }) + getEndpoint: cold('a', { a: pidLink }), }); requestService = jasmine.createSpyObj('requestService', { generateRequestId: requestUUID, - send: true + send: true, }); remoteData = createSuccessfulRemoteDataObject(Object.assign(new Item(), { type: 'item', - uuid: '123456789' + uuid: '123456789', })); rdbService = jasmine.createSpyObj('rdbService', { buildSingle: cold('a', { - a: remoteData - }) + a: remoteData, + }), }); redirectService = jasmine.createSpyObj('redirectService', { - redirect: {} + redirect: {}, }); service = new DsoRedirectService( + environment as AppConfig, requestService, rdbService, objectCache, halService, - redirectService + redirectService, ); }); @@ -107,7 +117,7 @@ describe('DsoRedirectService', () => { redir.subscribe(); scheduler.schedule(() => redir); scheduler.flush(); - expect(redirectService.redirect).toHaveBeenCalledWith('/items/' + remoteData.payload.uuid, 301); + expect(redirectService.redirect).toHaveBeenCalledWith(`${environment.ui.nameSpace}/items/${remoteData.payload.uuid}`, 301); }); it('should navigate to entities route with the corresponding entity type', () => { remoteData.payload.type = 'item'; @@ -115,8 +125,8 @@ describe('DsoRedirectService', () => { 'dspace.entity.type': [ { language: 'en_US', - value: 'Publication' - } + value: 'Publication', + }, ], }; const redir = service.findByIdAndIDType(dsoHandle, IdentifierType.HANDLE); @@ -124,7 +134,7 @@ describe('DsoRedirectService', () => { redir.subscribe(); scheduler.schedule(() => redir); scheduler.flush(); - expect(redirectService.redirect).toHaveBeenCalledWith('/entities/publication/' + remoteData.payload.uuid, 301); + expect(redirectService.redirect).toHaveBeenCalledWith(`${environment.ui.nameSpace}/entities/publication/${remoteData.payload.uuid}`, 301); }); it('should navigate to collections route', () => { @@ -133,7 +143,7 @@ describe('DsoRedirectService', () => { redir.subscribe(); scheduler.schedule(() => redir); scheduler.flush(); - expect(redirectService.redirect).toHaveBeenCalledWith('/collections/' + remoteData.payload.uuid, 301); + expect(redirectService.redirect).toHaveBeenCalledWith(`${environment.ui.nameSpace}/collections/${remoteData.payload.uuid}`, 301); }); it('should navigate to communities route', () => { @@ -142,7 +152,7 @@ describe('DsoRedirectService', () => { redir.subscribe(); scheduler.schedule(() => redir); scheduler.flush(); - expect(redirectService.redirect).toHaveBeenCalledWith('/communities/' + remoteData.payload.uuid, 301); + expect(redirectService.redirect).toHaveBeenCalledWith(`${environment.ui.nameSpace}/communities/${remoteData.payload.uuid}`, 301); }); }); diff --git a/src/app/core/data/dso-redirect.service.ts b/src/app/core/data/dso-redirect.service.ts index a27d1fb11f3..28628a62464 100644 --- a/src/app/core/data/dso-redirect.service.ts +++ b/src/app/core/data/dso-redirect.service.ts @@ -6,21 +6,29 @@ * http://www.dspace.org/license/ */ /* eslint-disable max-classes-per-file */ -import { Injectable } from '@angular/core'; +import { + Inject, + Injectable, +} from '@angular/core'; import { Observable } from 'rxjs'; import { tap } from 'rxjs/operators'; + +import { + APP_CONFIG, + AppConfig, +} from '../../../config/app-config.interface'; +import { getDSORoute } from '../../app-routing-paths'; import { hasValue } from '../../shared/empty.util'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { ObjectCacheService } from '../cache/object-cache.service'; +import { HardRedirectService } from '../services/hard-redirect.service'; +import { DSpaceObject } from '../shared/dspace-object.model'; import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { getFirstCompletedRemoteData } from '../shared/operators'; +import { IdentifiableDataService } from './base/identifiable-data.service'; import { RemoteData } from './remote-data'; import { IdentifierType } from './request.models'; import { RequestService } from './request.service'; -import { getFirstCompletedRemoteData } from '../shared/operators'; -import { DSpaceObject } from '../shared/dspace-object.model'; -import { IdentifiableDataService } from './base/identifiable-data.service'; -import { getDSORoute } from '../../app-routing-paths'; -import { HardRedirectService } from '../services/hard-redirect.service'; const ID_ENDPOINT = 'pid'; const UUID_ENDPOINT = 'dso'; @@ -42,7 +50,7 @@ class DsoByIdOrUUIDDataService extends IdentifiableDataService { // interpolate id/uuid as query parameter (endpoint: string, resourceID: string): string => { return endpoint.replace(/{\?id}/, `?id=${resourceID}`) - .replace(/{\?uuid}/, `?uuid=${resourceID}`); + .replace(/{\?uuid}/, `?uuid=${resourceID}`); }, ); } @@ -65,16 +73,17 @@ 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; constructor( + @Inject(APP_CONFIG) protected appConfig: AppConfig, protected requestService: RequestService, protected rdbService: RemoteDataBuildService, protected objectCache: ObjectCacheService, protected halService: HALEndpointService, - private hardRedirectService: HardRedirectService + private hardRedirectService: HardRedirectService, ) { this.dataService = new DsoByIdOrUUIDDataService(requestService, rdbService, objectCache, halService); } @@ -82,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) @@ -95,14 +104,14 @@ export class DsoRedirectService { if (response.hasSucceeded) { const dso = response.payload; if (hasValue(dso.uuid)) { - let newRoute = getDSORoute(dso); + const newRoute = getDSORoute(dso); if (hasValue(newRoute)) { // Use a "301 Moved Permanently" redirect for SEO purposes - this.hardRedirectService.redirect(newRoute, 301); + this.hardRedirectService.redirect(this.appConfig.ui.nameSpace.replace(/\/$/, '') + newRoute, 301); } } } - }) + }), ); } } diff --git a/src/app/core/data/dso-response-parsing.service.ts b/src/app/core/data/dso-response-parsing.service.ts index 74117e79d35..5cabba29eb2 100644 --- a/src/app/core/data/dso-response-parsing.service.ts +++ b/src/app/core/data/dso-response-parsing.service.ts @@ -1,20 +1,25 @@ import { Injectable } from '@angular/core'; +import { + hasNoValue, + hasValue, +} from '../../shared/empty.util'; import { ObjectCacheService } from '../cache/object-cache.service'; +import { + DSOSuccessResponse, + RestResponse, +} from '../cache/response.models'; import { RawRestResponse } from '../dspace-rest/raw-rest-response.model'; -import { RestResponse, DSOSuccessResponse } from '../cache/response.models'; - -import { ResponseParsingService } from './parsing.service'; -import { BaseResponseParsingService } from './base-response-parsing.service'; -import { hasNoValue, hasValue } from '../../shared/empty.util'; import { DSpaceObject } from '../shared/dspace-object.model'; +import { BaseResponseParsingService } from './base-response-parsing.service'; +import { ResponseParsingService } from './parsing.service'; 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.spec.ts b/src/app/core/data/dspace-object-data.service.spec.ts index 0f167ea47e2..1e4809ac4bc 100644 --- a/src/app/core/data/dspace-object-data.service.spec.ts +++ b/src/app/core/data/dspace-object-data.service.spec.ts @@ -1,12 +1,16 @@ -import { cold, getTestScheduler } from 'jasmine-marbles'; +import { + cold, + getTestScheduler, +} from 'jasmine-marbles'; import { TestScheduler } from 'rxjs/testing'; + import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { ObjectCacheService } from '../cache/object-cache.service'; import { DSpaceObject } from '../shared/dspace-object.model'; import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { DSpaceObjectDataService } from './dspace-object-data.service'; import { GetRequest } from './request.models'; import { RequestService } from './request.service'; -import { DSpaceObjectDataService } from './dspace-object-data.service'; -import { ObjectCacheService } from '../cache/object-cache.service'; describe('DSpaceObjectDataService', () => { let scheduler: TestScheduler; @@ -16,7 +20,7 @@ describe('DSpaceObjectDataService', () => { let rdbService: RemoteDataBuildService; let objectCache: ObjectCacheService; const testObject = { - uuid: '9b4f22f4-164a-49db-8817-3316b6ee5746' + uuid: '9b4f22f4-164a-49db-8817-3316b6ee5746', } as DSpaceObject; const dsoLink = 'https://rest.api/rest/api/dso/find{?uuid}'; const requestURL = `https://rest.api/rest/api/dso/find?uuid=${testObject.uuid}`; @@ -26,18 +30,18 @@ describe('DSpaceObjectDataService', () => { scheduler = getTestScheduler(); halService = jasmine.createSpyObj('halService', { - getEndpoint: cold('a', { a: dsoLink }) + getEndpoint: cold('a', { a: dsoLink }), }); requestService = jasmine.createSpyObj('requestService', { generateRequestId: requestUUID, - send: true + send: true, }); rdbService = jasmine.createSpyObj('rdbService', { buildSingle: cold('a', { a: { - payload: testObject - } - }) + payload: testObject, + }, + }), }); objectCache = {} as ObjectCacheService; @@ -68,8 +72,8 @@ describe('DSpaceObjectDataService', () => { const result = service.findById(testObject.uuid); const expected = cold('a', { a: { - payload: testObject - } + payload: testObject, + }, }); expect(result).toBeObservable(expected); }); diff --git a/src/app/core/data/dspace-object-data.service.ts b/src/app/core/data/dspace-object-data.service.ts index 2ad024133c7..bdebbd0582c 100644 --- a/src/app/core/data/dspace-object-data.service.ts +++ b/src/app/core/data/dspace-object-data.service.ts @@ -1,15 +1,13 @@ 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 { RequestService } from './request.service'; import { IdentifiableDataService } from './base/identifiable-data.service'; -import { dataService } from './base/data-service.decorator'; +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 500afc4aff6..1cd286427f3 100644 --- a/src/app/core/data/dspace-rest-response-parsing.service.ts +++ b/src/app/core/data/dspace-rest-response-parsing.service.ts @@ -1,23 +1,34 @@ /* eslint-disable max-classes-per-file */ -import { hasNoValue, hasValue, isNotEmpty } from '../../shared/empty.util'; -import { DSpaceSerializer } from '../dspace-rest/dspace.serializer'; -import { Serializer } from '../serializer'; -import { PageInfo } from '../shared/page-info.model'; -import { ObjectCacheService } from '../cache/object-cache.service'; -import { GenericConstructor } from '../shared/generic-constructor'; -import { PaginatedList, buildPaginatedList } from './paginated-list.model'; -import { getClassForType } from '../cache/builders/build-decorators'; +import { Injectable } from '@angular/core'; + import { environment } from '../../../environments/environment'; +import { + hasNoValue, + hasValue, + isNotEmpty, +} from '../../shared/empty.util'; +import { getClassForType } from '../cache/builders/build-decorators'; +import { CacheableObject } from '../cache/cacheable-object.model'; +import { ObjectCacheService } from '../cache/object-cache.service'; +import { ParsedResponse } from '../cache/response.models'; +import { DSpaceSerializer } from '../dspace-rest/dspace.serializer'; import { RawRestResponse } from '../dspace-rest/raw-rest-response.model'; +import { + getEmbedSizeParams, + getUrlWithoutEmbedParams, +} from '../index/index.selectors'; +import { Serializer } from '../serializer'; import { DSpaceObject } from '../shared/dspace-object.model'; -import { Injectable } from '@angular/core'; -import { ResponseParsingService } from './parsing.service'; -import { ParsedResponse } from '../cache/response.models'; -import { RestRequestMethod } from './rest-request-method'; -import { getUrlWithoutEmbedParams, getEmbedSizeParams } from '../index/index.selectors'; +import { GenericConstructor } from '../shared/generic-constructor'; +import { PageInfo } from '../shared/page-info.model'; import { URLCombiner } from '../url-combiner/url-combiner'; -import { CacheableObject } from '../cache/cacheable-object.model'; +import { + buildPaginatedList, + PaginatedList, +} from './paginated-list.model'; +import { ResponseParsingService } from './parsing.service'; import { RestRequest } from './rest-request.model'; +import { RestRequestMethod } from './rest-request-method'; /** @@ -109,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); }); } @@ -144,8 +162,8 @@ export class DspaceRestResponseParsingService implements ResponseParsingService console.warn(`The response for '${request.href}' doesn't have a self link. This could mean there's an issue with the REST endpoint`); response.payload._links = Object.assign({}, response.payload._links, { self: { - href: urlWithoutEmbedParams - } + href: urlWithoutEmbedParams, + }, }); } else { @@ -155,8 +173,8 @@ export class DspaceRestResponseParsingService implements ResponseParsingService console.warn(`The response for '${urlWithoutEmbedParams}' has the self link '${response.payload._links.self.href}'. These don't match. This could mean there's an issue with the REST endpoint`); response.payload._links = Object.assign({}, response.payload._links, { self: { - href: urlWithoutEmbedParams - } + href: urlWithoutEmbedParams, + }, }); } } @@ -184,8 +202,8 @@ export class DspaceRestResponseParsingService implements ResponseParsingService protected processArray(data: any, request: RestRequest): ObjectDomain[] { let array: ObjectDomain[] = []; data.forEach((datum) => { - array = [...array, this.process(datum, request)]; - } + array = [...array, this.process(datum, request)]; + }, ); return array; } @@ -226,12 +244,12 @@ 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)) { dataJSON = JSON.stringify(Object.assign({}, data, { - _embedded: '...' + _embedded: '...', })); } else { dataJSON = JSON.stringify(data); @@ -240,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 728714876c4..c7dd40b98bd 100644 --- a/src/app/core/data/endpoint-map-response-parsing.service.ts +++ b/src/app/core/data/endpoint-map-response-parsing.service.ts @@ -1,17 +1,17 @@ import { Injectable } from '@angular/core'; -import { - DspaceRestResponseParsingService, - isCacheableObject -} from './dspace-rest-response-parsing.service'; +import { environment } from '../../../environments/environment'; import { hasValue } from '../../shared/empty.util'; import { getClassForType } from '../cache/builders/build-decorators'; -import { GenericConstructor } from '../shared/generic-constructor'; -import { RawRestResponse } from '../dspace-rest/raw-rest-response.model'; +import { CacheableObject } from '../cache/cacheable-object.model'; import { ParsedResponse } from '../cache/response.models'; +import { RawRestResponse } from '../dspace-rest/raw-rest-response.model'; import { DSpaceObject } from '../shared/dspace-object.model'; -import { environment } from '../../../environments/environment'; -import { CacheableObject } from '../cache/cacheable-object.model'; +import { GenericConstructor } from '../shared/generic-constructor'; +import { + DspaceRestResponseParsingService, + isCacheableObject, +} from './dspace-rest-response-parsing.service'; import { RestRequest } from './rest-request.model'; /** @@ -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 { /** @@ -56,7 +56,7 @@ export class EndpointMapResponseParsingService extends DspaceRestResponseParsing } catch (e) { console.warn(`Couldn't parse endpoint request at ${request.href}`); return new ParsedResponse(response.statusCode, undefined, { - _links: response.payload._links + _links: response.payload._links, }); } } @@ -101,7 +101,7 @@ export class EndpointMapResponseParsingService extends DspaceRestResponseParsing let dataJSON: string; if (hasValue(data._embedded)) { dataJSON = JSON.stringify(Object.assign({}, data, { - _embedded: '...' + _embedded: '...', })); } else { dataJSON = JSON.stringify(data); diff --git a/src/app/core/data/entity-type-data.service.ts b/src/app/core/data/entity-type-data.service.ts index 4020ff638dd..d47fadce172 100644 --- a/src/app/core/data/entity-type-data.service.ts +++ b/src/app/core/data/entity-type-data.service.ts @@ -1,26 +1,41 @@ +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; +import { + map, + switchMap, + take, +} from 'rxjs/operators'; + import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; -import { RequestService } from './request.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; -import { HALEndpointService } from '../shared/hal-endpoint.service'; import { ObjectCacheService } from '../cache/object-cache.service'; -import { Injectable } from '@angular/core'; -import { Observable } from 'rxjs'; -import { filter, map, switchMap, take } from 'rxjs/operators'; -import { RemoteData } from './remote-data'; +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'; +import { BaseDataService } from './base/base-data.service'; +import { + FindAllData, + FindAllDataImpl, +} from './base/find-all-data'; +import { + SearchData, + SearchDataImpl, +} from './base/search-data'; +import { FindListOptions } from './find-list-options.model'; import { PaginatedList } from './paginated-list.model'; -import { ItemType } from '../shared/item-relationships/item-type.model'; -import { getFirstSucceededRemoteData, getRemoteDataPayload } from '../shared/operators'; import { RelationshipTypeDataService } from './relationship-type-data.service'; -import { FindListOptions } from './find-list-options.model'; -import { BaseDataService } from './base/base-data.service'; -import { SearchData, SearchDataImpl } from './base/search-data'; -import { FindAllData, FindAllDataImpl } from './base/find-all-data'; +import { RemoteData } from './remote-data'; +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; @@ -48,7 +63,7 @@ export class EntityTypeDataService extends BaseDataService implements */ getRelationshipTypesEndpoint(entityTypeId: string): Observable { return this.halService.getEndpoint(this.linkPath).pipe( - switchMap((href) => this.halService.getEndpoint('relationshiptypes', `${href}/${entityTypeId}`)) + switchMap((href) => this.halService.getEndpoint('relationshiptypes', `${href}/${entityTypeId}`)), ); } @@ -74,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()); } /** @@ -84,7 +98,7 @@ export class EntityTypeDataService extends BaseDataService implements hasMoreThanOneAuthorized(): Observable { const findListOptions: FindListOptions = { elementsPerPage: 2, - currentPage: 1 + currentPage: 1, }; return this.getAllAuthorizedRelationshipType(findListOptions).pipe( map((result: RemoteData>) => { @@ -95,7 +109,7 @@ export class EntityTypeDataService extends BaseDataService implements output = false; } return output; - }) + }), ); } @@ -108,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()); } /** @@ -118,18 +131,11 @@ export class EntityTypeDataService extends BaseDataService implements hasMoreThanOneAuthorizedImport(): Observable { const findListOptions: FindListOptions = { elementsPerPage: 2, - currentPage: 1 + 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/eperson-registration.service.spec.ts b/src/app/core/data/eperson-registration.service.spec.ts index afd49271036..a60cef121a8 100644 --- a/src/app/core/data/eperson-registration.service.spec.ts +++ b/src/app/core/data/eperson-registration.service.spec.ts @@ -1,16 +1,17 @@ -import { RequestService } from './request.service'; -import { EpersonRegistrationService } from './eperson-registration.service'; -import { RestResponse } from '../cache/response.models'; +import { HttpHeaders } from '@angular/common/http'; import { cold } from 'jasmine-marbles'; -import { PostRequest } from './request.models'; -import { Registration } from '../shared/registration.model'; -import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub'; -import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils'; import { of as observableOf } from 'rxjs'; import { TestScheduler } from 'rxjs/testing'; -import { RequestEntry } from './request-entry.model'; -import { HttpHeaders } from '@angular/common/http'; + +import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils'; +import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub'; +import { RestResponse } from '../cache/response.models'; import { HttpOptions } from '../dspace-rest/dspace-rest.service'; +import { Registration } from '../shared/registration.model'; +import { EpersonRegistrationService } from './eperson-registration.service'; +import { PostRequest } from './request.models'; +import { RequestService } from './request.service'; +import { RequestEntry } from './request-entry.model'; describe('EpersonRegistrationService', () => { let testScheduler; @@ -44,7 +45,7 @@ describe('EpersonRegistrationService', () => { generateRequestId: 'request-id', send: {}, getByUUID: cold('a', - { a: Object.assign(new RequestEntry(), { response: new RestResponse(true, 200, 'Success') }) }) + { a: Object.assign(new RequestEntry(), { response: new RestResponse(true, 200, 'Success') }) }), }); rdbService = jasmine.createSpyObj('rdbService', { buildSingle: observableOf(rd), @@ -53,7 +54,7 @@ describe('EpersonRegistrationService', () => { service = new EpersonRegistrationService( requestService, rdbService, - halService + halService, ); }); @@ -111,9 +112,9 @@ describe('EpersonRegistrationService', () => { payload: Object.assign(new Registration(), { email: registrationWithUser.email, token: 'test-token', - user: registrationWithUser.user - }) - }) + user: registrationWithUser.user, + }), + }), })); }); @@ -128,10 +129,10 @@ describe('EpersonRegistrationService', () => { jasmine.objectContaining({ uuid: 'request-id', method: 'GET', href: 'rest-url/registrations/search/findByToken?token=test-token', - }), true + }), true, ); expectObservable(rdbService.buildSingle.calls.argsFor(0)[0]).toBe('(a|)', { - a: 'rest-url/registrations/search/findByToken?token=test-token' + a: 'rest-url/registrations/search/findByToken?token=test-token', }); }); }); diff --git a/src/app/core/data/eperson-registration.service.ts b/src/app/core/data/eperson-registration.service.ts index 499d05af380..90a3fab83a9 100644 --- a/src/app/core/data/eperson-registration.service.ts +++ b/src/app/core/data/eperson-registration.service.ts @@ -1,20 +1,33 @@ +import { + HttpHeaders, + HttpParams, +} from '@angular/common/http'; import { Injectable } from '@angular/core'; -import { RequestService } from './request.service'; -import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { GetRequest, PostRequest } from './request.models'; import { Observable } from 'rxjs'; -import { filter, find, map } from 'rxjs/operators'; -import { hasValue, isNotEmpty } from '../../shared/empty.util'; -import { Registration } from '../shared/registration.model'; +import { + filter, + find, + map, +} from 'rxjs/operators'; + +import { + hasValue, + isNotEmpty, +} from '../../shared/empty.util'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { HttpOptions } from '../dspace-rest/dspace-rest.service'; +import { GenericConstructor } from '../shared/generic-constructor'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; import { getFirstCompletedRemoteData } from '../shared/operators'; +import { Registration } from '../shared/registration.model'; import { ResponseParsingService } from './parsing.service'; -import { GenericConstructor } from '../shared/generic-constructor'; import { RegistrationResponseParsingService } from './registration-response-parsing.service'; import { RemoteData } from './remote-data'; -import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; -import { HttpOptions } from '../dspace-rest/dspace-rest.service'; -import { HttpHeaders } from '@angular/common/http'; -import { HttpParams } from '@angular/common/http'; +import { + GetRequest, + PostRequest, +} from './request.models'; +import { RequestService } from './request.service'; @Injectable({ providedIn: 'root', @@ -81,11 +94,11 @@ export class EpersonRegistrationService { map((href: string) => { const request = new PostRequest(requestId, href, registration, options); this.requestService.send(request); - }) + }), ).subscribe(); return this.rdbService.buildFromRequestUUID(requestId).pipe( - getFirstCompletedRemoteData() + getFirstCompletedRemoteData(), ); } @@ -105,7 +118,7 @@ export class EpersonRegistrationService { Object.assign(request, { getResponseParser(): GenericConstructor { return RegistrationResponseParsingService; - } + }, }); this.requestService.send(request, true); }); @@ -117,7 +130,7 @@ export class EpersonRegistrationService { } else { return rd; } - }) + }), ); } diff --git a/src/app/core/data/external-source-data.service.spec.ts b/src/app/core/data/external-source-data.service.spec.ts index 723d7f9bed6..5e643cc5491 100644 --- a/src/app/core/data/external-source-data.service.spec.ts +++ b/src/app/core/data/external-source-data.service.spec.ts @@ -1,11 +1,12 @@ -import { ExternalSourceDataService } from './external-source-data.service'; +import { of as observableOf } from 'rxjs'; +import { take } from 'rxjs/operators'; + import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; import { createPaginatedList } from '../../shared/testing/utils.test'; import { ExternalSourceEntry } from '../shared/external-source-entry.model'; -import { of as observableOf } from 'rxjs'; -import { GetRequest } from './request.models'; import { testSearchDataImplementation } from './base/search-data.spec'; -import { take } from 'rxjs/operators'; +import { ExternalSourceDataService } from './external-source-data.service'; +import { GetRequest } from './request.models'; describe('ExternalSourceService', () => { let service: ExternalSourceDataService; @@ -22,10 +23,10 @@ describe('ExternalSourceService', () => { metadata: { 'dc.identifier.uri': [ { - value: 'https://orcid.org/0001-0001-0001-0001' - } - ] - } + value: 'https://orcid.org/0001-0001-0001-0001', + }, + ], + }, }), Object.assign(new ExternalSourceEntry(), { id: '0001-0001-0001-0002', @@ -34,20 +35,20 @@ describe('ExternalSourceService', () => { metadata: { 'dc.identifier.uri': [ { - value: 'https://orcid.org/0001-0001-0001-0002' - } - ] - } - }) + value: 'https://orcid.org/0001-0001-0001-0002', + }, + ], + }, + }), ]; function init() { requestService = jasmine.createSpyObj('requestService', { generateRequestId: 'request-uuid', - send: {} + send: {}, }); rdbService = jasmine.createSpyObj('rdbService', { - buildList: createSuccessfulRemoteDataObject$(createPaginatedList(entries)) + buildList: createSuccessfulRemoteDataObject$(createPaginatedList(entries)), }); halService = jasmine.createSpyObj('halService', { getEndpoint: observableOf('external-sources-REST-endpoint'), @@ -96,12 +97,6 @@ describe('ExternalSourceService', () => { result.pipe(take(1)).subscribe(); expect(requestService.send).toHaveBeenCalledWith(jasmine.any(GetRequest), false); }); - - it('should return the entries', () => { - result.subscribe((resultRD) => { - expect(resultRD.payload.page).toBe(entries); - }); - }); }); }); }); diff --git a/src/app/core/data/external-source-data.service.ts b/src/app/core/data/external-source-data.service.ts index 02c5e4a53cc..e7f123dd18d 100644 --- a/src/app/core/data/external-source-data.service.ts +++ b/src/app/core/data/external-source-data.service.ts @@ -1,25 +1,37 @@ import { Injectable } from '@angular/core'; -import { ExternalSource } from '../shared/external-source.model'; -import { RequestService } from './request.service'; -import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; -import { ObjectCacheService } from '../cache/object-cache.service'; -import { HALEndpointService } from '../shared/hal-endpoint.service'; import { Observable } from 'rxjs'; -import { distinctUntilChanged, map, switchMap, take } from 'rxjs/operators'; +import { + distinctUntilChanged, + map, + switchMap, + take, +} from 'rxjs/operators'; + +import { + hasValue, + isNotEmptyOperator, +} from '../../shared/empty.util'; import { PaginatedSearchOptions } from '../../shared/search/models/paginated-search-options.model'; -import { hasValue, isNotEmptyOperator } from '../../shared/empty.util'; -import { RemoteData } from './remote-data'; -import { PaginatedList } from './paginated-list.model'; -import { ExternalSourceEntry } from '../shared/external-source-entry.model'; import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; -import { FindListOptions } from './find-list-options.model'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { ObjectCacheService } from '../cache/object-cache.service'; +import { ExternalSource } from '../shared/external-source.model'; +import { ExternalSourceEntry } from '../shared/external-source-entry.model'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; import { IdentifiableDataService } from './base/identifiable-data.service'; -import { SearchData, SearchDataImpl } from './base/search-data'; +import { + SearchData, + SearchDataImpl, +} from './base/search-data'; +import { FindListOptions } from './find-list-options.model'; +import { PaginatedList } from './paginated-list.model'; +import { RemoteData } from './remote-data'; +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; @@ -50,7 +62,7 @@ export class ExternalSourceDataService extends IdentifiableDataService { return this.getBrowseEndpoint().pipe( map((href) => this.getIDHref(href, externalSourceId)), - switchMap((href) => this.halService.getEndpoint('entries', href)) + switchMap((href) => this.halService.getEndpoint('entries', href)), ); } @@ -78,7 +90,7 @@ export class ExternalSourceDataService extends IdentifiableDataService { return this.findListByHref(href$, undefined, !hasCachedErrorResponse, reRequestOnStale, ...linksToFollow as any); - }) + }), ) as any; } 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 3e4493c32bf..4ae22c34d87 100644 --- a/src/app/core/data/facet-config-response-parsing.service.ts +++ b/src/app/core/data/facet-config-response-parsing.service.ts @@ -1,13 +1,14 @@ import { Injectable } from '@angular/core'; + +import { FacetConfigResponse } from '../../shared/search/models/facet-config-response.model'; import { SearchFilterConfig } from '../../shared/search/models/search-filter-config.model'; import { ParsedResponse } from '../cache/response.models'; -import { RawRestResponse } from '../dspace-rest/raw-rest-response.model'; import { DSpaceSerializer } from '../dspace-rest/dspace.serializer'; +import { RawRestResponse } from '../dspace-rest/raw-rest-response.model'; import { DspaceRestResponseParsingService } from './dspace-rest-response-parsing.service'; -import { FacetConfigResponse } from '../../shared/search/models/facet-config-response.model'; import { RestRequest } from './rest-request.model'; -@Injectable() +@Injectable({ providedIn: 'root' }) export class FacetConfigResponseParsingService extends DspaceRestResponseParsingService { parse(request: RestRequest, data: RawRestResponse): ParsedResponse { @@ -16,19 +17,19 @@ export class FacetConfigResponseParsingService extends DspaceRestResponseParsing const filters = serializer.deserializeArray(config); const _links = { - self: data.payload._links.self + self: data.payload._links.self, }; // fill in the missing links section filters.forEach((filterConfig: SearchFilterConfig) => { _links[filterConfig.name] = { - href: filterConfig._links.self.href + href: filterConfig._links.self.href, }; }); const facetConfigResponse = Object.assign(new FacetConfigResponse(), { filters, - _links + _links, }); this.addToObjectCache(facetConfigResponse, request, data); 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 0911ed50734..5cd24770d8b 100644 --- a/src/app/core/data/facet-value-response-parsing.service.ts +++ b/src/app/core/data/facet-value-response-parsing.service.ts @@ -1,13 +1,14 @@ import { Injectable } from '@angular/core'; + import { FacetValue } from '../../shared/search/models/facet-value.model'; +import { FacetValues } from '../../shared/search/models/facet-values.model'; import { ParsedResponse } from '../cache/response.models'; -import { RawRestResponse } from '../dspace-rest/raw-rest-response.model'; import { DSpaceSerializer } from '../dspace-rest/dspace.serializer'; -import { FacetValues } from '../../shared/search/models/facet-values.model'; +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 ae44d590a4c..4ada344bebb 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 @@ -1,18 +1,26 @@ -import { AuthorizationDataService } from './authorization-data.service'; -import { SiteDataService } from '../site-data.service'; -import { Site } from '../../shared/site.model'; -import { EPerson } from '../../eperson/models/eperson.model'; -import { of as observableOf, combineLatest as observableCombineLatest, Observable } from 'rxjs'; -import { FeatureID } from './feature-id'; +import { + combineLatest as observableCombineLatest, + Observable, + of as observableOf, +} from 'rxjs'; + import { hasValue } from '../../../shared/empty.util'; +import { getMockObjectCacheService } from '../../../shared/mocks/object-cache.service.mock'; +import { + createFailedRemoteDataObject$, + createSuccessfulRemoteDataObject$, +} from '../../../shared/remote-data.utils'; +import { createPaginatedList } from '../../../shared/testing/utils.test'; import { RequestParam } from '../../cache/models/request-param.model'; +import { EPerson } from '../../eperson/models/eperson.model'; import { Authorization } from '../../shared/authorization.model'; -import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; -import { createPaginatedList } from '../../../shared/testing/utils.test'; import { Feature } from '../../shared/feature.model'; -import { FindListOptions } from '../find-list-options.model'; +import { Site } from '../../shared/site.model'; import { testSearchDataImplementation } from '../base/search-data.spec'; -import { getMockObjectCacheService } from '../../../shared/mocks/object-cache.service.mock'; +import { FindListOptions } from '../find-list-options.model'; +import { SiteDataService } from '../site-data.service'; +import { AuthorizationDataService } from './authorization-data.service'; +import { FeatureID } from './feature-id'; describe('AuthorizationDataService', () => { let service: AuthorizationDataService; @@ -23,19 +31,19 @@ describe('AuthorizationDataService', () => { let ePerson: EPerson; const requestService = jasmine.createSpyObj('requestService', { - setStaleByHrefSubstring: jasmine.createSpy('setStaleByHrefSubstring') + setStaleByHrefSubstring: jasmine.createSpy('setStaleByHrefSubstring'), }); function init() { site = Object.assign(new Site(), { id: 'test-site', _links: { - self: { href: 'test-site-href' } - } + self: { href: 'test-site-href' }, + }, }); ePerson = Object.assign(new EPerson(), { id: 'test-eperson', - uuid: 'test-eperson' + uuid: 'test-eperson', }); siteService = jasmine.createSpyObj('siteService', { find: observableOf(site), @@ -64,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)); } @@ -157,26 +165,26 @@ describe('AuthorizationDataService', () => { const validPayload = [ Object.assign(new Authorization(), { feature: createSuccessfulRemoteDataObject$(Object.assign(new Feature(), { - id: 'invalid-feature' - })) + id: 'invalid-feature', + })), }), Object.assign(new Authorization(), { feature: createSuccessfulRemoteDataObject$(Object.assign(new Feature(), { - id: featureID - })) - }) + id: featureID, + })), + }), ]; const invalidPayload = [ Object.assign(new Authorization(), { feature: createSuccessfulRemoteDataObject$(Object.assign(new Feature(), { - id: 'invalid-feature' - })) + id: 'invalid-feature', + })), }), Object.assign(new Authorization(), { feature: createSuccessfulRemoteDataObject$(Object.assign(new Feature(), { - id: 'another-invalid-feature' - })) - }) + id: 'another-invalid-feature', + })), + }), ]; const emptyPayload = []; 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 c43d335234b..e5efbae671c 100644 --- a/src/app/core/data/feature-authorization/authorization-data.service.ts +++ b/src/app/core/data/feature-authorization/authorization-data.service.ts @@ -1,32 +1,47 @@ -import { Observable, of as observableOf } from 'rxjs'; import { Injectable } from '@angular/core'; -import { AUTHORIZATION } from '../../shared/authorization.resource-type'; -import { Authorization } from '../../shared/authorization.model'; -import { RequestService } from '../request.service'; +import { + Observable, + of as observableOf, +} from 'rxjs'; +import { + catchError, + map, + switchMap, +} from 'rxjs/operators'; + +import { + hasNoValue, + hasValue, + isNotEmpty, +} from '../../../shared/empty.util'; +import { + followLink, + 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 { Authorization } from '../../shared/authorization.model'; import { HALEndpointService } from '../../shared/hal-endpoint.service'; -import { SiteDataService } from '../site-data.service'; -import { followLink, FollowLinkConfig } from '../../../shared/utils/follow-link-config.model'; -import { RemoteData } from '../remote-data'; +import { getFirstCompletedRemoteData } from '../../shared/operators'; +import { BaseDataService } from '../base/base-data.service'; +import { + SearchData, + SearchDataImpl, +} from '../base/search-data'; +import { FindListOptions } from '../find-list-options.model'; import { PaginatedList } from '../paginated-list.model'; -import { catchError, map, switchMap } from 'rxjs/operators'; -import { hasNoValue, hasValue, isNotEmpty } from '../../../shared/empty.util'; -import { RequestParam } from '../../cache/models/request-param.model'; +import { RemoteData } from '../remote-data'; +import { RequestService } from '../request.service'; +import { SiteDataService } from '../site-data.service'; import { AuthorizationSearchParams } from './authorization-search-params'; import { oneAuthorizationMatchesFeature } from './authorization-utils'; import { FeatureID } from './feature-id'; -import { getFirstCompletedRemoteData } from '../../shared/operators'; -import { FindListOptions } from '../find-list-options.model'; -import { BaseDataService } from '../base/base-data.service'; -import { SearchData, SearchDataImpl } from '../base/search-data'; -import { dataService } from '../base/data-service.decorator'; /** * 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'; @@ -74,8 +89,8 @@ export class AuthorizationDataService extends BaseDataService imp return []; } }), - catchError(() => observableOf(false)), - oneAuthorizationMatchesFeature(featureId) + catchError(() => observableOf([])), + oneAuthorizationMatchesFeature(featureId), ); } @@ -100,7 +115,7 @@ export class AuthorizationDataService extends BaseDataService imp switchMap((url) => { if (hasNoValue(url)) { return this.siteService.find().pipe( - map((site) => site.self) + map((site) => site.self), ); } else { return observableOf(url); @@ -112,7 +127,7 @@ export class AuthorizationDataService extends BaseDataService imp map((url: string) => new AuthorizationSearchParams(url, ePersonUuid, featureId)), switchMap((params: AuthorizationSearchParams) => { return this.searchBy(this.searchByObjectPath, this.createSearchOptions(params.objectUrl, options, params.ePersonUuid, params.featureId), useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow); - }) + }), ); this.addDependency(out$, objectUrl$); @@ -132,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 d1b65f61235..cd4bc452a5e 100644 --- a/src/app/core/data/feature-authorization/authorization-utils.ts +++ b/src/app/core/data/feature-authorization/authorization-utils.ts @@ -1,13 +1,25 @@ -import { map, switchMap } from 'rxjs/operators'; -import { combineLatest as observableCombineLatest, Observable, of as observableOf } from 'rxjs'; -import { AuthorizationSearchParams } from './authorization-search-params'; -import { SiteDataService } from '../site-data.service'; -import { hasNoValue, hasValue, isNotEmpty } from '../../../shared/empty.util'; +import { + combineLatest as observableCombineLatest, + Observable, + of as observableOf, +} from 'rxjs'; +import { + map, + switchMap, +} from 'rxjs/operators'; + +import { + hasNoValue, + hasValue, + isNotEmpty, +} from '../../../shared/empty.util'; import { AuthService } from '../../auth/auth.service'; import { Authorization } from '../../shared/authorization.model'; import { Feature } from '../../shared/feature.model'; -import { FeatureID } from './feature-id'; import { getFirstSucceededRemoteDataPayload } from '../../shared/operators'; +import { SiteDataService } from '../site-data.service'; +import { AuthorizationSearchParams } from './authorization-search-params'; +import { FeatureID } from './feature-id'; /** * Operator accepting {@link AuthorizationSearchParams} and adding the current {@link Site}'s selflink to the parameter's @@ -20,12 +32,12 @@ export const addSiteObjectUrlIfEmpty = (siteService: SiteDataService) => switchMap((params: AuthorizationSearchParams) => { if (hasNoValue(params.objectUrl)) { return siteService.find().pipe( - map((site) => Object.assign({}, params, { objectUrl: site.self })) + map((site) => Object.assign({}, params, { objectUrl: site.self })), ); } else { return observableOf(params); } - }) + }), ); /** @@ -42,17 +54,17 @@ export const addAuthenticatedUserUuidIfEmpty = (authService: AuthService) => switchMap((authenticated) => { if (authenticated) { return authService.getAuthenticatedUserFromStore().pipe( - map((ePerson) => Object.assign({}, params, { ePersonUuid: ePerson.uuid })) + map((ePerson) => Object.assign({}, params, { ePersonUuid: ePerson.uuid })), ); } else { return observableOf(params); } - }) + }), ); } else { return observableOf(params); } - }) + }), ); /** @@ -68,16 +80,16 @@ export const oneAuthorizationMatchesFeature = (featureID: FeatureID) => source.pipe( switchMap((authorizations: Authorization[]) => { if (isNotEmpty(authorizations)) { - return observableCombineLatest( + return observableCombineLatest([ ...authorizations .filter((authorization: Authorization) => hasValue(authorization.feature)) .map((authorization: Authorization) => authorization.feature.pipe( - getFirstSucceededRemoteDataPayload() - )) - ); + getFirstSucceededRemoteDataPayload(), + )), + ]); } else { 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 b41a322cb62..1b1b4a9d6ca 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,27 +1,13 @@ -import { Injectable } from '@angular/core'; -import { SingleFeatureAuthorizationGuard } from './single-feature-authorization.guard'; -import { AuthorizationDataService } from '../authorization-data.service'; -import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router'; -import { AuthService } from '../../../auth/auth.service'; -import { Observable, of as observableOf } from 'rxjs'; +import { CanActivateFn } from '@angular/router'; +import { of as observableOf } from 'rxjs'; + import { FeatureID } from '../feature-id'; +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 2ab77a00cc4..6d7dac314e3 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,27 +1,13 @@ -import { Injectable } from '@angular/core'; -import { SingleFeatureAuthorizationGuard } from './single-feature-authorization.guard'; -import { AuthorizationDataService } from '../authorization-data.service'; -import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router'; -import { AuthService } from '../../../auth/auth.service'; -import { Observable, of as observableOf } from 'rxjs'; +import { CanActivateFn } from '@angular/router'; +import { of as observableOf } from 'rxjs'; + import { FeatureID } from '../feature-id'; +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 6c1f330c695..18292bb943b 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,77 +1,82 @@ -import { AuthorizationDataService } from '../authorization-data.service'; -import { ActivatedRouteSnapshot, Resolve, Router, RouterStateSnapshot } from '@angular/router'; -import { RemoteData } from '../../remote-data'; -import { Observable, of as observableOf } from 'rxjs'; +import { TestBed } from '@angular/core/testing'; +import { + ResolveFn, + Router, + UrlTree, +} from '@angular/router'; +import { + Observable, + of as observableOf, +} from 'rxjs'; + import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils'; +import { AuthService } from '../../../auth/auth.service'; import { DSpaceObject } from '../../../shared/dspace-object.model'; -import { DsoPageSingleFeatureGuard } from './dso-page-single-feature.guard'; +import { RemoteData } from '../../remote-data'; +import { AuthorizationDataService } from '../authorization-data.service'; import { FeatureID } from '../feature-id'; -import { AuthService } from '../../../auth/auth.service'; +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' + self: 'test-selflink', } as DSpaceObject; authorizationService = jasmine.createSpyObj('authorizationService', { - isAuthorized: observableOf(true) + isAuthorized: observableOf(true), }); router = jasmine.createSpyObj('router', { - parseUrl: {} - }); - resolver = jasmine.createSpyObj('resolver', { - resolve: createSuccessfulRemoteDataObject$(object) + parseUrl: {}, }); + resolver = () => createSuccessfulRemoteDataObject$(object); authService = jasmine.createSpyObj('authService', { - isAuthenticated: observableOf(true) + isAuthenticated: observableOf(true), }); parentRoute = { params: { - id: '3e1a5327-dabb-41ff-af93-e6cab9d032f0' - } + id: '3e1a5327-dabb-41ff-af93-e6cab9d032f0', + }, }; route = { params: { }, - parent: parentRoute + 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(); }); @@ -80,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 3fc90f90696..5073a386532 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,27 +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 { DsoPageSomeFeatureGuard } from './dso-page-some-feature.guard'; -import { ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; +import { RemoteData } from '../../remote-data'; import { FeatureID } from '../feature-id'; -import { map } from 'rxjs/operators'; -import { Observable } from 'rxjs'; +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 071b1b0731f..08f1c96b299 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,77 +1,82 @@ -import { AuthorizationDataService } from '../authorization-data.service'; -import { ActivatedRouteSnapshot, Resolve, Router, RouterStateSnapshot } from '@angular/router'; -import { RemoteData } from '../../remote-data'; -import { Observable, of as observableOf } from 'rxjs'; +import { TestBed } from '@angular/core/testing'; +import { + ResolveFn, + Router, + UrlTree, +} from '@angular/router'; +import { + Observable, + of as observableOf, +} from 'rxjs'; + import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils'; +import { AuthService } from '../../../auth/auth.service'; import { DSpaceObject } from '../../../shared/dspace-object.model'; +import { RemoteData } from '../../remote-data'; +import { AuthorizationDataService } from '../authorization-data.service'; import { FeatureID } from '../feature-id'; -import { AuthService } from '../../../auth/auth.service'; -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' + self: 'test-selflink', } as DSpaceObject; - + featureIds = [FeatureID.LoginOnBehalfOf, FeatureID.CanDelete]; authorizationService = jasmine.createSpyObj('authorizationService', { - isAuthorized: observableOf(true) + isAuthorized: observableOf(true), }); router = jasmine.createSpyObj('router', { - parseUrl: {} - }); - resolver = jasmine.createSpyObj('resolver', { - resolve: createSuccessfulRemoteDataObject$(object) + parseUrl: {}, }); + resolver = () => createSuccessfulRemoteDataObject$(object); authService = jasmine.createSpyObj('authService', { - isAuthenticated: observableOf(true) + isAuthenticated: observableOf(true), }); parentRoute = { params: { - id: '3e1a5327-dabb-41ff-af93-e6cab9d032f0' - } + id: '3e1a5327-dabb-41ff-af93-e6cab9d032f0', + }, }; route = { params: { }, - parent: parentRoute + 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(); }); @@ -80,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 86837093459..7469f113b49 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,46 +1,60 @@ -import { ActivatedRouteSnapshot, Resolve, Router, RouterStateSnapshot } from '@angular/router'; -import { RemoteData } from '../../remote-data'; -import { AuthorizationDataService } from '../authorization-data.service'; +import { + ActivatedRouteSnapshot, + CanActivateFn, + ResolveFn, + RouterStateSnapshot, +} from '@angular/router'; import { Observable } from 'rxjs'; -import { getAllSucceededRemoteDataPayload } from '../../../shared/operators'; import { map } from 'rxjs/operators'; + +import { + hasNoValue, + hasValue, +} from '../../../../shared/empty.util'; import { DSpaceObject } from '../../../shared/dspace-object.model'; -import { AuthService } from '../../../auth/auth.service'; -import { hasNoValue, hasValue } from '../../../../shared/empty.util'; -import { SomeFeatureAuthorizationGuard } from './some-feature-authorization.guard'; +import { getAllSucceededRemoteDataPayload } from '../../../shared/operators'; +import { RemoteData } from '../../remote-data'; +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) + 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 5afd572326f..9641d0aace9 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,27 +1,12 @@ -import { Injectable } from '@angular/core'; -import { SingleFeatureAuthorizationGuard } from './single-feature-authorization.guard'; -import { AuthorizationDataService } from '../authorization-data.service'; -import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router'; -import { AuthService } from '../../../auth/auth.service'; -import { Observable, of as observableOf } from 'rxjs'; +import { CanActivateFn } from '@angular/router'; +import { of as observableOf } from 'rxjs'; + import { FeatureID } from '../feature-id'; +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 635aa3530bd..7c15fa4cdff 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,39 +1,22 @@ -import { SingleFeatureAuthorizationGuard } from './single-feature-authorization.guard'; +import { + TestBed, + waitForAsync, +} from '@angular/core/testing'; +import { + Router, + UrlTree, +} from '@angular/router'; +import { + Observable, + of as observableOf, +} from 'rxjs'; + +import { AuthService } from '../../../auth/auth.service'; import { AuthorizationDataService } from '../authorization-data.service'; import { FeatureID } from '../feature-id'; -import { Observable, of as observableOf } from 'rxjs'; -import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router'; -import { AuthService } from '../../../auth/auth.service'; - -/** - * 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); - } +import { singleFeatureAuthorizationGuard } from './single-feature-authorization.guard'; - 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; @@ -48,25 +31,44 @@ describe('SingleFeatureAuthorizationGuard', () => { ePersonUuid = 'fake-eperson-uuid'; authorizationService = jasmine.createSpyObj('authorizationService', { - isAuthorized: observableOf(true) + isAuthorized: observableOf(true), }); router = jasmine.createSpyObj('router', { - parseUrl: {} + parseUrl: {}, }); authService = jasmine.createSpyObj('authService', { - isAuthenticated: observableOf(true) + isAuthenticated: observableOf(true), + }); + + TestBed.configureTestingModule({ + providers: [ + { provide: AuthorizationDataService, useValue: authorizationService }, + { provide: Router, useValue: router }, + { provide: AuthService, useValue: authService }, + ], }); - guard = new SingleFeatureAuthorizationGuardImpl(authorizationService, router, authService, featureId, objectUrl, ePersonUuid); } - 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 cb71d2f4181..995dcb6f5c4 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,27 +1,35 @@ -import { ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; -import { FeatureID } from '../feature-id'; +import { + ActivatedRouteSnapshot, + CanActivateFn, + RouterStateSnapshot, +} from '@angular/router'; import { Observable } from 'rxjs'; -import { map} from 'rxjs/operators'; -import { SomeFeatureAuthorizationGuard } from './some-feature-authorization.guard'; +import { map } from 'rxjs/operators'; + +import { FeatureID } from '../feature-id'; +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 cc6f50c1613..4caa1f806d9 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,27 +1,12 @@ -import { Injectable } from '@angular/core'; -import { SingleFeatureAuthorizationGuard } from './single-feature-authorization.guard'; +import { CanActivateFn } from '@angular/router'; +import { of as observableOf } from 'rxjs'; + import { FeatureID } from '../feature-id'; -import { AuthorizationDataService } from '../authorization-data.service'; -import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router'; -import { Observable, of as observableOf } from 'rxjs'; -import { AuthService } from '../../../auth/auth.service'; +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 bdbb8250e27..ee08532d38f 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,27 +1,12 @@ -import { SingleFeatureAuthorizationGuard } from './single-feature-authorization.guard'; -import { Injectable } from '@angular/core'; -import { AuthorizationDataService } from '../authorization-data.service'; -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 { FeatureID } from '../feature-id'; -import { AuthService } from '../../../auth/auth.service'; +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 90153d2d148..79e023bdd0f 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,39 +1,22 @@ +import { + TestBed, + waitForAsync, +} from '@angular/core/testing'; +import { + Router, + UrlTree, +} from '@angular/router'; +import { + Observable, + of as observableOf, +} from 'rxjs'; + +import { AuthService } from '../../../auth/auth.service'; import { AuthorizationDataService } from '../authorization-data.service'; import { FeatureID } from '../feature-id'; -import { Observable, of as observableOf } from 'rxjs'; -import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router'; -import { AuthService } from '../../../auth/auth.service'; -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; @@ -52,20 +35,29 @@ describe('SomeFeatureAuthorizationGuard', () => { authorizationService = Object.assign({ isAuthorized(featureId?: FeatureID): Observable { return observableOf(authorizedFeatureIds.indexOf(featureId) > -1); - } + }, }); + router = jasmine.createSpyObj('router', { - parseUrl: {} + parseUrl: {}, }); + authService = jasmine.createSpyObj('authService', { - isAuthenticated: observableOf(true) + isAuthenticated: observableOf(true), + }); + + TestBed.configureTestingModule({ + providers: [ + { provide: AuthorizationDataService, useValue: authorizationService }, + { provide: Router, useValue: router }, + { provide: AuthService, useValue: authService }, + ], }); - guard = new SomeFeatureAuthorizationGuardImpl(authorizationService, router, authService, featureIds, objectUrl, ePersonUuid); } - beforeEach(() => { + beforeEach(waitForAsync(() => { init(); - }); + })); describe('canActivate', () => { describe('when the user isn\'t authorized', () => { @@ -74,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(); }); @@ -87,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(); }); @@ -100,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 b909640ea64..53e5e582eb8 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,54 +1,56 @@ -import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot, UrlTree } from '@angular/router'; -import { AuthorizationDataService } from '../authorization-data.service'; -import { FeatureID } from '../feature-id'; -import { combineLatest as observableCombineLatest, Observable, of as observableOf } from 'rxjs'; +import { inject } from '@angular/core'; +import { + ActivatedRouteSnapshot, + CanActivateFn, + Router, + RouterStateSnapshot, + UrlTree, +} from '@angular/router'; +import { + combineLatest as observableCombineLatest, + Observable, + of as observableOf, +} from 'rxjs'; import { switchMap } from 'rxjs/operators'; + import { AuthService } from '../../../auth/auth.service'; import { returnForbiddenUrlTreeOrLoginOnAllFalse } from '../../../shared/authorized.operators'; +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. - */ -export abstract class SomeFeatureAuthorizationGuard implements CanActivate { - constructor(protected authorizationService: AuthorizationDataService, - protected router: Router, - protected authService: AuthService) { - } + * 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. - /** - * 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))) + * @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 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 680495686eb..21cafeaba3f 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,27 +1,12 @@ -import { Injectable } from '@angular/core'; -import { SingleFeatureAuthorizationGuard } from './single-feature-authorization.guard'; -import { AuthorizationDataService } from '../authorization-data.service'; -import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router'; -import { AuthService } from '../../../auth/auth.service'; -import { Observable, of as observableOf } from 'rxjs'; +import { CanActivateFn } from '@angular/router'; +import { of as observableOf } from 'rxjs'; + import { FeatureID } from '../feature-id'; +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 eda87911539..191d8b002ca 100644 --- a/src/app/core/data/feature-authorization/feature-data.service.ts +++ b/src/app/core/data/feature-authorization/feature-data.service.ts @@ -1,18 +1,16 @@ import { Injectable } from '@angular/core'; -import { FEATURE } from '../../shared/feature.resource-type'; -import { Feature } from '../../shared/feature.model'; -import { RequestService } from '../request.service'; + import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service'; import { ObjectCacheService } from '../../cache/object-cache.service'; +import { Feature } from '../../shared/feature.model'; 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/feature-authorization/feature-id.ts b/src/app/core/data/feature-authorization/feature-id.ts index 8fef45a9532..3d5803b018b 100644 --- a/src/app/core/data/feature-authorization/feature-id.ts +++ b/src/app/core/data/feature-authorization/feature-id.ts @@ -34,4 +34,7 @@ export enum FeatureID { CanEditItem = 'canEditItem', CanRegisterDOI = 'canRegisterDOI', CanSubscribe = 'canSubscribeDso', + CoarNotifyEnabled = 'coarNotifyEnabled', + CanSeeQA = 'canSeeQA', + EPersonForgotPassword = 'epersonForgotPassword' } diff --git a/src/app/core/data/filtered-discovery-page-response-parsing.service.spec.ts b/src/app/core/data/filtered-discovery-page-response-parsing.service.spec.ts index ac0e96a2e6b..6966e6a6311 100644 --- a/src/app/core/data/filtered-discovery-page-response-parsing.service.spec.ts +++ b/src/app/core/data/filtered-discovery-page-response-parsing.service.spec.ts @@ -1,10 +1,10 @@ -import { FilteredDiscoveryPageResponseParsingService } from './filtered-discovery-page-response-parsing.service'; import { getMockObjectCacheService } from '../../shared/mocks/object-cache.service.mock'; +import { FilteredDiscoveryQueryResponse } from '../cache/response.models'; +import { RawRestResponse } from '../dspace-rest/raw-rest-response.model'; import { GenericConstructor } from '../shared/generic-constructor'; +import { FilteredDiscoveryPageResponseParsingService } from './filtered-discovery-page-response-parsing.service'; import { ResponseParsingService } from './parsing.service'; import { GetRequest } from './request.models'; -import { RawRestResponse } from '../dspace-rest/raw-rest-response.model'; -import { FilteredDiscoveryQueryResponse } from '../cache/response.models'; describe('FilteredDiscoveryPageResponseParsingService', () => { let service: FilteredDiscoveryPageResponseParsingService; @@ -17,15 +17,15 @@ describe('FilteredDiscoveryPageResponseParsingService', () => { const request = Object.assign(new GetRequest('client/f5b4ccb8-fbb0-4548-b558-f234d9fdfad6', 'https://rest.api/path'), { getResponseParser(): GenericConstructor { return FilteredDiscoveryPageResponseParsingService; - } + }, }); const mockResponse = { payload: { - 'discovery-query': 'query' + 'discovery-query': 'query', }, statusCode: 200, - statusText: 'OK' + statusText: 'OK', } as RawRestResponse; it('should return a FilteredDiscoveryQueryResponse containing the correct query', () => { 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 da7a21c488f..e07f46e9160 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 @@ -1,16 +1,20 @@ import { Injectable } from '@angular/core'; -import { ResponseParsingService } from './parsing.service'; + +import { ObjectCacheService } from '../cache/object-cache.service'; +import { + FilteredDiscoveryQueryResponse, + RestResponse, +} from '../cache/response.models'; import { RawRestResponse } from '../dspace-rest/raw-rest-response.model'; import { BaseResponseParsingService } from './base-response-parsing.service'; -import { ObjectCacheService } from '../cache/object-cache.service'; -import { FilteredDiscoveryQueryResponse, RestResponse } from '../cache/response.models'; +import { ResponseParsingService } from './parsing.service'; 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/find-list-options.model.ts b/src/app/core/data/find-list-options.model.ts index dc567d4b531..78fe26fcab9 100644 --- a/src/app/core/data/find-list-options.model.ts +++ b/src/app/core/data/find-list-options.model.ts @@ -1,15 +1,15 @@ -import { SortOptions } from '../cache/models/sort-options.model'; import { RequestParam } from '../cache/models/request-param.model'; +import { SortOptions } from '../cache/models/sort-options.model'; /** * The options for a find list request */ export class FindListOptions { - scopeID?: string; - elementsPerPage?: number; - currentPage?: number; - sort?: SortOptions; - searchParams?: RequestParam[]; - startsWith?: string; - fetchThumbnail?: boolean; + scopeID?: string; + elementsPerPage?: number; + currentPage?: number; + sort?: SortOptions; + searchParams?: RequestParam[]; + startsWith?: string; + fetchThumbnail?: boolean; } diff --git a/src/app/core/data/href-only-data.service.spec.ts b/src/app/core/data/href-only-data.service.spec.ts index bf7d2890ea2..cba85d5de65 100644 --- a/src/app/core/data/href-only-data.service.spec.ts +++ b/src/app/core/data/href-only-data.service.spec.ts @@ -1,8 +1,11 @@ -import { HrefOnlyDataService } from './href-only-data.service'; -import { followLink, FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; -import { FindListOptions } from './find-list-options.model'; +import { + followLink, + FollowLinkConfig, +} from '../../shared/utils/follow-link-config.model'; import { BaseDataService } from './base/base-data.service'; +import { FindListOptions } from './find-list-options.model'; +import { HrefOnlyDataService } from './href-only-data.service'; describe(`HrefOnlyDataService`, () => { let service: HrefOnlyDataService; @@ -23,60 +26,60 @@ describe(`HrefOnlyDataService`, () => { expect((service as any).dataService).toBeInstanceOf(BaseDataService); }); - describe(`findByHref`, () => { - beforeEach(() => { - spy = spyOn((service as any).dataService, 'findByHref').and.returnValue(createSuccessfulRemoteDataObject$(null)); - }); + describe(`findByHref`, () => { + beforeEach(() => { + spy = spyOn((service as any).dataService, 'findByHref').and.returnValue(createSuccessfulRemoteDataObject$(null)); + }); - it(`should forward to findByHref on the internal DataService`, () => { - service.findByHref(href, false, false, ...followLinks); - expect(spy).toHaveBeenCalledWith(href, false, false, ...followLinks); - }); + it(`should forward to findByHref on the internal DataService`, () => { + service.findByHref(href, false, false, ...followLinks); + expect(spy).toHaveBeenCalledWith(href, false, false, ...followLinks); + }); - describe(`when useCachedVersionIfAvailable is omitted`, () => { - it(`should call findByHref on the internal DataService with useCachedVersionIfAvailable = true`, () => { - service.findByHref(href); - expect(spy).toHaveBeenCalledWith(jasmine.anything(), true, jasmine.anything()); - }); + describe(`when useCachedVersionIfAvailable is omitted`, () => { + it(`should call findByHref on the internal DataService with useCachedVersionIfAvailable = true`, () => { + service.findByHref(href); + expect(spy).toHaveBeenCalledWith(jasmine.anything(), true, jasmine.anything()); }); + }); - describe(`when reRequestOnStale is omitted`, () => { - it(`should call findByHref on the internal DataService with reRequestOnStale = true`, () => { - service.findByHref(href); - expect(spy).toHaveBeenCalledWith(jasmine.anything(), jasmine.anything(), true); - }); + describe(`when reRequestOnStale is omitted`, () => { + it(`should call findByHref on the internal DataService with reRequestOnStale = true`, () => { + service.findByHref(href); + expect(spy).toHaveBeenCalledWith(jasmine.anything(), jasmine.anything(), true); }); }); + }); - describe(`findListByHref`, () => { - beforeEach(() => { - spy = spyOn((service as any).dataService, 'findListByHref').and.returnValue(createSuccessfulRemoteDataObject$(null)); - }); + describe(`findListByHref`, () => { + beforeEach(() => { + spy = spyOn((service as any).dataService, 'findListByHref').and.returnValue(createSuccessfulRemoteDataObject$(null)); + }); - it(`should delegate to findListByHref on the internal DataService`, () => { - service.findListByHref(href, findListOptions, false, false, ...followLinks); - expect(spy).toHaveBeenCalledWith(href, findListOptions, false, false, ...followLinks); - }); + it(`should delegate to findListByHref on the internal DataService`, () => { + service.findListByHref(href, findListOptions, false, false, ...followLinks); + expect(spy).toHaveBeenCalledWith(href, findListOptions, false, false, ...followLinks); + }); - describe(`when findListOptions is omitted`, () => { - it(`should call findListByHref on the internal DataService with findListOptions = {}`, () => { - service.findListByHref(href); - expect(spy).toHaveBeenCalledWith(jasmine.anything(), {}, jasmine.anything(), jasmine.anything()); - }); + describe(`when findListOptions is omitted`, () => { + it(`should call findListByHref on the internal DataService with findListOptions = {}`, () => { + service.findListByHref(href); + expect(spy).toHaveBeenCalledWith(jasmine.anything(), {}, jasmine.anything(), jasmine.anything()); }); + }); - describe(`when useCachedVersionIfAvailable is omitted`, () => { - it(`should call findListByHref on the internal DataService with useCachedVersionIfAvailable = true`, () => { - service.findListByHref(href); - expect(spy).toHaveBeenCalledWith(jasmine.anything(), jasmine.anything(), true, jasmine.anything()); - }); + describe(`when useCachedVersionIfAvailable is omitted`, () => { + it(`should call findListByHref on the internal DataService with useCachedVersionIfAvailable = true`, () => { + service.findListByHref(href); + expect(spy).toHaveBeenCalledWith(jasmine.anything(), jasmine.anything(), true, jasmine.anything()); }); + }); - describe(`when reRequestOnStale is omitted`, () => { - it(`should call findListByHref on the internal DataService with reRequestOnStale = true`, () => { - service.findListByHref(href); - expect(spy).toHaveBeenCalledWith(jasmine.anything(), jasmine.anything(), jasmine.anything(), true); - }); + describe(`when reRequestOnStale is omitted`, () => { + it(`should call findListByHref on the internal DataService with reRequestOnStale = true`, () => { + service.findListByHref(href); + expect(spy).toHaveBeenCalledWith(jasmine.anything(), jasmine.anything(), jasmine.anything(), true); }); }); + }); }); diff --git a/src/app/core/data/href-only-data.service.ts b/src/app/core/data/href-only-data.service.ts index 0a765de101b..dd40be8f7df 100644 --- a/src/app/core/data/href-only-data.service.ts +++ b/src/app/core/data/href-only-data.service.ts @@ -1,24 +1,21 @@ -import { RequestService } from './request.service'; -import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; -import { ObjectCacheService } from '../cache/object-cache.service'; -import { HALEndpointService } from '../shared/hal-endpoint.service'; import { Injectable } from '@angular/core'; -import { VOCABULARY_ENTRY } from '../submission/vocabularies/models/vocabularies.resource-type'; -import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; -import { RemoteData } from './remote-data'; import { Observable } from 'rxjs'; -import { PaginatedList } from './paginated-list.model'; -import { ITEM_TYPE } from '../shared/item-relationships/item-type.resource-type'; -import { LICENSE } from '../shared/license.resource-type'; + +import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { CacheableObject } from '../cache/cacheable-object.model'; -import { FindListOptions } from './find-list-options.model'; +import { ObjectCacheService } from '../cache/object-cache.service'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; import { BaseDataService } from './base/base-data.service'; import { HALDataService } from './base/hal-data-service.interface'; -import { dataService } from './base/data-service.decorator'; +import { FindListOptions } from './find-list-options.model'; +import { PaginatedList } from './paginated-list.model'; +import { RemoteData } from './remote-data'; +import { RequestService } from './request.service'; /** - * A DataService with only findByHref methods. Its purpose is to be used for resources that don't - * need to be retrieved by ID, or have any way to update them, but require a DataService in order + * A UpdateDataServiceImpl with only findByHref methods. Its purpose is to be used for resources that don't + * need to be retrieved by ID, or have any way to update them, but require a UpdateDataServiceImpl in order * for their links to be resolved by the LinkService. * * an @dataService annotation can be added for any number of these resource types @@ -32,12 +29,7 @@ import { dataService } from './base/data-service.decorator'; * ``` * 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 03422dadfb0..502b1fe7107 100644 --- a/src/app/core/data/identifier-data.service.ts +++ b/src/app/core/data/identifier-data.service.ts @@ -1,27 +1,33 @@ -import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http'; +import { + HttpClient, + HttpHeaders, + HttpParams, +} from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Store } from '@ngrx/store'; +import { Observable } from 'rxjs'; +import { + map, + switchMap, +} from 'rxjs/operators'; + import { NotificationsService } from '../../shared/notifications/notifications.service'; -import { dataService } from './base/data-service.decorator'; +import { IdentifierData } from '../../shared/object-list/identifier-data/identifier-data.model'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { ObjectCacheService } from '../cache/object-cache.service'; +import { CoreState } from '../core-state.model'; +import { HttpOptions } from '../dspace-rest/dspace-rest.service'; +import { ConfigurationProperty } from '../shared/configuration-property.model'; import { HALEndpointService } from '../shared/hal-endpoint.service'; +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 { RequestService } from './request.service'; +import { ConfigurationDataService } from './configuration-data.service'; import { DefaultChangeAnalyzer } from './default-change-analyzer.service'; -import { CoreState } from '../core-state.model'; -import { Observable } from 'rxjs'; import { RemoteData } from './remote-data'; -import { Item } from '../shared/item.model'; -import { IDENTIFIERS } from '../../shared/object-list/identifier-data/identifier-data.resource-type'; -import { IdentifierData } from '../../shared/object-list/identifier-data/identifier-data.model'; -import { getFirstCompletedRemoteData } from '../shared/operators'; -import { map, switchMap } from 'rxjs/operators'; -import {ConfigurationProperty} from '../shared/configuration-property.model'; -import {ConfigurationDataService} from './configuration-data.service'; -import { HttpOptions } from '../dspace-rest/dspace-rest.service'; import { PostRequest } from './request.models'; -import { sendRequest } from '../shared/request.operators'; +import { RequestService } from './request.service'; import { RestRequest } from './rest-request.model'; /** @@ -29,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( @@ -61,7 +66,7 @@ export class IdentifierDataService extends BaseDataService { public getIdentifierRegistrationConfiguration(): Observable { return this.configurationService.findByPropertyName('identifiers.item-status.register-doi').pipe( getFirstCompletedRemoteData(), - map((propertyRD: RemoteData) => propertyRD.hasSucceeded ? propertyRD.payload.values : []) + map((propertyRD: RemoteData) => propertyRD.hasSucceeded ? propertyRD.payload.values : []), ); } @@ -79,7 +84,7 @@ export class IdentifierDataService extends BaseDataService { return new PostRequest(requestId, endpointURL, item._links.self.href, options); }), sendRequest(this.requestService), - switchMap((request: RestRequest) => this.rdbService.buildFromRequestUUID(request.uuid) as Observable>) + switchMap((request: RestRequest) => this.rdbService.buildFromRequestUUID(request.uuid) as Observable>), ); } } diff --git a/src/app/core/data/item-data.service.spec.ts b/src/app/core/data/item-data.service.spec.ts index 2c20ed0fb69..dd60d940702 100644 --- a/src/app/core/data/item-data.service.spec.ts +++ b/src/app/core/data/item-data.service.spec.ts @@ -1,25 +1,32 @@ import { HttpClient } from '@angular/common/http'; import { Store } from '@ngrx/store'; -import { cold, getTestScheduler } from 'jasmine-marbles'; +import { + cold, + getTestScheduler, +} from 'jasmine-marbles'; import { of as observableOf } from 'rxjs'; import { TestScheduler } from 'rxjs/testing'; + +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'; +import { CoreState } from '../core-state.model'; import { ExternalSourceEntry } from '../shared/external-source-entry.model'; +import { testCreateDataImplementation } from './base/create-data.spec'; +import { testDeleteDataImplementation } from './base/delete-data.spec'; +import { testPatchDataImplementation } from './base/patch-data.spec'; +import { FindListOptions } from './find-list-options.model'; import { ItemDataService } from './item-data.service'; -import { DeleteRequest, PostRequest } from './request.models'; +import { + DeleteRequest, + PostRequest, +} from './request.models'; import { RequestService } from './request.service'; -import { getMockRemoteDataBuildService } from '../../shared/mocks/remote-data-build.service.mock'; -import { CoreState } from '../core-state.model'; import { RequestEntry } from './request-entry.model'; -import { FindListOptions } from './find-list-options.model'; -import { HALEndpointServiceStub } from 'src/app/shared/testing/hal-endpoint-service.stub'; -import { testCreateDataImplementation } from './base/create-data.spec'; -import { testPatchDataImplementation } from './base/patch-data.spec'; -import { testDeleteDataImplementation } from './base/delete-data.spec'; describe('ItemDataService', () => { let scheduler: TestScheduler; @@ -46,7 +53,7 @@ describe('ItemDataService', () => { const objectCache = {} as ObjectCacheService; const halEndpointService: any = new HALEndpointServiceStub(itemEndpoint); const bundleService = jasmine.createSpyObj('bundleService', { - findByHref: {} + findByHref: {}, }); const scopeID = '4af28e99-6a9c-4036-a199-e1b587046d39'; @@ -54,8 +61,8 @@ describe('ItemDataService', () => { scopeID: scopeID, sort: { field: '', - direction: undefined - } + direction: undefined, + }, }); const browsesEndpoint = 'https://rest.api/discover/browses'; @@ -73,7 +80,7 @@ describe('ItemDataService', () => { cold('--a-', { a: itemBrowseEndpoint }) : cold('--#-', undefined, browseError); return jasmine.createSpyObj('bs', { - getBrowseURLFor: obs + getBrowseURLFor: obs, }); } @@ -158,7 +165,7 @@ describe('ItemDataService', () => { const externalSourceEntry = Object.assign(new ExternalSourceEntry(), { display: 'John, Doe', value: 'John, Doe', - _links: { self: { href: 'http://test-rest.com/server/api/integration/externalSources/orcidV2/entryValues/0000-0003-4851-8004' } } + _links: { self: { href: 'http://test-rest.com/server/api/integration/externalSources/orcidV2/entryValues/0000-0003-4851-8004' } }, }); beforeEach(() => { diff --git a/src/app/core/data/item-data.service.ts b/src/app/core/data/item-data.service.ts index c3fa84dd6c8..e1f789b5da7 100644 --- a/src/app/core/data/item-data.service.ts +++ b/src/app/core/data/item-data.service.ts @@ -8,44 +8,71 @@ /* eslint-disable max-classes-per-file */ import { HttpHeaders } from '@angular/common/http'; import { Injectable } from '@angular/core'; +import { Operation } from 'fast-json-patch'; import { Observable } from 'rxjs'; -import { distinctUntilChanged, filter, find, map, switchMap, take } from 'rxjs/operators'; -import { hasValue, isNotEmpty, isNotEmptyOperator } from '../../shared/empty.util'; +import { + distinctUntilChanged, + filter, + find, + map, + switchMap, + take, +} from 'rxjs/operators'; + +import { + hasValue, + isNotEmpty, + isNotEmptyOperator, +} from '../../shared/empty.util'; import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { PaginatedSearchOptions } from '../../shared/search/models/paginated-search-options.model'; import { BrowseService } from '../browse/browse.service'; 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 { HttpOptions } from '../dspace-rest/dspace-rest.service'; +import { Bundle } from '../shared/bundle.model'; import { Collection } from '../shared/collection.model'; 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'; import { URLCombiner } from '../url-combiner/url-combiner'; +import { + CreateData, + CreateDataImpl, +} from './base/create-data'; +import { + DeleteData, + DeleteDataImpl, +} from './base/delete-data'; +import { + ConstructIdEndpoint, + IdentifiableDataService, +} from './base/identifiable-data.service'; +import { + PatchData, + PatchDataImpl, +} from './base/patch-data'; +import { BundleDataService } from './bundle-data.service'; import { DSOChangeAnalyzer } from './dso-change-analyzer.service'; +import { FindListOptions } from './find-list-options.model'; import { PaginatedList } from './paginated-list.model'; +import { ResponseParsingService } from './parsing.service'; import { RemoteData } from './remote-data'; -import { DeleteRequest, GetRequest, PostRequest, PutRequest } from './request.models'; +import { + DeleteRequest, + GetRequest, + PostRequest, + PutRequest, +} from './request.models'; import { RequestService } from './request.service'; -import { PaginatedSearchOptions } from '../../shared/search/models/paginated-search-options.model'; -import { Bundle } from '../shared/bundle.model'; -import { MetadataMap } from '../shared/metadata.models'; -import { BundleDataService } from './bundle-data.service'; -import { Operation } from 'fast-json-patch'; -import { NoContent } from '../shared/NoContent.model'; -import { GenericConstructor } from '../shared/generic-constructor'; -import { ResponseParsingService } from './parsing.service'; -import { StatusCodeOnlyResponseParsingService } from './status-code-only-response-parsing.service'; -import { sendRequest } from '../shared/request.operators'; import { RestRequest } from './rest-request.model'; -import { FindListOptions } from './find-list-options.model'; -import { ConstructIdEndpoint, IdentifiableDataService } from './base/identifiable-data.service'; -import { PatchData, PatchDataImpl } from './base/patch-data'; -import { DeleteData, DeleteDataImpl } from './base/delete-data'; import { RestRequestMethod } from './rest-request-method'; -import { CreateData, CreateDataImpl } from './base/create-data'; -import { RequestParam } from '../cache/models/request-param.model'; -import { dataService } from './base/data-service.decorator'; +import { StatusCodeOnlyResponseParsingService } from './status-code-only-response-parsing.service'; /** * An abstract service for CRUD operations on Items @@ -140,7 +167,7 @@ export abstract class BaseItemDataService extends IdentifiableDataService return new PostRequest(this.requestService.generateRequestId(), endpointURL, collectionHref, options); }), sendRequest(this.requestService), - switchMap((request: RestRequest) => this.rdbService.buildFromRequestUUID(request.uuid)) + switchMap((request: RestRequest) => this.rdbService.buildFromRequestUUID(request.uuid)), ); } @@ -152,7 +179,7 @@ export abstract class BaseItemDataService extends IdentifiableDataService public setWithDrawn(item: Item, withdrawn: boolean): Observable> { const patchOperation = { - op: 'replace', path: '/withdrawn', value: withdrawn + op: 'replace', path: '/withdrawn', value: withdrawn, } as Operation; this.requestService.removeByHrefSubstring('/discover'); @@ -166,7 +193,7 @@ export abstract class BaseItemDataService extends IdentifiableDataService */ public setDiscoverable(item: Item, discoverable: boolean): Observable> { const patchOperation = { - op: 'replace', path: '/discoverable', value: discoverable + op: 'replace', path: '/discoverable', value: discoverable, } as Operation; this.requestService.removeByHrefSubstring('/discover'); @@ -180,7 +207,7 @@ export abstract class BaseItemDataService extends IdentifiableDataService */ public getBundlesEndpoint(itemId: string): Observable { return this.halService.getEndpoint(this.linkPath).pipe( - switchMap((url: string) => this.halService.getEndpoint('bundles', `${url}/${itemId}`)) + switchMap((url: string) => this.halService.getEndpoint('bundles', `${url}/${itemId}`)), ); } @@ -191,10 +218,10 @@ export abstract class BaseItemDataService extends IdentifiableDataService */ public getBundles(itemId: string, searchOptions?: PaginatedSearchOptions): Observable>> { const hrefObs = this.getBundlesEndpoint(itemId).pipe( - map((href) => searchOptions ? searchOptions.toRestUrl(href) : href) + map((href) => searchOptions ? searchOptions.toRestUrl(href) : href), ); hrefObs.pipe( - take(1) + take(1), ).subscribe((href) => { const request = new GetRequest(this.requestService.generateRequestId(), href); this.requestService.send(request); @@ -215,11 +242,11 @@ export abstract class BaseItemDataService extends IdentifiableDataService const bundleJson = { name: bundleName, - metadata: metadata ? metadata : {} + metadata: metadata ? metadata : {}, }; hrefObs.pipe( - take(1) + take(1), ).subscribe((href) => { const options: HttpOptions = Object.create({}); let headers = new HttpHeaders(); @@ -238,7 +265,7 @@ export abstract class BaseItemDataService extends IdentifiableDataService */ public getIdentifiersEndpoint(itemId: string): Observable { return this.halService.getEndpoint(this.linkPath).pipe( - switchMap((url: string) => this.halService.getEndpoint('identifiers', `${url}/${itemId}`)) + switchMap((url: string) => this.halService.getEndpoint('identifiers', `${url}/${itemId}`)), ); } @@ -249,7 +276,7 @@ export abstract class BaseItemDataService extends IdentifiableDataService public getMoveItemEndpoint(itemId: string, inheritPolicies: boolean): Observable { return this.halService.getEndpoint(this.linkPath).pipe( map((endpoint: string) => this.getIDHref(endpoint, itemId)), - map((endpoint: string) => `${endpoint}/owningCollection?inheritPolicies=${inheritPolicies}`) + map((endpoint: string) => `${endpoint}/owningCollection?inheritPolicies=${inheritPolicies}`), ); } @@ -275,10 +302,10 @@ export abstract class BaseItemDataService extends IdentifiableDataService // TODO: for now, the move Item endpoint returns a malformed collection -- only look at the status code getResponseParser(): GenericConstructor { return StatusCodeOnlyResponseParsingService; - } + }, }); return request; - }) + }), ).subscribe((request) => { this.requestService.send(request); }); @@ -305,7 +332,7 @@ export abstract class BaseItemDataService extends IdentifiableDataService map((href: string) => { const request = new PostRequest(requestId, href, externalSourceEntry._links.self.href, options); this.requestService.send(request); - }) + }), ).subscribe(); return this.rdbService.buildFromRequestUUID(requestId); @@ -317,7 +344,7 @@ export abstract class BaseItemDataService extends IdentifiableDataService */ public getBitstreamsEndpoint(itemId: string): Observable { return this.halService.getEndpoint(this.linkPath).pipe( - switchMap((url: string) => this.halService.getEndpoint('bitstreams', `${url}/${itemId}`)) + switchMap((url: string) => this.halService.getEndpoint('bitstreams', `${url}/${itemId}`)), ); } @@ -403,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-request-data.service.spec.ts b/src/app/core/data/item-request-data.service.spec.ts index a5d18725109..68577ae6e26 100644 --- a/src/app/core/data/item-request-data.service.spec.ts +++ b/src/app/core/data/item-request-data.service.spec.ts @@ -1,12 +1,13 @@ -import { ItemRequestDataService } from './item-request-data.service'; -import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { RequestService } from './request.service'; -import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { of as observableOf } from 'rxjs'; + +import { RequestCopyEmail } from '../../request-copy/email-request-copy/request-copy-email.model'; import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; import { ItemRequest } from '../shared/item-request.model'; +import { ItemRequestDataService } from './item-request-data.service'; import { PostRequest } from './request.models'; -import { RequestCopyEmail } from '../../request-copy/email-request-copy/request-copy-email.model'; +import { RequestService } from './request.service'; import { RestRequestMethod } from './rest-request-method'; describe('ItemRequestDataService', () => { diff --git a/src/app/core/data/item-request-data.service.ts b/src/app/core/data/item-request-data.service.ts index ff6025f7ac8..5c85ed1471d 100644 --- a/src/app/core/data/item-request-data.service.ts +++ b/src/app/core/data/item-request-data.service.ts @@ -1,20 +1,32 @@ +import { HttpHeaders } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; -import { distinctUntilChanged, filter, find, map } from 'rxjs/operators'; +import { + distinctUntilChanged, + filter, + find, + map, +} from 'rxjs/operators'; + +import { RequestCopyEmail } from '../../request-copy/email-request-copy/request-copy-email.model'; +import { + hasValue, + isNotEmpty, +} from '../../shared/empty.util'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; -import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { getFirstCompletedRemoteData } from '../shared/operators'; -import { RemoteData } from './remote-data'; -import { PostRequest, PutRequest } from './request.models'; -import { RequestService } from './request.service'; -import { ItemRequest } from '../shared/item-request.model'; -import { hasValue, isNotEmpty } from '../../shared/empty.util'; import { ObjectCacheService } from '../cache/object-cache.service'; -import { HttpHeaders } from '@angular/common/http'; -import { RequestCopyEmail } from '../../request-copy/email-request-copy/request-copy-email.model'; import { HttpOptions } from '../dspace-rest/dspace-rest.service'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { ItemRequest } from '../shared/item-request.model'; +import { getFirstCompletedRemoteData } from '../shared/operators'; import { sendRequest } from '../shared/request.operators'; import { IdentifiableDataService } from './base/identifiable-data.service'; +import { RemoteData } from './remote-data'; +import { + PostRequest, + PutRequest, +} from './request.models'; +import { RequestService } from './request.service'; /** * A service responsible for fetching/sending data from/to the REST API on the itemrequests endpoint @@ -60,11 +72,11 @@ export class ItemRequestDataService extends IdentifiableDataService map((href: string) => { const request = new PostRequest(requestId, href, itemRequest); this.requestService.send(request); - }) + }), ).subscribe(); return this.rdbService.buildFromRequestUUID(requestId).pipe( - getFirstCompletedRemoteData() + getFirstCompletedRemoteData(), ); } diff --git a/src/app/core/data/item-template-data.service.spec.ts b/src/app/core/data/item-template-data.service.spec.ts index 16cf0dbd99d..27db819861c 100644 --- a/src/app/core/data/item-template-data.service.spec.ts +++ b/src/app/core/data/item-template-data.service.spec.ts @@ -1,22 +1,26 @@ -import { ItemTemplateDataService } from './item-template-data.service'; -import { RestResponse } from '../cache/response.models'; -import { RequestService } from './request.service'; -import { Observable, of as observableOf } from 'rxjs'; -import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { Store } from '@ngrx/store'; -import { BrowseService } from '../browse/browse.service'; import { cold } from 'jasmine-marbles'; -import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { + Observable, + of as observableOf, +} from 'rxjs'; + import { NotificationsService } from '../../shared/notifications/notifications.service'; -import { CollectionDataService } from './collection-data.service'; -import { RestRequestMethod } from './rest-request-method'; -import { Item } from '../shared/item.model'; -import { RestRequest } from './rest-request.model'; +import { BrowseService } from '../browse/browse.service'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { RestResponse } from '../cache/response.models'; import { CoreState } from '../core-state.model'; -import { RequestEntry } from './request-entry.model'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { Item } from '../shared/item.model'; import { testCreateDataImplementation } from './base/create-data.spec'; -import { testPatchDataImplementation } from './base/patch-data.spec'; import { testDeleteDataImplementation } from './base/delete-data.spec'; +import { testPatchDataImplementation } from './base/patch-data.spec'; +import { CollectionDataService } from './collection-data.service'; +import { ItemTemplateDataService } from './item-template-data.service'; +import { RequestService } from './request.service'; +import { RequestEntry } from './request-entry.model'; +import { RestRequest } from './rest-request.model'; +import { RestRequestMethod } from './rest-request-method'; import createSpyObj = jasmine.createSpyObj; describe('ItemTemplateDataService', () => { @@ -46,7 +50,7 @@ describe('ItemTemplateDataService', () => { }, commit(method?: RestRequestMethod) { // Do nothing - } + }, } as RequestService; const rdbService = {} as RemoteDataBuildService; const store = {} as Store; @@ -62,18 +66,18 @@ describe('ItemTemplateDataService', () => { const halEndpointService = { getEndpoint(linkPath: string): Observable { return cold('a', { a: itemEndpoint }); - } + }, } as HALEndpointService; const notificationsService = {} as NotificationsService; const comparator = { diff(first, second) { return [{}]; - } + }, } as any; const collectionService = { getIDHrefObs(id): Observable { return observableOf(collectionEndpoint); - } + }, } as CollectionDataService; function initTestService() { diff --git a/src/app/core/data/item-template-data.service.ts b/src/app/core/data/item-template-data.service.ts index 634c966dbaa..f89a297fad9 100644 --- a/src/app/core/data/item-template-data.service.ts +++ b/src/app/core/data/item-template-data.service.ts @@ -1,22 +1,23 @@ /* eslint-disable max-classes-per-file */ import { Injectable } from '@angular/core'; -import { BaseItemDataService } from './item-data.service'; -import { Item } from '../shared/item.model'; -import { RemoteData } from './remote-data'; import { Observable } from 'rxjs'; -import { DSOChangeAnalyzer } from './dso-change-analyzer.service'; -import { RequestService } from './request.service'; +import { switchMap } from 'rxjs/operators'; + +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; +import { BrowseService } from '../browse/browse.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { ObjectCacheService } from '../cache/object-cache.service'; import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { NotificationsService } from '../../shared/notifications/notifications.service'; -import { BrowseService } from '../browse/browse.service'; -import { CollectionDataService } from './collection-data.service'; -import { switchMap } from 'rxjs/operators'; -import { BundleDataService } from './bundle-data.service'; -import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; -import { IdentifiableDataService } from './base/identifiable-data.service'; +import { Item } from '../shared/item.model'; import { CreateDataImpl } from './base/create-data'; +import { IdentifiableDataService } from './base/identifiable-data.service'; +import { BundleDataService } from './bundle-data.service'; +import { CollectionDataService } from './collection-data.service'; +import { DSOChangeAnalyzer } from './dso-change-analyzer.service'; +import { BaseItemDataService } from './item-data.service'; +import { RemoteData } from './remote-data'; +import { RequestService } from './request.service'; /** * Data service for interacting with Item templates via their Collection @@ -63,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.spec.ts b/src/app/core/data/lookup-relation.service.spec.ts index 58598b9870d..93b4ed66968 100644 --- a/src/app/core/data/lookup-relation.service.spec.ts +++ b/src/app/core/data/lookup-relation.service.spec.ts @@ -1,18 +1,22 @@ -import { LookupRelationService } from './lookup-relation.service'; -import { ExternalSourceDataService } from './external-source-data.service'; -import { SearchService } from '../shared/search/search.service'; +import { of as observableOf } from 'rxjs'; +import { + skip, + take, +} from 'rxjs/operators'; + +import { RelationshipOptions } from '../../shared/form/builder/models/relationship-options.model'; import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; -import { createPaginatedList } from '../../shared/testing/utils.test'; -import { buildPaginatedList } from './paginated-list.model'; -import { PageInfo } from '../shared/page-info.model'; import { PaginatedSearchOptions } from '../../shared/search/models/paginated-search-options.model'; -import { RelationshipOptions } from '../../shared/form/builder/models/relationship-options.model'; import { SearchResult } from '../../shared/search/models/search-result.model'; -import { Item } from '../shared/item.model'; -import { skip, take } from 'rxjs/operators'; +import { createPaginatedList } from '../../shared/testing/utils.test'; import { ExternalSource } from '../shared/external-source.model'; +import { Item } from '../shared/item.model'; +import { PageInfo } from '../shared/page-info.model'; +import { SearchService } from '../shared/search/search.service'; +import { ExternalSourceDataService } from './external-source-data.service'; +import { LookupRelationService } from './lookup-relation.service'; +import { buildPaginatedList } from './paginated-list.model'; import { RequestService } from './request.service'; -import { of as observableOf } from 'rxjs'; describe('LookupRelationService', () => { let service: LookupRelationService; @@ -24,20 +28,20 @@ describe('LookupRelationService', () => { const optionsWithQuery = new PaginatedSearchOptions({ query: 'test-query' }); const relationship = Object.assign(new RelationshipOptions(), { filter: 'test-filter', - configuration: 'test-configuration' + configuration: 'test-configuration', }); const localResults = [ Object.assign(new SearchResult(), { indexableObject: Object.assign(new Item(), { uuid: 'test-item-uuid', - handle: 'test-item-handle' - }) - }) + handle: 'test-item-handle', + }), + }), ]; const externalSource = Object.assign(new ExternalSource(), { id: 'orcidV2', name: 'orcidV2', - hierarchical: false + hierarchical: false, }); const searchServiceEndpoint = 'http://test-rest.com/server/api/core/search'; @@ -47,12 +51,12 @@ describe('LookupRelationService', () => { elementsPerPage: 1, totalElements: totalExternal, totalPages: totalExternal, - currentPage: 1 - }), [{}])) + currentPage: 1, + }), [{}])), }); searchService = jasmine.createSpyObj('searchService', { search: createSuccessfulRemoteDataObject$(createPaginatedList(localResults)), - getEndpoint: observableOf(searchServiceEndpoint) + getEndpoint: observableOf(searchServiceEndpoint), }); requestService = jasmine.createSpyObj('requestService', ['removeByHrefSubstring']); service = new LookupRelationService(externalSourceService, searchService, requestService); @@ -77,7 +81,7 @@ describe('LookupRelationService', () => { it('should set the searchConfig to contain a fixedFilter and configuration', () => { expect(service.searchConfig).toEqual(Object.assign(new PaginatedSearchOptions({}), optionsWithQuery, - { fixedFilter: relationship.filter, configuration: relationship.searchConfiguration } + { fixedFilter: relationship.filter, configuration: relationship.searchConfiguration }, )); }); }); diff --git a/src/app/core/data/lookup-relation.service.ts b/src/app/core/data/lookup-relation.service.ts index 7a6bc2358b5..55b68afa652 100644 --- a/src/app/core/data/lookup-relation.service.ts +++ b/src/app/core/data/lookup-relation.service.ts @@ -1,25 +1,40 @@ -import { ExternalSourceDataService } from './external-source-data.service'; -import { SearchService } from '../shared/search/search.service'; -import { concat, distinctUntilChanged, map, multicast, startWith, take, takeWhile } from 'rxjs/operators'; +import { Injectable } from '@angular/core'; +import { + Observable, + ReplaySubject, +} from 'rxjs'; +import { + concat, + distinctUntilChanged, + map, + multicast, + startWith, + take, + takeWhile, +} from 'rxjs/operators'; + +import { RelationshipOptions } from '../../shared/form/builder/models/relationship-options.model'; +import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; import { PaginatedSearchOptions } from '../../shared/search/models/paginated-search-options.model'; -import { Observable, ReplaySubject } from 'rxjs'; -import { RemoteData } from './remote-data'; -import { PaginatedList } from './paginated-list.model'; import { SearchResult } from '../../shared/search/models/search-result.model'; import { DSpaceObject } from '../shared/dspace-object.model'; -import { RelationshipOptions } from '../../shared/form/builder/models/relationship-options.model'; -import { Item } from '../shared/item.model'; -import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; -import { getAllSucceededRemoteData, getRemoteDataPayload } from '../shared/operators'; -import { Injectable } from '@angular/core'; import { ExternalSource } from '../shared/external-source.model'; import { ExternalSourceEntry } from '../shared/external-source-entry.model'; +import { Item } from '../shared/item.model'; +import { + getAllSucceededRemoteData, + getRemoteDataPayload, +} from '../shared/operators'; +import { SearchService } from '../shared/search/search.service'; +import { ExternalSourceDataService } from './external-source-data.service'; +import { PaginatedList } from './paginated-list.model'; +import { RemoteData } from './remote-data'; 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 @@ -31,7 +46,7 @@ export class LookupRelationService { */ private singleResultOptions = Object.assign(new PaginationComponentOptions(), { id: 'single-result-options', - pageSize: 1 + pageSize: 1, }); constructor(protected externalSourceService: ExternalSourceDataService, @@ -47,7 +62,7 @@ export class LookupRelationService { */ getLocalResults(relationship: RelationshipOptions, searchOptions: PaginatedSearchOptions, setSearchConfig = true): Observable>>> { const newConfig = Object.assign(new PaginatedSearchOptions({}), searchOptions, - { fixedFilter: relationship.filter, configuration: relationship.searchConfiguration } + { fixedFilter: relationship.filter, configuration: relationship.searchConfiguration }, ); if (setSearchConfig) { this.searchConfig = newConfig; @@ -59,8 +74,8 @@ export class LookupRelationService { () => new ReplaySubject(1), (subject) => subject.pipe( takeWhile((rd: RemoteData>>) => rd.isLoading), - concat(subject.pipe(take(1))) - ) + concat(subject.pipe(take(1))), + ), ) as any , ) as Observable>>>; @@ -76,7 +91,7 @@ export class LookupRelationService { getAllSucceededRemoteData(), getRemoteDataPayload(), map((results: PaginatedList>) => results.totalElements), - startWith(0) + startWith(0), ); } @@ -91,7 +106,7 @@ export class LookupRelationService { getRemoteDataPayload(), map((results: PaginatedList) => results.totalElements), startWith(0), - distinctUntilChanged() + distinctUntilChanged(), ); } diff --git a/src/app/core/data/metadata-field-data.service.spec.ts b/src/app/core/data/metadata-field-data.service.spec.ts index 1ce078f5d53..8d65038060d 100644 --- a/src/app/core/data/metadata-field-data.service.spec.ts +++ b/src/app/core/data/metadata-field-data.service.spec.ts @@ -1,20 +1,21 @@ -import { RequestService } from './request.service'; -import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { NotificationsService } from '../../shared/notifications/notifications.service'; import { of as observableOf } from 'rxjs'; -import { RestResponse } from '../cache/response.models'; + +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub'; -import { MetadataFieldDataService } from './metadata-field-data.service'; -import { MetadataSchema } from '../metadata/metadata-schema.model'; +import { createPaginatedList } from '../../shared/testing/utils.test'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; -import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; import { RequestParam } from '../cache/models/request-param.model'; -import { FindListOptions } from './find-list-options.model'; -import { createPaginatedList } from '../../shared/testing/utils.test'; +import { RestResponse } from '../cache/response.models'; +import { MetadataSchema } from '../metadata/metadata-schema.model'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; import { testCreateDataImplementation } from './base/create-data.spec'; -import { testSearchDataImplementation } from './base/search-data.spec'; -import { testPutDataImplementation } from './base/put-data.spec'; import { testDeleteDataImplementation } from './base/delete-data.spec'; +import { testPutDataImplementation } from './base/put-data.spec'; +import { testSearchDataImplementation } from './base/search-data.spec'; +import { FindListOptions } from './find-list-options.model'; +import { MetadataFieldDataService } from './metadata-field-data.service'; +import { RequestService } from './request.service'; describe('MetadataFieldDataService', () => { let metadataFieldService: MetadataFieldDataService; @@ -31,8 +32,8 @@ describe('MetadataFieldDataService', () => { prefix: 'dc', namespace: 'namespace', _links: { - self: { href: 'selflink' } - } + self: { href: 'selflink' }, + }, }); requestService = jasmine.createSpyObj('requestService', { generateRequestId: '34cfed7c-f597-49ef-9cbe-ea351f0023c2', @@ -73,7 +74,7 @@ describe('MetadataFieldDataService', () => { it('should call searchBy with the correct arguments', () => { metadataFieldService.findBySchema(schema); const expectedOptions = Object.assign(new FindListOptions(), { - searchParams: [new RequestParam('schema', schema.prefix)] + searchParams: [new RequestParam('schema', schema.prefix)], }); expect(metadataFieldService.searchBy).toHaveBeenCalledWith('bySchema', expectedOptions, true, true); }); diff --git a/src/app/core/data/metadata-field-data.service.ts b/src/app/core/data/metadata-field-data.service.ts index d05e3533d32..aa79ef517ee 100644 --- a/src/app/core/data/metadata-field-data.service.ts +++ b/src/app/core/data/metadata-field-data.service.ts @@ -1,33 +1,43 @@ import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; +import { take } from 'rxjs/operators'; + import { hasValue } from '../../shared/empty.util'; -import { PaginatedList } from './paginated-list.model'; -import { RemoteData } from './remote-data'; -import { RequestService } from './request.service'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; -import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { METADATA_FIELD } from '../metadata/metadata-field.resource-type'; +import { RequestParam } from '../cache/models/request-param.model'; +import { ObjectCacheService } from '../cache/object-cache.service'; import { MetadataField } from '../metadata/metadata-field.model'; import { MetadataSchema } from '../metadata/metadata-schema.model'; -import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; -import { Observable } from 'rxjs'; -import { take } from 'rxjs/operators'; -import { RequestParam } from '../cache/models/request-param.model'; -import { FindListOptions } from './find-list-options.model'; -import { SearchData, SearchDataImpl } from './base/search-data'; -import { PutData, PutDataImpl } from './base/put-data'; -import { CreateData, CreateDataImpl } from './base/create-data'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; import { NoContent } from '../shared/NoContent.model'; -import { NotificationsService } from '../../shared/notifications/notifications.service'; -import { ObjectCacheService } from '../cache/object-cache.service'; -import { DeleteData, DeleteDataImpl } from './base/delete-data'; +import { + CreateData, + CreateDataImpl, +} from './base/create-data'; +import { + DeleteData, + DeleteDataImpl, +} from './base/delete-data'; import { IdentifiableDataService } from './base/identifiable-data.service'; -import { dataService } from './base/data-service.decorator'; +import { + PutData, + PutDataImpl, +} from './base/put-data'; +import { + SearchData, + SearchDataImpl, +} from './base/search-data'; +import { FindListOptions } from './find-list-options.model'; +import { PaginatedList } from './paginated-list.model'; +import { RemoteData } from './remote-data'; +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.spec.ts b/src/app/core/data/metadata-schema-data.service.spec.ts index 1bcf4e1104e..02fbc016e7f 100644 --- a/src/app/core/data/metadata-schema-data.service.spec.ts +++ b/src/app/core/data/metadata-schema-data.service.spec.ts @@ -1,16 +1,20 @@ -import { RequestService } from './request.service'; -import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { NotificationsService } from '../../shared/notifications/notifications.service'; -import { MetadataSchemaDataService } from './metadata-schema-data.service'; import { of as observableOf } from 'rxjs'; -import { RestResponse } from '../cache/response.models'; + +import { getMockRemoteDataBuildService } from '../../shared/mocks/remote-data-build.service.mock'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub'; -import { MetadataSchema } from '../metadata/metadata-schema.model'; -import { CreateRequest, PutRequest } from './request.models'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; -import { getMockRemoteDataBuildService } from '../../shared/mocks/remote-data-build.service.mock'; -import { testFindAllDataImplementation } from './base/find-all-data.spec'; +import { RestResponse } from '../cache/response.models'; +import { MetadataSchema } from '../metadata/metadata-schema.model'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; import { testDeleteDataImplementation } from './base/delete-data.spec'; +import { testFindAllDataImplementation } from './base/find-all-data.spec'; +import { MetadataSchemaDataService } from './metadata-schema-data.service'; +import { + CreateRequest, + PutRequest, +} from './request.models'; +import { RequestService } from './request.service'; describe('MetadataSchemaDataService', () => { let metadataSchemaService: MetadataSchemaDataService; @@ -61,8 +65,8 @@ describe('MetadataSchemaDataService', () => { prefix: 'dc', namespace: 'namespace', _links: { - self: { href: 'selflink' } - } + self: { href: 'selflink' }, + }, }); }); @@ -78,7 +82,7 @@ describe('MetadataSchemaDataService', () => { describe('called with an existing metadata schema', () => { beforeEach(() => { schema = Object.assign(schema, { - id: 'id-of-existing-schema' + id: 'id-of-existing-schema', }); }); diff --git a/src/app/core/data/metadata-schema-data.service.ts b/src/app/core/data/metadata-schema-data.service.ts index 6bd633b8c64..e893cb5404b 100644 --- a/src/app/core/data/metadata-schema-data.service.ts +++ b/src/app/core/data/metadata-schema-data.service.ts @@ -1,31 +1,41 @@ import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; +import { tap } from 'rxjs/operators'; + +import { hasValue } from '../../shared/empty.util'; import { NotificationsService } from '../../shared/notifications/notifications.service'; +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 { RequestService } from './request.service'; -import { Observable } from 'rxjs'; -import { hasValue } from '../../shared/empty.util'; -import { tap } from 'rxjs/operators'; -import { RemoteData } from './remote-data'; -import { PutData, PutDataImpl } from './base/put-data'; -import { CreateData, CreateDataImpl } from './base/create-data'; import { NoContent } from '../shared/NoContent.model'; -import { FindAllData, FindAllDataImpl } from './base/find-all-data'; +import { + CreateData, + CreateDataImpl, +} from './base/create-data'; +import { + DeleteData, + DeleteDataImpl, +} from './base/delete-data'; +import { + FindAllData, + FindAllDataImpl, +} from './base/find-all-data'; +import { IdentifiableDataService } from './base/identifiable-data.service'; +import { + PutData, + PutDataImpl, +} from './base/put-data'; import { FindListOptions } from './find-list-options.model'; -import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; import { PaginatedList } from './paginated-list.model'; -import { IdentifiableDataService } from './base/identifiable-data.service'; -import { DeleteData, DeleteDataImpl } from './base/delete-data'; -import { dataService } from './base/data-service.decorator'; +import { RemoteData } from './remote-data'; +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 e46e319149c..f824be7f56b 100644 --- a/src/app/core/data/mydspace-response-parsing.service.ts +++ b/src/app/core/data/mydspace-response-parsing.service.ts @@ -1,21 +1,25 @@ import { Injectable } from '@angular/core'; + +import { hasValue } from '../../shared/empty.util'; +import { SearchObjects } from '../../shared/search/models/search-objects.model'; import { ParsedResponse } from '../cache/response.models'; import { DSpaceSerializer } from '../dspace-rest/dspace.serializer'; import { RawRestResponse } from '../dspace-rest/raw-rest-response.model'; -import { hasValue } from '../../shared/empty.util'; -import { SearchObjects } from '../../shared/search/models/search-objects.model'; -import { MetadataMap, MetadataValue } from '../shared/metadata.models'; +import { + MetadataMap, + MetadataValue, +} from '../shared/metadata.models'; 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 const emptyPayload = { _embedded: { - objects: [] - } + objects: [], + }, }; const payload = data.payload._embedded.searchResult || emptyPayload; const hitHighlights: MetadataMap[] = payload._embedded.objects @@ -26,7 +30,7 @@ export class MyDSpaceResponseParsingService extends DspaceRestResponseParsingSer for (const key of Object.keys(hhObject)) { const value: MetadataValue = Object.assign(new MetadataValue(), { value: hhObject[key].join('...'), - language: null + language: null, }); mdMap[key] = [value]; } @@ -46,7 +50,7 @@ export class MyDSpaceResponseParsingService extends DspaceRestResponseParsingSer .map((object, index) => Object.assign({}, object, { indexableObject: dsoSelfLinks[index], hitHighlights: hitHighlights[index], - _embedded: this.filterEmbeddedObjects(object) + _embedded: this.filterEmbeddedObjects(object), })); payload.objects = objects; const deserialized: any = new DSpaceSerializer(SearchObjects).deserialize(payload); @@ -65,8 +69,8 @@ export class MyDSpaceResponseParsingService extends DspaceRestResponseParsingSer .reduce((obj, key) => { obj[key] = object._embedded.indexableObject._embedded[key]; return obj; - }, {}) - }) + }, {}), + }), }); } else { return object; diff --git a/src/app/core/data/notify-services-status-data.service.spec.ts b/src/app/core/data/notify-services-status-data.service.spec.ts new file mode 100644 index 00000000000..e3368435052 --- /dev/null +++ b/src/app/core/data/notify-services-status-data.service.spec.ts @@ -0,0 +1,88 @@ +import { + cold, + getTestScheduler, +} from 'jasmine-marbles'; +import { of } from 'rxjs'; +import { TestScheduler } from 'rxjs/testing'; + +import { NotifyRequestsStatus } from '../../item-page/simple/notify-requests-status/notify-requests-status.model'; +import { + createSuccessfulRemoteDataObject, + createSuccessfulRemoteDataObject$, +} from '../../shared/remote-data.utils'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { ObjectCacheService } from '../cache/object-cache.service'; +import { RestResponse } from '../cache/response.models'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { NotifyRequestsStatusDataService } from './notify-services-status-data.service'; +import { RemoteData } from './remote-data'; +import { RequestService } from './request.service'; +import { RequestEntry } from './request-entry.model'; +import { RequestEntryState } from './request-entry-state.model'; + +describe('NotifyRequestsStatusDataService test', () => { + let scheduler: TestScheduler; + let service: NotifyRequestsStatusDataService; + let requestService: RequestService; + let rdbService: RemoteDataBuildService; + let objectCache: ObjectCacheService; + let halService: HALEndpointService; + let responseCacheEntry: RequestEntry; + + const endpointURL = `https://rest.api/rest/api/suggestiontargets`; + const requestUUID = '8b3c613a-5a4b-438b-9686-be1d5b4a1c5a'; + + const remoteDataMocks = { + Success: new RemoteData(null, null, null, RequestEntryState.Success, null, null, 200), + }; + + function initTestService() { + return new NotifyRequestsStatusDataService( + requestService, + rdbService, + objectCache, + halService, + ); + } + + beforeEach(() => { + scheduler = getTestScheduler(); + + objectCache = {} as ObjectCacheService; + responseCacheEntry = new RequestEntry(); + responseCacheEntry.request = { href: 'https://rest.api/' } as any; + responseCacheEntry.response = new RestResponse(true, 200, 'Success'); + + requestService = jasmine.createSpyObj('requestService', { + generateRequestId: requestUUID, + send: true, + removeByHrefSubstring: {}, + getByHref: of(responseCacheEntry), + getByUUID: of(responseCacheEntry), + }); + + halService = jasmine.createSpyObj('halService', { + getEndpoint: of(endpointURL), + }); + + rdbService = jasmine.createSpyObj('rdbService', { + buildSingle: createSuccessfulRemoteDataObject$({}, 500), + buildList: cold('a', { a: remoteDataMocks.Success }), + buildFromHref: createSuccessfulRemoteDataObject$({ test: 'test' }), + }); + + + service = initTestService(); + }); + + describe('getNotifyRequestsStatus', () => { + it('should get notify status', (done) => { + service.getNotifyRequestsStatus(requestUUID).subscribe((status) => { + expect(halService.getEndpoint).toHaveBeenCalled(); + expect(requestService.generateRequestId).toHaveBeenCalled(); + expect(status).toEqual(createSuccessfulRemoteDataObject({ test: 'test' } as unknown as NotifyRequestsStatus)); + done(); + }); + }); + }); +}); diff --git a/src/app/core/data/notify-services-status-data.service.ts b/src/app/core/data/notify-services-status-data.service.ts new file mode 100644 index 00000000000..deeaad967a7 --- /dev/null +++ b/src/app/core/data/notify-services-status-data.service.ts @@ -0,0 +1,46 @@ +import { Injectable } from '@angular/core'; +import { + map, + Observable, + take, +} from 'rxjs'; + +import { NotifyRequestsStatus } from '../../item-page/simple/notify-requests-status/notify-requests-status.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 { IdentifiableDataService } from './base/identifiable-data.service'; +import { RemoteData } from './remote-data'; +import { GetRequest } from './request.models'; +import { RequestService } from './request.service'; + +@Injectable({ providedIn: 'root' }) +export class NotifyRequestsStatusDataService extends IdentifiableDataService { + + constructor( + protected requestService: RequestService, + protected rdbService: RemoteDataBuildService, + protected objectCache: ObjectCacheService, + protected halService: HALEndpointService, + ) { + super('notifyrequests', requestService, rdbService, objectCache, halService); + } + + /** + * Retrieves the status of notify requests for a specific item. + * @param itemUuid The UUID of the item. + * @returns An Observable that emits the remote data containing the notify requests status. + */ + getNotifyRequestsStatus(itemUuid: string): Observable> { + const href$ = this.getEndpoint().pipe( + map((url: string) => url + '/' + itemUuid ), + ); + + href$.pipe(take(1)).subscribe((url: string) => { + const request = new GetRequest(this.requestService.generateRequestId(), url); + this.requestService.send(request, true); + }); + + return this.rdbService.buildFromHref(href$); + } +} diff --git a/src/app/core/data/object-updates/field-update.model.ts b/src/app/core/data/object-updates/field-update.model.ts index 47b67824713..71ae3bb68f6 100644 --- a/src/app/core/data/object-updates/field-update.model.ts +++ b/src/app/core/data/object-updates/field-update.model.ts @@ -1,5 +1,5 @@ -import { Identifiable } from './identifiable.model'; import { FieldChangeType } from './field-change-type.model'; +import { Identifiable } from './identifiable.model'; /** * The state of a single field update diff --git a/src/app/core/data/object-updates/object-updates.actions.ts b/src/app/core/data/object-updates/object-updates.actions.ts index 615dedbaf9d..8a95a7827ce 100644 --- a/src/app/core/data/object-updates/object-updates.actions.ts +++ b/src/app/core/data/object-updates/object-updates.actions.ts @@ -1,11 +1,12 @@ /* eslint-disable max-classes-per-file */ -import { type } from '../../../shared/ngrx/type'; import { Action } from '@ngrx/store'; + +import { type } from '../../../shared/ngrx/type'; import { INotification } from '../../../shared/notifications/models/notification.model'; -import { PatchOperationService } from './patch-operation-service/patch-operation.service'; import { GenericConstructor } from '../../shared/generic-constructor'; -import { Identifiable } from './identifiable.model'; import { FieldChangeType } from './field-change-type.model'; +import { Identifiable } from './identifiable.model'; +import { PatchOperationService } from './patch-operation-service/patch-operation.service'; /** * The list of ObjectUpdatesAction type definitions @@ -20,7 +21,7 @@ export const ObjectUpdatesActionTypes = { REINSTATE: type('dspace/core/cache/object-updates/REINSTATE'), REMOVE: type('dspace/core/cache/object-updates/REMOVE'), REMOVE_ALL: type('dspace/core/cache/object-updates/REMOVE_ALL'), - REMOVE_FIELD: type('dspace/core/cache/object-updates/REMOVE_FIELD') + REMOVE_FIELD: type('dspace/core/cache/object-updates/REMOVE_FIELD'), }; @@ -49,7 +50,7 @@ export class InitializeFieldsAction implements Action { url: string, fields: Identifiable[], lastModified: Date, - patchOperationService?: GenericConstructor + patchOperationService?: GenericConstructor, ) { this.payload = { url, fields, lastModified, patchOperationService }; } @@ -113,7 +114,7 @@ export class SelectVirtualMetadataAction implements Action { uuid: string, select: boolean, ) { - this.payload = { url, source, uuid, select: select}; + this.payload = { url, source, uuid, select: select }; } } @@ -193,7 +194,7 @@ export class DiscardObjectUpdatesAction implements Action { constructor( url: string, notification: INotification, - discardAll = false + discardAll = false, ) { this.payload = { url, notification, discardAll }; } @@ -215,7 +216,7 @@ export class ReinstateObjectUpdatesAction implements Action { * the unique url of the page for which the changes should be reinstated */ constructor( - url: string + url: string, ) { this.payload = { url }; } @@ -237,7 +238,7 @@ export class RemoveObjectUpdatesAction implements Action { * the unique url of the page for which the changes should be removed */ constructor( - url: string + url: string, ) { this.payload = { url }; } @@ -269,7 +270,7 @@ export class RemoveFieldUpdateAction implements Action { */ constructor( url: string, - uuid: string + uuid: string, ) { this.payload = { url, uuid }; } diff --git a/src/app/core/data/object-updates/object-updates.effects.spec.ts b/src/app/core/data/object-updates/object-updates.effects.spec.ts index ffd20a73006..10f37d78cb1 100644 --- a/src/app/core/data/object-updates/object-updates.effects.spec.ts +++ b/src/app/core/data/object-updates/object-updates.effects.spec.ts @@ -1,24 +1,35 @@ -import { TestBed, waitForAsync } from '@angular/core/testing'; -import { Observable, Subject } from 'rxjs'; +import { + TestBed, + waitForAsync, +} from '@angular/core/testing'; import { provideMockActions } from '@ngrx/effects/testing'; -import { cold, hot } from 'jasmine-marbles'; +import { Action } from '@ngrx/store'; +import { + cold, + hot, +} from 'jasmine-marbles'; +import { + Observable, + Subject, +} from 'rxjs'; +import { take } from 'rxjs/operators'; + +import { NoOpAction } from '../../../shared/ngrx/no-op.action'; +import { + INotification, + Notification, +} from '../../../shared/notifications/models/notification.model'; +import { NotificationType } from '../../../shared/notifications/models/notification-type'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; -import { ObjectUpdatesEffects } from './object-updates.effects'; +import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub'; import { DiscardObjectUpdatesAction, ObjectUpdatesAction, ReinstateObjectUpdatesAction, RemoveFieldUpdateAction, - RemoveObjectUpdatesAction + RemoveObjectUpdatesAction, } from './object-updates.actions'; -import { - INotification, - Notification -} from '../../../shared/notifications/models/notification.model'; -import { NotificationType } from '../../../shared/notifications/models/notification-type'; -import { filter } from 'rxjs/operators'; -import { hasValue } from '../../../shared/empty.util'; -import { NoOpAction } from '../../../shared/ngrx/no-op.action'; +import { ObjectUpdatesEffects } from './object-updates.effects'; describe('ObjectUpdatesEffects', () => { let updatesEffects: ObjectUpdatesEffects; @@ -31,13 +42,7 @@ describe('ObjectUpdatesEffects', () => { providers: [ ObjectUpdatesEffects, provideMockActions(() => actions), - { - provide: NotificationsService, - useValue: { - remove: (notification) => { /* empty */ - } - } - }, + { provide: NotificationsService, useClass: NotificationsServiceStub }, ], }); })); @@ -59,7 +64,6 @@ describe('ObjectUpdatesEffects', () => { action = new RemoveObjectUpdatesAction(testURL); }); it('should emit the action from the actionMap\'s value which key matches the action\'s URL', () => { - action = new RemoveObjectUpdatesAction(testURL); actions = hot('--a-', { a: action }); (updatesEffects as any).actionMap$[testURL].subscribe((act) => emittedAction = act); const expected = cold('--b-', { b: undefined }); @@ -81,14 +85,19 @@ describe('ObjectUpdatesEffects', () => { removeAction = new RemoveObjectUpdatesAction(testURL); }); it('should return a RemoveObjectUpdatesAction', () => { - actions = hot('a|', { a: new DiscardObjectUpdatesAction(testURL, infoNotification) }); - updatesEffects.removeAfterDiscardOrReinstateOnUndo$.pipe( - filter(((action) => hasValue(action)))) - .subscribe((t) => { - expect(t).toEqual(removeAction); - } - ) - ; + actions = hot('a', { a: new DiscardObjectUpdatesAction(testURL, infoNotification) }); + + // Because we use Subject and not BehaviourSubject we need to subscribe to it beforehand because it does not + // keep track of the current state + let emittedAction: Action | undefined; + updatesEffects.removeAfterDiscardOrReinstateOnUndo$.subscribe((action: Action | NoOpAction) => { + emittedAction = action; + }); + + // This expect ensures that the mapLastActions$ was processed + expect(updatesEffects.mapLastActions$).toBeObservable(cold('a', { a: undefined })); + + expect(emittedAction).toEqual(removeAction); }); }); @@ -98,12 +107,24 @@ describe('ObjectUpdatesEffects', () => { infoNotification.options.timeOut = 10; }); it('should return an action with type NO_ACTION', () => { - actions = hot('a', { a: new DiscardObjectUpdatesAction(testURL, infoNotification) }); - actions = hot('b', { b: new ReinstateObjectUpdatesAction(testURL) }); - updatesEffects.removeAfterDiscardOrReinstateOnUndo$.subscribe((t) => { - expect(t).toEqual(new NoOpAction()); - } - ); + actions = hot('--(ab)', { + a: new DiscardObjectUpdatesAction(testURL, infoNotification), + b: new ReinstateObjectUpdatesAction(testURL), + }); + + // Because we use Subject and not BehaviourSubject we need to subscribe to it beforehand because it does not + // keep track of the current state + let emittedAction: Action | undefined; + updatesEffects.removeAfterDiscardOrReinstateOnUndo$.pipe( + take(2), + ).subscribe((action: Action | NoOpAction) => { + emittedAction = action; + }); + + // This expect ensures that the mapLastActions$ was processed + expect(updatesEffects.mapLastActions$).toBeObservable(cold('--(ab)', { a: undefined, b: undefined })); + + expect(emittedAction).toEqual(new RemoveObjectUpdatesAction(testURL)); }); }); @@ -113,12 +134,22 @@ describe('ObjectUpdatesEffects', () => { infoNotification.options.timeOut = 10; }); it('should return a RemoveObjectUpdatesAction', () => { - actions = hot('a', { a: new DiscardObjectUpdatesAction(testURL, infoNotification) }); - actions = hot('b', { b: new RemoveFieldUpdateAction(testURL, testUUID) }); + actions = hot('--(ab)', { + a: new DiscardObjectUpdatesAction(testURL, infoNotification), + b: new RemoveFieldUpdateAction(testURL, testUUID), + }); + + // Because we use Subject and not BehaviourSubject we need to subscribe to it beforehand because it does not + // keep track of the current state + let emittedAction: Action | undefined; + updatesEffects.removeAfterDiscardOrReinstateOnUndo$.subscribe((action: Action | NoOpAction) => { + emittedAction = action; + }); + + // This expect ensures that the mapLastActions$ was processed + expect(updatesEffects.mapLastActions$).toBeObservable(cold('--(ab)', { a: undefined, b: undefined })); - updatesEffects.removeAfterDiscardOrReinstateOnUndo$.subscribe((t) => - expect(t).toEqual(new RemoveObjectUpdatesAction(testURL)) - ); + expect(emittedAction).toEqual(new RemoveObjectUpdatesAction(testURL)); }); }); }); diff --git a/src/app/core/data/object-updates/object-updates.effects.ts b/src/app/core/data/object-updates/object-updates.effects.ts index 1dfdc95f23e..5ef86dbbec3 100644 --- a/src/app/core/data/object-updates/object-updates.effects.ts +++ b/src/app/core/data/object-updates/object-updates.effects.ts @@ -1,24 +1,43 @@ import { Injectable } from '@angular/core'; -import { Actions, createEffect, ofType } from '@ngrx/effects'; import { - DiscardObjectUpdatesAction, - ObjectUpdatesAction, - ObjectUpdatesActionTypes, - RemoveAllObjectUpdatesAction, - RemoveObjectUpdatesAction -} from './object-updates.actions'; -import { delay, filter, map, switchMap, take, tap } from 'rxjs/operators'; -import { of as observableOf, race as observableRace, Subject } from 'rxjs'; -import { hasNoValue, hasValue } from '../../../shared/empty.util'; -import { NotificationsService } from '../../../shared/notifications/notifications.service'; + Actions, + createEffect, + ofType, +} from '@ngrx/effects'; +import { Action } from '@ngrx/store'; +import { + of as observableOf, + race as observableRace, + Subject, +} from 'rxjs'; +import { + delay, + filter, + map, + switchMap, + take, + tap, +} from 'rxjs/operators'; + +import { + hasNoValue, + hasValue, +} from '../../../shared/empty.util'; +import { NoOpAction } from '../../../shared/ngrx/no-op.action'; import { INotification } from '../../../shared/notifications/models/notification.model'; import { NotificationsActions, NotificationsActionTypes, - RemoveNotificationAction + RemoveNotificationAction, } from '../../../shared/notifications/notifications.actions'; -import { Action } from '@ngrx/store'; -import { NoOpAction } from '../../../shared/ngrx/no-op.action'; +import { NotificationsService } from '../../../shared/notifications/notifications.service'; +import { + DiscardObjectUpdatesAction, + ObjectUpdatesAction, + ObjectUpdatesActionTypes, + RemoveAllObjectUpdatesAction, + RemoveObjectUpdatesAction, +} from './object-updates.actions'; /** * NGRX effects for ObjectUpdatesActions @@ -52,7 +71,7 @@ export class ObjectUpdatesEffects { /** * Effect that makes sure all last fired ObjectUpdatesActions are stored in the map of this service, with the url as their key */ - mapLastActions$ = createEffect(() => this.actions$ + mapLastActions$ = createEffect(() => this.actions$ .pipe( ofType(...Object.values(ObjectUpdatesActionTypes)), map((action: ObjectUpdatesAction) => { @@ -63,23 +82,23 @@ export class ObjectUpdatesEffects { } this.actionMap$[url].next(action); } - }) + }), ), { dispatch: false }); /** * Effect that makes sure all last fired NotificationActions are stored in the notification map of this service, with the id as their key */ - mapLastNotificationActions$ = createEffect(() => this.actions$ + mapLastNotificationActions$ = createEffect(() => this.actions$ .pipe( ofType(...Object.values(NotificationsActionTypes)), map((action: RemoveNotificationAction) => { - const id: string = action.payload.id || action.payload || this.allIdentifier; - if (hasNoValue(this.notificationActionMap$[id])) { - this.notificationActionMap$[id] = new Subject(); - } - this.notificationActionMap$[id].next(action); + const id: string = action.payload.id || action.payload || this.allIdentifier; + if (hasNoValue(this.notificationActionMap$[id])) { + this.notificationActionMap$[id] = new Subject(); } - ) + this.notificationActionMap$[id].next(action); + }, + ), ), { dispatch: false }); /** @@ -88,52 +107,52 @@ export class ObjectUpdatesEffects { * When a REINSTATE action is fired during the timeout, a NO_ACTION action will be returned * When any other ObjectUpdatesAction is fired during the timeout, a RemoteObjectUpdatesAction will be returned */ - removeAfterDiscardOrReinstateOnUndo$ = createEffect(() => this.actions$ + removeAfterDiscardOrReinstateOnUndo$ = createEffect(() => this.actions$ .pipe( ofType(ObjectUpdatesActionTypes.DISCARD), switchMap((action: DiscardObjectUpdatesAction) => { - const url: string = action.payload.url; - const notification: INotification = action.payload.notification; - const timeOut = notification.options.timeOut; + const url: string = action.payload.url; + const notification: INotification = action.payload.notification; + const timeOut = notification.options.timeOut; - let removeAction: Action = new RemoveObjectUpdatesAction(action.payload.url); - if (action.payload.discardAll) { - removeAction = new RemoveAllObjectUpdatesAction(); - } - - return observableRace( - // Either wait for the delay and perform a remove action - observableOf(removeAction).pipe(delay(timeOut)), - // Or wait for a a user action - this.actionMap$[url].pipe( - take(1), - tap(() => { - this.notificationsService.remove(notification); - }), - map((updateAction: ObjectUpdatesAction) => { - if (updateAction.type === ObjectUpdatesActionTypes.REINSTATE) { - // If someone reinstated, do nothing, just let the reinstating happen - return new NoOpAction(); - } - // If someone performed another action, assume the user does not want to reinstate and remove all changes - return removeAction; - }) - ), - this.notificationActionMap$[notification.id].pipe( - filter((notificationsAction: NotificationsActions) => notificationsAction.type === NotificationsActionTypes.REMOVE_NOTIFICATION), - map(() => { - return removeAction; - }) - ), - this.notificationActionMap$[this.allIdentifier].pipe( - filter((notificationsAction: NotificationsActions) => notificationsAction.type === NotificationsActionTypes.REMOVE_ALL_NOTIFICATIONS), - map(() => { - return removeAction; - }) - ) - ); + let removeAction: Action = new RemoveObjectUpdatesAction(action.payload.url); + if (action.payload.discardAll) { + removeAction = new RemoveAllObjectUpdatesAction(); } - ) + + return observableRace( + // Either wait for the delay and perform a remove action + observableOf(removeAction).pipe(delay(timeOut)), + // Or wait for a a user action + this.actionMap$[url].pipe( + take(1), + tap(() => { + this.notificationsService.remove(notification); + }), + map((updateAction: ObjectUpdatesAction) => { + if (updateAction.type === ObjectUpdatesActionTypes.REINSTATE) { + // If someone reinstated, do nothing, just let the reinstating happen + return new NoOpAction(); + } + // If someone performed another action, assume the user does not want to reinstate and remove all changes + return removeAction; + }), + ), + this.notificationActionMap$[notification.id].pipe( + filter((notificationsAction: NotificationsActions) => notificationsAction.type === NotificationsActionTypes.REMOVE_NOTIFICATION), + map(() => { + return removeAction; + }), + ), + this.notificationActionMap$[this.allIdentifier].pipe( + filter((notificationsAction: NotificationsActions) => notificationsAction.type === NotificationsActionTypes.REMOVE_ALL_NOTIFICATIONS), + map(() => { + return removeAction; + }), + ), + ); + }, + ), )); constructor(private actions$: Actions, diff --git a/src/app/core/data/object-updates/object-updates.reducer.spec.ts b/src/app/core/data/object-updates/object-updates.reducer.spec.ts index 08944a073f7..1f2a15769b4 100644 --- a/src/app/core/data/object-updates/object-updates.reducer.spec.ts +++ b/src/app/core/data/object-updates/object-updates.reducer.spec.ts @@ -1,5 +1,8 @@ // eslint-disable-next-line import/no-namespace import * as deepFreeze from 'deep-freeze'; + +import { Relationship } from '../../shared/item-relationships/relationship.model'; +import { FieldChangeType } from './field-change-type.model'; import { AddFieldUpdateAction, DiscardObjectUpdatesAction, @@ -10,11 +13,13 @@ import { RemoveObjectUpdatesAction, SelectVirtualMetadataAction, SetEditableFieldUpdateAction, - SetValidFieldUpdateAction + SetValidFieldUpdateAction, } from './object-updates.actions'; -import { OBJECT_UPDATES_TRASH_PATH, objectUpdatesReducer } from './object-updates.reducer'; -import { Relationship } from '../../shared/item-relationships/relationship.model'; -import { FieldChangeType } from './field-change-type.model'; +import { + OBJECT_UPDATES_TRASH_PATH, + objectUpdatesReducer, + ObjectUpdatesState, +} from './object-updates.reducer'; class NullAction extends RemoveFieldUpdateAction { type = null; @@ -29,26 +34,26 @@ const identifiable1 = { uuid: '8222b07e-330d-417b-8d7f-3b82aeaf2320', key: 'dc.contributor.author', language: null, - value: 'Smith, John' + value: 'Smith, John', }; const identifiable1update = { uuid: '8222b07e-330d-417b-8d7f-3b82aeaf2320', key: 'dc.contributor.author', language: null, - value: 'Smith, James' + value: 'Smith, James', }; const identifiable2 = { uuid: '26cbb5ce-5786-4e57-a394-b9fcf8eaf241', key: 'dc.title', language: null, - value: 'New title' + value: 'New title', }; const identifiable3 = { uuid: 'c5d2c2f7-d757-48bf-84cc-8c9229c8407e', key: 'dc.description.abstract', language: null, - value: 'Unchanged value' + value: 'Unchanged value', }; const relationship: Relationship = Object.assign(new Relationship(), { uuid: 'test relationship uuid' }); @@ -56,65 +61,62 @@ const modDate = new Date(2010, 2, 11); const uuid = identifiable1.uuid; const url = 'test-object.url/edit'; describe('objectUpdatesReducer', () => { - const testState = { + const testState: ObjectUpdatesState = { [url]: { fieldStates: { [identifiable1.uuid]: { editable: true, isNew: false, - isValid: true + isValid: true, }, [identifiable2.uuid]: { editable: false, isNew: true, - isValid: true + isValid: true, }, [identifiable3.uuid]: { editable: false, isNew: false, - isValid: false + isValid: false, }, }, fieldUpdates: { [identifiable2.uuid]: { field: { uuid: identifiable2.uuid, - key: 'dc.titl', - language: null, - value: 'New title' }, - changeType: FieldChangeType.ADD - } + changeType: FieldChangeType.ADD, + }, }, lastModified: modDate, virtualMetadataSources: { - [relationship.uuid]: { [identifiable1.uuid]: true } + [relationship.uuid]: { [identifiable1.uuid]: true }, }, - } + }, }; - const discardedTestState = { + const discardedTestState: ObjectUpdatesState = { [url]: { fieldStates: { [identifiable1.uuid]: { editable: true, isNew: false, - isValid: true + isValid: true, }, [identifiable2.uuid]: { editable: false, isNew: true, - isValid: true + isValid: true, }, [identifiable3.uuid]: { editable: false, isNew: false, - isValid: true + isValid: true, }, }, lastModified: modDate, virtualMetadataSources: { - [relationship.uuid]: { [identifiable1.uuid]: true } + [relationship.uuid]: { [identifiable1.uuid]: true }, }, }, [url + OBJECT_UPDATES_TRASH_PATH]: { @@ -122,35 +124,32 @@ describe('objectUpdatesReducer', () => { [identifiable1.uuid]: { editable: true, isNew: false, - isValid: true + isValid: true, }, [identifiable2.uuid]: { editable: false, isNew: true, - isValid: true + isValid: true, }, [identifiable3.uuid]: { editable: false, isNew: false, - isValid: false + isValid: false, }, }, fieldUpdates: { [identifiable2.uuid]: { field: { uuid: identifiable2.uuid, - key: 'dc.titl', - language: null, - value: 'New title' }, - changeType: FieldChangeType.ADD - } + changeType: FieldChangeType.ADD, + }, }, lastModified: modDate, virtualMetadataSources: { - [relationship.uuid]: { [identifiable1.uuid]: true } + [relationship.uuid]: { [identifiable1.uuid]: true }, }, - } + }, }; deepFreeze(testState); @@ -173,48 +172,80 @@ describe('objectUpdatesReducer', () => { const action = new InitializeFieldsAction(url, [identifiable1, identifiable2], modDate); // testState has already been frozen above objectUpdatesReducer(testState, action); + + // no expect required, deepFreeze will ensure an exception is thrown if the state + // is mutated, and any uncaught exception will cause the test to fail + expect().nothing(); }); it('should perform the SET_EDITABLE_FIELD action without affecting the previous state', () => { const action = new SetEditableFieldUpdateAction(url, uuid, false); // testState has already been frozen above objectUpdatesReducer(testState, action); + + // no expect required, deepFreeze will ensure an exception is thrown if the state + // is mutated, and any uncaught exception will cause the test to fail + expect().nothing(); }); it('should perform the ADD_FIELD action without affecting the previous state', () => { const action = new AddFieldUpdateAction(url, identifiable1update, FieldChangeType.UPDATE); // testState has already been frozen above objectUpdatesReducer(testState, action); + + // no expect required, deepFreeze will ensure an exception is thrown if the state + // is mutated, and any uncaught exception will cause the test to fail + expect().nothing(); }); it('should perform the DISCARD action without affecting the previous state', () => { const action = new DiscardObjectUpdatesAction(url, null); // testState has already been frozen above objectUpdatesReducer(testState, action); + + // no expect required, deepFreeze will ensure an exception is thrown if the state + // is mutated, and any uncaught exception will cause the test to fail + expect().nothing(); }); it('should perform the REINSTATE action without affecting the previous state', () => { const action = new ReinstateObjectUpdatesAction(url); // testState has already been frozen above objectUpdatesReducer(testState, action); + + // no expect required, deepFreeze will ensure an exception is thrown if the state + // is mutated, and any uncaught exception will cause the test to fail + expect().nothing(); }); it('should perform the REMOVE action without affecting the previous state', () => { const action = new RemoveFieldUpdateAction(url, uuid); // testState has already been frozen above objectUpdatesReducer(testState, action); + + // no expect required, deepFreeze will ensure an exception is thrown if the state + // is mutated, and any uncaught exception will cause the test to fail + expect().nothing(); }); it('should perform the REMOVE_FIELD action without affecting the previous state', () => { const action = new RemoveFieldUpdateAction(url, uuid); // testState has already been frozen above objectUpdatesReducer(testState, action); + + // no expect required, deepFreeze will ensure an exception is thrown if the state + // is mutated, and any uncaught exception will cause the test to fail + expect().nothing(); }); it('should perform the SELECT_VIRTUAL_METADATA action without affecting the previous state', () => { const action = new SelectVirtualMetadataAction(url, relationship.uuid, identifiable1.uuid, true); // testState has already been frozen above objectUpdatesReducer(testState, action); + + // no expect required, deepFreeze will ensure an exception is thrown if the state + // is mutated, and any uncaught exception will cause the test to fail + expect().nothing(); }); it('should initialize all fields when the INITIALIZE action is dispatched, based on the payload', () => { @@ -226,19 +257,19 @@ describe('objectUpdatesReducer', () => { [identifiable1.uuid]: { editable: false, isNew: false, - isValid: true + isValid: true, }, [identifiable3.uuid]: { editable: false, isNew: false, - isValid: true + isValid: true, }, }, fieldUpdates: {}, virtualMetadataSources: {}, lastModified: modDate, - patchOperationService: undefined - } + patchOperationService: undefined, + }, }; const newState = objectUpdatesReducer(testState, action); expect(newState).toEqual(expectedState); 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 14bacc52db4..cadae9ae838 100644 --- a/src/app/core/data/object-updates/object-updates.reducer.ts +++ b/src/app/core/data/object-updates/object-updates.reducer.ts @@ -1,3 +1,14 @@ +import { + hasNoValue, + hasValue, +} from '../../../shared/empty.util'; +import { GenericConstructor } from '../../shared/generic-constructor'; +import { Item } from '../../shared/item.model'; +import { Relationship } from '../../shared/item-relationships/relationship.model'; +import { RelationshipType } from '../../shared/item-relationships/relationship-type.model'; +import { FieldChangeType } from './field-change-type.model'; +import { FieldUpdates } from './field-updates.model'; +import { Identifiable } from './identifiable.model'; import { AddFieldUpdateAction, DiscardObjectUpdatesAction, @@ -11,15 +22,7 @@ import { SetEditableFieldUpdateAction, SetValidFieldUpdateAction, } from './object-updates.actions'; -import { hasNoValue, hasValue } from '../../../shared/empty.util'; -import { Relationship } from '../../shared/item-relationships/relationship.model'; import { PatchOperationService } from './patch-operation-service/patch-operation.service'; -import { Item } from '../../shared/item.model'; -import { RelationshipType } from '../../shared/item-relationships/relationship-type.model'; -import { GenericConstructor } from '../../shared/generic-constructor'; -import { Identifiable } from './identifiable.model'; -import { FieldUpdates } from './field-updates.model'; -import { FieldChangeType } from './field-change-type.model'; /** * Path where discarded objects are saved @@ -58,6 +61,8 @@ export interface VirtualMetadataSource { export interface RelationshipIdentifiable extends Identifiable { nameVariant?: string; + originalItem: Item; + originalIsLeft: boolean relatedItem: Item; relationship: Relationship; type: RelationshipType; @@ -77,7 +82,7 @@ export interface DeleteRelationship extends RelationshipIdentifiable { */ export interface ObjectUpdatesEntry { fieldStates: FieldStates; - fieldUpdates: FieldUpdates; + fieldUpdates?: FieldUpdates; virtualMetadataSources: VirtualMetadataSources; lastModified: Date; patchOperationService?: GenericConstructor; @@ -164,7 +169,7 @@ function initializeFieldsUpdate(state: any, action: InitializeFieldsAction) { { fieldUpdates: {} }, { virtualMetadataSources: {} }, { lastModified: lastModifiedServer }, - { patchOperationService } + { patchOperationService }, ); return Object.assign({}, state, { [url]: newPageState }); } @@ -178,7 +183,7 @@ function addFieldUpdate(state: any, action: AddFieldUpdateAction) { const url: string = action.payload.url; const field: Identifiable = action.payload.field; const changeType: FieldChangeType = action.payload.changeType; - const pageState: ObjectUpdatesEntry = state[url] || {fieldUpdates: {}}; + const pageState: ObjectUpdatesEntry = state[url] || { fieldUpdates: {} }; let states = pageState.fieldStates; if (changeType === FieldChangeType.ADD) { @@ -231,7 +236,7 @@ function selectVirtualMetadata(state: any, action: SelectVirtualMetadataAction) const newPageState = Object.assign( {}, pageState, - {virtualMetadataSources: virtualMetadataSources}, + { virtualMetadataSources: virtualMetadataSources }, ); return Object.assign( @@ -239,7 +244,7 @@ function selectVirtualMetadata(state: any, action: SelectVirtualMetadataAction) state, { [url]: newPageState, - } + }, ); } @@ -279,7 +284,7 @@ function discardObjectUpdatesFor(url: string, state: any) { const discardedPageState = Object.assign({}, pageState, { fieldUpdates: {}, - fieldStates: newFieldStates + fieldStates: newFieldStates, }); return Object.assign({}, state, { [url]: discardedPageState }, { [url + OBJECT_UPDATES_TRASH_PATH]: pageState }); } @@ -357,7 +362,7 @@ function removeFieldUpdate(state: any, action: RemoveFieldUpdateAction) { } newPageState = Object.assign({}, state[url], { fieldUpdates: newUpdates, - fieldStates: newFieldStates + fieldStates: newFieldStates, }); } return Object.assign({}, state, { [url]: newPageState }); 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 9cf856f03a0..6602cda080c 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,21 +1,23 @@ +import { Injector } from '@angular/core'; import { Store } from '@ngrx/store'; -import { ObjectUpdatesService } from './object-updates.service'; +import { createMockStore } from '@ngrx/store/testing'; +import { of as observableOf } from 'rxjs'; + +import { Notification } from '../../../shared/notifications/models/notification.model'; +import { NotificationType } from '../../../shared/notifications/models/notification-type'; +import { CoreState } from '../../core-state.model'; +import { Relationship } from '../../shared/item-relationships/relationship.model'; +import { FieldChangeType } from './field-change-type.model'; import { DiscardObjectUpdatesAction, InitializeFieldsAction, ReinstateObjectUpdatesAction, RemoveFieldUpdateAction, SelectVirtualMetadataAction, - SetEditableFieldUpdateAction + SetEditableFieldUpdateAction, } from './object-updates.actions'; -import { of as observableOf } from 'rxjs'; -import { Notification } from '../../../shared/notifications/models/notification.model'; -import { NotificationType } from '../../../shared/notifications/models/notification-type'; import { OBJECT_UPDATES_TRASH_PATH } from './object-updates.reducer'; -import { Relationship } from '../../shared/item-relationships/relationship.model'; -import { Injector } from '@angular/core'; -import { FieldChangeType } from './field-change-type.model'; -import { CoreState } from '../../core-state.model'; +import { ObjectUpdatesService } from './object-updates.service'; describe('ObjectUpdatesService', () => { let service: ObjectUpdatesService; @@ -31,7 +33,7 @@ describe('ObjectUpdatesService', () => { const fieldUpdates = { [identifiable1.uuid]: { field: identifiable1Updated, changeType: FieldChangeType.UPDATE }, - [identifiable3.uuid]: { field: identifiable3, changeType: FieldChangeType.ADD } + [identifiable3.uuid]: { field: identifiable3, changeType: FieldChangeType.ADD }, }; const modDate = new Date(2010, 2, 11); @@ -46,15 +48,15 @@ describe('ObjectUpdatesService', () => { }; patchOperationService = jasmine.createSpyObj('patchOperationService', { - fieldUpdatesToPatchOperations: [] + fieldUpdatesToPatchOperations: [], }); const objectEntry = { - fieldStates, fieldUpdates, lastModified: modDate, virtualMetadataSources: {}, patchOperationService + fieldStates, fieldUpdates, lastModified: modDate, virtualMetadataSources: {}, patchOperationService, }; - store = new Store(undefined, undefined, undefined); + store = createMockStore({}); spyOn(store, 'dispatch'); injector = jasmine.createSpyObj('injector', { - get: patchOperationService + get: patchOperationService, }); service = new ObjectUpdatesService(store, injector); @@ -80,7 +82,7 @@ describe('ObjectUpdatesService', () => { const expectedResult = { [identifiable1.uuid]: { field: identifiable1Updated, changeType: FieldChangeType.UPDATE }, [identifiable2.uuid]: { field: identifiable2, changeType: undefined }, - [identifiable3.uuid]: { field: identifiable3, changeType: FieldChangeType.ADD } + [identifiable3.uuid]: { field: identifiable3, changeType: FieldChangeType.ADD }, }; result$.subscribe((result) => { @@ -96,7 +98,7 @@ describe('ObjectUpdatesService', () => { const expectedResult = { [identifiable1.uuid]: { field: identifiable1Updated, changeType: FieldChangeType.UPDATE }, - [identifiable2.uuid]: { field: identifiable2, changeType: undefined } + [identifiable2.uuid]: { field: identifiable2, changeType: undefined }, }; result$.subscribe((result) => { 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 2fb6d47d31c..75b554d87b1 100644 --- a/src/app/core/data/object-updates/object-updates.service.ts +++ b/src/app/core/data/object-updates/object-updates.service.ts @@ -1,14 +1,37 @@ -import { Injectable, Injector } from '@angular/core'; -import { createSelector, MemoizedSelector, select, Store } from '@ngrx/store'; -import { coreSelector } from '../../core.selectors'; import { - FieldState, - OBJECT_UPDATES_TRASH_PATH, - ObjectUpdatesEntry, - ObjectUpdatesState, - VirtualMetadataSource -} from './object-updates.reducer'; + Injectable, + Injector, +} from '@angular/core'; +import { + createSelector, + MemoizedSelector, + select, + Store, +} from '@ngrx/store'; +import { Operation } from 'fast-json-patch'; import { Observable } from 'rxjs'; +import { + distinctUntilChanged, + filter, + map, + switchMap, + take, +} from 'rxjs/operators'; + +import { + hasNoValue, + hasValue, + hasValueOperator, + isEmpty, + isNotEmpty, +} from '../../../shared/empty.util'; +import { INotification } from '../../../shared/notifications/models/notification.model'; +import { coreSelector } from '../../core.selectors'; +import { CoreState } from '../../core-state.model'; +import { GenericConstructor } from '../../shared/generic-constructor'; +import { FieldChangeType } from './field-change-type.model'; +import { FieldUpdates } from './field-updates.model'; +import { Identifiable } from './identifiable.model'; import { AddFieldUpdateAction, DiscardObjectUpdatesAction, @@ -17,24 +40,16 @@ import { RemoveFieldUpdateAction, SelectVirtualMetadataAction, SetEditableFieldUpdateAction, - SetValidFieldUpdateAction + SetValidFieldUpdateAction, } from './object-updates.actions'; -import { distinctUntilChanged, filter, map, switchMap } from 'rxjs/operators'; import { - hasNoValue, - hasValue, - isEmpty, - isNotEmpty, - hasValueOperator -} from '../../../shared/empty.util'; -import { INotification } from '../../../shared/notifications/models/notification.model'; -import { Operation } from 'fast-json-patch'; + FieldState, + OBJECT_UPDATES_TRASH_PATH, + ObjectUpdatesEntry, + ObjectUpdatesState, + VirtualMetadataSource, +} from './object-updates.reducer'; import { PatchOperationService } from './patch-operation-service/patch-operation.service'; -import { GenericConstructor } from '../../shared/generic-constructor'; -import { Identifiable } from './identifiable.model'; -import { FieldUpdates } from './field-updates.model'; -import { FieldChangeType } from './field-change-type.model'; -import { CoreState } from '../../core-state.model'; function objectUpdatesStateSelector(): MemoizedSelector { return createSelector(coreSelector, (state: CoreState) => state['cache/object-updates']); @@ -55,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) { @@ -122,7 +137,7 @@ export class ObjectUpdatesService { fieldUpdates[uuid] = fieldUpdatesExclusive[uuid]; }); return fieldUpdates; - }) + }), ); }), ); @@ -139,16 +154,16 @@ export class ObjectUpdatesService { return objectUpdates.pipe( hasValueOperator(), map((objectEntry) => { - const fieldUpdates: FieldUpdates = {}; - for (const object of initialFields) { - let fieldUpdate = objectEntry.fieldUpdates[object.uuid]; - if (isEmpty(fieldUpdate)) { - fieldUpdate = { field: object, changeType: undefined }; + const fieldUpdates: FieldUpdates = {}; + for (const object of initialFields) { + let fieldUpdate = objectEntry.fieldUpdates[object.uuid]; + if (isEmpty(fieldUpdate)) { + fieldUpdate = { field: object, changeType: undefined }; + } + fieldUpdates[object.uuid] = fieldUpdate; } - fieldUpdates[object.uuid] = fieldUpdate; - } - return fieldUpdates; - })); + return fieldUpdates; + })); } /** @@ -161,7 +176,7 @@ export class ObjectUpdatesService { return fieldState$.pipe( filter((fieldState) => hasValue(fieldState)), map((fieldState) => fieldState.editable), - distinctUntilChanged() + distinctUntilChanged(), ); } @@ -175,7 +190,7 @@ export class ObjectUpdatesService { return fieldState$.pipe( filter((fieldState) => hasValue(fieldState)), map((fieldState) => fieldState.isValid), - distinctUntilChanged() + distinctUntilChanged(), ); } @@ -189,7 +204,7 @@ export class ObjectUpdatesService { map((entry: ObjectUpdatesEntry) => { return Object.values(entry.fieldStates).findIndex((state: FieldState) => !state.isValid) < 0; }), - distinctUntilChanged() + distinctUntilChanged(), ); } @@ -198,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$; } /** @@ -207,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$; } /** @@ -234,7 +261,7 @@ export class ObjectUpdatesService { .pipe( select(virtualMetadataSourceSelector(url, relationship)), map((virtualMetadataSource) => virtualMetadataSource && virtualMetadataSource[item]), - ); + ); } /** @@ -367,7 +394,7 @@ export class ObjectUpdatesService { patch = this.injector.get(entry.patchOperationService).fieldUpdatesToPatchOperations(entry.fieldUpdates); } return patch; - }) + }), ); } } diff --git a/src/app/core/data/object-updates/patch-operation-service/metadata-patch-operation.service.spec.ts b/src/app/core/data/object-updates/patch-operation-service/metadata-patch-operation.service.spec.ts index db46426b79f..ddf38dd2bb1 100644 --- a/src/app/core/data/object-updates/patch-operation-service/metadata-patch-operation.service.spec.ts +++ b/src/app/core/data/object-updates/patch-operation-service/metadata-patch-operation.service.spec.ts @@ -1,8 +1,9 @@ -import { MetadataPatchOperationService } from './metadata-patch-operation.service'; import { Operation } from 'fast-json-patch'; + import { MetadatumViewModel } from '../../../shared/metadata.models'; -import { FieldUpdates } from '../field-updates.model'; import { FieldChangeType } from '../field-change-type.model'; +import { FieldUpdates } from '../field-updates.model'; +import { MetadataPatchOperationService } from './metadata-patch-operation.service'; describe('MetadataPatchOperationService', () => { let service: MetadataPatchOperationService; @@ -23,13 +24,13 @@ describe('MetadataPatchOperationService', () => { field: Object.assign(new MetadatumViewModel(), { key: 'dc.title', value: 'Deleted title', - place: 0 + place: 0, }), - changeType: FieldChangeType.REMOVE - } + changeType: FieldChangeType.REMOVE, + }, }); expected = [ - { op: 'remove', path: '/metadata/dc.title/0' } + { op: 'remove', path: '/metadata/dc.title/0' }, ] as any[]; result = service.fieldUpdatesToPatchOperations(fieldUpdates); }); @@ -46,13 +47,13 @@ describe('MetadataPatchOperationService', () => { field: Object.assign(new MetadatumViewModel(), { key: 'dc.title', value: 'Added title', - place: 0 + place: 0, }), - changeType: FieldChangeType.ADD - } + changeType: FieldChangeType.ADD, + }, }); expected = [ - { op: 'add', path: '/metadata/dc.title/-', value: [{ value: 'Added title', language: undefined }] } + { op: 'add', path: '/metadata/dc.title/-', value: [{ value: 'Added title', language: undefined }] }, ] as any[]; result = service.fieldUpdatesToPatchOperations(fieldUpdates); }); @@ -69,13 +70,13 @@ describe('MetadataPatchOperationService', () => { field: Object.assign(new MetadatumViewModel(), { key: 'dc.title', value: 'Changed title', - place: 0 + place: 0, }), - changeType: FieldChangeType.UPDATE - } + changeType: FieldChangeType.UPDATE, + }, }); expected = [ - { op: 'replace', path: '/metadata/dc.title/0', value: { value: 'Changed title', language: undefined } } + { op: 'replace', path: '/metadata/dc.title/0', value: { value: 'Changed title', language: undefined } }, ] as any[]; result = service.fieldUpdatesToPatchOperations(fieldUpdates); }); @@ -92,31 +93,31 @@ describe('MetadataPatchOperationService', () => { field: Object.assign(new MetadatumViewModel(), { key: 'dc.title', value: 'First deleted title', - place: 0 + place: 0, }), - changeType: FieldChangeType.REMOVE + changeType: FieldChangeType.REMOVE, }, update2: { field: Object.assign(new MetadatumViewModel(), { key: 'dc.title', value: 'Second deleted title', - place: 1 + place: 1, }), - changeType: FieldChangeType.REMOVE + changeType: FieldChangeType.REMOVE, }, update3: { field: Object.assign(new MetadatumViewModel(), { key: 'dc.title', value: 'Third deleted title', - place: 2 + place: 2, }), - changeType: FieldChangeType.REMOVE - } + changeType: FieldChangeType.REMOVE, + }, }); expected = [ { op: 'remove', path: '/metadata/dc.title/0' }, { op: 'remove', path: '/metadata/dc.title/0' }, - { op: 'remove', path: '/metadata/dc.title/0' } + { op: 'remove', path: '/metadata/dc.title/0' }, ] as any[]; result = service.fieldUpdatesToPatchOperations(fieldUpdates); }); @@ -133,31 +134,31 @@ describe('MetadataPatchOperationService', () => { field: Object.assign(new MetadatumViewModel(), { key: 'dc.title', value: 'Third deleted title', - place: 2 + place: 2, }), - changeType: FieldChangeType.REMOVE + changeType: FieldChangeType.REMOVE, }, update2: { field: Object.assign(new MetadatumViewModel(), { key: 'dc.title', value: 'Second deleted title', - place: 1 + place: 1, }), - changeType: FieldChangeType.REMOVE + changeType: FieldChangeType.REMOVE, }, update3: { field: Object.assign(new MetadatumViewModel(), { key: 'dc.title', value: 'First deleted title', - place: 0 + place: 0, }), - changeType: FieldChangeType.REMOVE - } + changeType: FieldChangeType.REMOVE, + }, }); expected = [ { op: 'remove', path: '/metadata/dc.title/2' }, { op: 'remove', path: '/metadata/dc.title/1' }, - { op: 'remove', path: '/metadata/dc.title/0' } + { op: 'remove', path: '/metadata/dc.title/0' }, ] as any[]; result = service.fieldUpdatesToPatchOperations(fieldUpdates); }); @@ -174,31 +175,31 @@ describe('MetadataPatchOperationService', () => { field: Object.assign(new MetadatumViewModel(), { key: 'dc.title', value: 'Second deleted title', - place: 1 + place: 1, }), - changeType: FieldChangeType.REMOVE + changeType: FieldChangeType.REMOVE, }, update2: { field: Object.assign(new MetadatumViewModel(), { key: 'dc.title', value: 'Third deleted title', - place: 2 + place: 2, }), - changeType: FieldChangeType.REMOVE + changeType: FieldChangeType.REMOVE, }, update3: { field: Object.assign(new MetadatumViewModel(), { key: 'dc.title', value: 'First deleted title', - place: 0 + place: 0, }), - changeType: FieldChangeType.REMOVE - } + changeType: FieldChangeType.REMOVE, + }, }); expected = [ { op: 'remove', path: '/metadata/dc.title/1' }, { op: 'remove', path: '/metadata/dc.title/1' }, - { op: 'remove', path: '/metadata/dc.title/0' } + { op: 'remove', path: '/metadata/dc.title/0' }, ] as any[]; result = service.fieldUpdatesToPatchOperations(fieldUpdates); }); @@ -215,31 +216,31 @@ describe('MetadataPatchOperationService', () => { field: Object.assign(new MetadatumViewModel(), { key: 'dc.title', value: 'Second deleted title', - place: 1 + place: 1, }), - changeType: FieldChangeType.REMOVE + changeType: FieldChangeType.REMOVE, }, update2: { field: Object.assign(new MetadatumViewModel(), { key: 'dc.title', value: 'Third changed title', - place: 2 + place: 2, }), - changeType: FieldChangeType.UPDATE + changeType: FieldChangeType.UPDATE, }, update3: { field: Object.assign(new MetadatumViewModel(), { key: 'dc.title', value: 'First deleted title', - place: 0 + place: 0, }), - changeType: FieldChangeType.REMOVE - } + changeType: FieldChangeType.REMOVE, + }, }); expected = [ { op: 'remove', path: '/metadata/dc.title/1' }, { op: 'replace', path: '/metadata/dc.title/1', value: { value: 'Third changed title', language: undefined } }, - { op: 'remove', path: '/metadata/dc.title/0' } + { op: 'remove', path: '/metadata/dc.title/0' }, ] as any[]; result = service.fieldUpdatesToPatchOperations(fieldUpdates); }); diff --git a/src/app/core/data/object-updates/patch-operation-service/metadata-patch-operation.service.ts b/src/app/core/data/object-updates/patch-operation-service/metadata-patch-operation.service.ts index 33e9129a9d1..b6dccb759b2 100644 --- a/src/app/core/data/object-updates/patch-operation-service/metadata-patch-operation.service.ts +++ b/src/app/core/data/object-updates/patch-operation-service/metadata-patch-operation.service.ts @@ -1,21 +1,22 @@ -import { PatchOperationService } from './patch-operation.service'; -import { MetadatumViewModel } from '../../../shared/metadata.models'; -import { Operation } from 'fast-json-patch'; import { Injectable } from '@angular/core'; -import { MetadataPatchOperation } from './operations/metadata/metadata-patch-operation.model'; +import { Operation } from 'fast-json-patch'; + import { hasValue } from '../../../../shared/empty.util'; +import { MetadatumViewModel } from '../../../shared/metadata.models'; +import { FieldChangeType } from '../field-change-type.model'; +import { FieldUpdates } from '../field-updates.model'; import { MetadataPatchAddOperation } from './operations/metadata/metadata-patch-add-operation.model'; +import { MetadataPatchOperation } from './operations/metadata/metadata-patch-operation.model'; import { MetadataPatchRemoveOperation } from './operations/metadata/metadata-patch-remove-operation.model'; import { MetadataPatchReplaceOperation } from './operations/metadata/metadata-patch-replace-operation.model'; -import { FieldUpdates } from '../field-updates.model'; -import { FieldChangeType } from '../field-change-type.model'; +import { PatchOperationService } from './patch-operation.service'; /** * Service transforming {@link FieldUpdates} into {@link Operation}s for metadata values * This expects the fields within every {@link FieldUpdate} to be {@link MetadatumViewModel}s */ @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class MetadataPatchOperationService implements PatchOperationService { @@ -75,7 +76,7 @@ export class MetadataPatchOperationService implements PatchOperationService { const metadatum = update.field as MetadatumViewModel; const val = { value: metadatum.value, - language: metadatum.language + language: metadatum.language, }; let operation: MetadataPatchOperation; diff --git a/src/app/core/data/object-updates/patch-operation-service/operations/metadata/metadata-patch-add-operation.model.ts b/src/app/core/data/object-updates/patch-operation-service/operations/metadata/metadata-patch-add-operation.model.ts index 7f9b1d772f4..9242290c6b7 100644 --- a/src/app/core/data/object-updates/patch-operation-service/operations/metadata/metadata-patch-add-operation.model.ts +++ b/src/app/core/data/object-updates/patch-operation-service/operations/metadata/metadata-patch-add-operation.model.ts @@ -1,6 +1,7 @@ -import { MetadataPatchOperation } from './metadata-patch-operation.model'; import { Operation } from 'fast-json-patch'; +import { MetadataPatchOperation } from './metadata-patch-operation.model'; + /** * Wrapper object for a metadata patch add Operation */ diff --git a/src/app/core/data/object-updates/patch-operation-service/operations/metadata/metadata-patch-move-operation.model.ts b/src/app/core/data/object-updates/patch-operation-service/operations/metadata/metadata-patch-move-operation.model.ts index 962d53dfee4..d80ec16cd1a 100644 --- a/src/app/core/data/object-updates/patch-operation-service/operations/metadata/metadata-patch-move-operation.model.ts +++ b/src/app/core/data/object-updates/patch-operation-service/operations/metadata/metadata-patch-move-operation.model.ts @@ -1,6 +1,7 @@ -import { MetadataPatchOperation } from './metadata-patch-operation.model'; import { Operation } from 'fast-json-patch'; +import { MetadataPatchOperation } from './metadata-patch-operation.model'; + /** * Wrapper object for a metadata patch move Operation */ diff --git a/src/app/core/data/object-updates/patch-operation-service/operations/metadata/metadata-patch-remove-operation.model.ts b/src/app/core/data/object-updates/patch-operation-service/operations/metadata/metadata-patch-remove-operation.model.ts index 61fbae1980b..efaf61f3814 100644 --- a/src/app/core/data/object-updates/patch-operation-service/operations/metadata/metadata-patch-remove-operation.model.ts +++ b/src/app/core/data/object-updates/patch-operation-service/operations/metadata/metadata-patch-remove-operation.model.ts @@ -1,6 +1,7 @@ -import { MetadataPatchOperation } from './metadata-patch-operation.model'; import { Operation } from 'fast-json-patch'; +import { MetadataPatchOperation } from './metadata-patch-operation.model'; + /** * Wrapper object for a metadata patch remove Operation */ diff --git a/src/app/core/data/object-updates/patch-operation-service/operations/metadata/metadata-patch-replace-operation.model.ts b/src/app/core/data/object-updates/patch-operation-service/operations/metadata/metadata-patch-replace-operation.model.ts index e889bede0b7..c2d95812933 100644 --- a/src/app/core/data/object-updates/patch-operation-service/operations/metadata/metadata-patch-replace-operation.model.ts +++ b/src/app/core/data/object-updates/patch-operation-service/operations/metadata/metadata-patch-replace-operation.model.ts @@ -1,6 +1,7 @@ -import { MetadataPatchOperation } from './metadata-patch-operation.model'; import { Operation } from 'fast-json-patch'; +import { MetadataPatchOperation } from './metadata-patch-operation.model'; + /** * Wrapper object for a metadata patch replace Operation */ diff --git a/src/app/core/data/object-updates/patch-operation-service/patch-operation.service.ts b/src/app/core/data/object-updates/patch-operation-service/patch-operation.service.ts index 171c1d2a54e..7e9c1087ff0 100644 --- a/src/app/core/data/object-updates/patch-operation-service/patch-operation.service.ts +++ b/src/app/core/data/object-updates/patch-operation-service/patch-operation.service.ts @@ -1,4 +1,5 @@ import { Operation } from 'fast-json-patch'; + import { FieldUpdates } from '../field-updates.model'; /** diff --git a/src/app/core/data/paginated-list.model.ts b/src/app/core/data/paginated-list.model.ts index 415bfe234ea..ef2819afd8f 100644 --- a/src/app/core/data/paginated-list.model.ts +++ b/src/app/core/data/paginated-list.model.ts @@ -1,13 +1,22 @@ -import { PageInfo } from '../shared/page-info.model'; -import { hasValue, isEmpty, hasNoValue, isUndefined } from '../../shared/empty.util'; -import { HALResource } from '../shared/hal-resource.model'; -import { HALLink } from '../shared/hal-link.model'; +import { + autoserialize, + deserialize, +} from 'cerialize'; + +import { + hasNoValue, + hasValue, + isEmpty, + isUndefined, +} from '../../shared/empty.util'; import { typedObject } from '../cache/builders/build-decorators'; -import { PAGINATED_LIST } from './paginated-list.resource-type'; +import { CacheableObject } from '../cache/cacheable-object.model'; +import { HALLink } from '../shared/hal-link.model'; +import { HALResource } from '../shared/hal-resource.model'; +import { PageInfo } from '../shared/page-info.model'; import { ResourceType } from '../shared/resource-type'; import { excludeFromEquals } from '../utilities/equals.decorators'; -import { autoserialize, deserialize } from 'cerialize'; -import { CacheableObject } from '../cache/cacheable-object.model'; +import { PAGINATED_LIST } from './paginated-list.resource-type'; /** * Factory function for a paginated list @@ -45,7 +54,7 @@ export const buildPaginatedList = (pageInfo: PageInfo, page: T[], normalized } result._links = Object.assign({}, _links, pageInfo._links, { - page: pageLinks + page: pageLinks, }); if (!normalized || isUndefined(pageLinks)) { diff --git a/src/app/core/data/parsing.service.ts b/src/app/core/data/parsing.service.ts index fbebe75b2b5..9bf91121cc4 100644 --- a/src/app/core/data/parsing.service.ts +++ b/src/app/core/data/parsing.service.ts @@ -1,5 +1,5 @@ -import { RawRestResponse } from '../dspace-rest/raw-rest-response.model'; import { ParsedResponse } from '../cache/response.models'; +import { RawRestResponse } from '../dspace-rest/raw-rest-response.model'; import { RestRequest } from './rest-request.model'; export interface ResponseParsingService { diff --git a/src/app/core/data/primary-bitstream.service.spec.ts b/src/app/core/data/primary-bitstream.service.spec.ts index 00d6d7f03ce..6a9c89f7968 100644 --- a/src/app/core/data/primary-bitstream.service.spec.ts +++ b/src/app/core/data/primary-bitstream.service.spec.ts @@ -1,20 +1,30 @@ -import { ObjectCacheService } from '../cache/object-cache.service'; -import { RequestService } from './request.service'; -import { Bitstream } from '../shared/bitstream.model'; -import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { getMockRequestService } from '../../shared/mocks/request.service.mock'; -import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub'; -import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { getTestScheduler } from 'jasmine-marbles'; +import { of as observableOf } from 'rxjs'; + import { getMockRemoteDataBuildService } from '../../shared/mocks/remote-data-build.service.mock'; -import { PrimaryBitstreamService } from './primary-bitstream.service'; -import { BundleDataService } from './bundle-data.service'; +import { getMockRequestService } from '../../shared/mocks/request.service.mock'; import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { + createFailedRemoteDataObject, + createSuccessfulRemoteDataObject, + createSuccessfulRemoteDataObject$, +} from '../../shared/remote-data.utils'; +import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub'; import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub'; -import { CreateRequest, DeleteRequest, PostRequest, PutRequest } from './request.models'; -import { createFailedRemoteDataObject, createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { ObjectCacheService } from '../cache/object-cache.service'; +import { Bitstream } from '../shared/bitstream.model'; import { Bundle } from '../shared/bundle.model'; -import { getTestScheduler } from 'jasmine-marbles'; -import { of as observableOf } from 'rxjs'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { BundleDataService } from './bundle-data.service'; +import { PrimaryBitstreamService } from './primary-bitstream.service'; +import { + CreateRequest, + DeleteRequest, + PostRequest, + PutRequest, +} from './request.models'; +import { RequestService } from './request.service'; describe('PrimaryBitstreamService', () => { let service: PrimaryBitstreamService; @@ -28,8 +38,8 @@ describe('PrimaryBitstreamService', () => { const bitstream = Object.assign(new Bitstream(), { uuid: 'fake-bitstream', _links: { - self: { href: 'fake-bitstream-self' } - } + self: { href: 'fake-bitstream-self' }, + }, }); const bundle = Object.assign(new Bundle(), { @@ -37,21 +47,21 @@ describe('PrimaryBitstreamService', () => { _links: { self: { href: 'fake-bundle-self' }, primaryBitstream: { href: 'fake-primary-bitstream-self' }, - } + }, }); const url = 'fake-bitstream-url'; beforeEach(() => { objectCache = jasmine.createSpyObj('objectCache', { - remove: jasmine.createSpy('remove') + remove: jasmine.createSpy('remove'), }); requestService = getMockRequestService(); halService = Object.assign(new HALEndpointServiceStub(url)); rdbService = getMockRemoteDataBuildService(); notificationService = new NotificationsServiceStub() as any; - bundleDataService = jasmine.createSpyObj('bundleDataService', {'findByHref': createSuccessfulRemoteDataObject$(bundle)}); + bundleDataService = jasmine.createSpyObj('bundleDataService', { 'findByHref': createSuccessfulRemoteDataObject$(bundle) }); service = new PrimaryBitstreamService(requestService, rdbService, objectCache, halService, notificationService, bundleDataService); }); @@ -96,7 +106,7 @@ describe('PrimaryBitstreamService', () => { expect((service as any).createAndSendRequest).toHaveBeenCalledWith( PostRequest, bundle._links.primaryBitstream.href, - bitstream.self + bitstream.self, ); }); }); @@ -113,7 +123,7 @@ describe('PrimaryBitstreamService', () => { expect((service as any).createAndSendRequest).toHaveBeenCalledWith( PutRequest, bundle._links.primaryBitstream.href, - bitstream.self + bitstream.self, ); }); }); @@ -121,12 +131,12 @@ describe('PrimaryBitstreamService', () => { const testBundle = Object.assign(new Bundle(), { _links: { self: { - href: 'test-href' + href: 'test-href', }, primaryBitstream: { - href: 'test-primaryBitstream-href' - } - } + href: 'test-primaryBitstream-href', + }, + }, }); describe('when the delete request succeeds', () => { diff --git a/src/app/core/data/primary-bitstream.service.ts b/src/app/core/data/primary-bitstream.service.ts index 488cb5d22e7..a5367e67ed4 100644 --- a/src/app/core/data/primary-bitstream.service.ts +++ b/src/app/core/data/primary-bitstream.service.ts @@ -1,20 +1,28 @@ -import { Bitstream } from '../shared/bitstream.model'; +import { HttpHeaders } from '@angular/common/http'; import { Injectable } from '@angular/core'; -import { RequestService } from './request.service'; +import { + Observable, + switchMap, +} from 'rxjs'; + +import { NotificationsService } from '../../shared/notifications/notifications.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { ObjectCacheService } from '../cache/object-cache.service'; -import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { Observable, switchMap } from 'rxjs'; -import { RemoteData } from './remote-data'; -import { Bundle } from '../shared/bundle.model'; -import { NotificationsService } from '../../shared/notifications/notifications.service'; import { HttpOptions } from '../dspace-rest/dspace-rest.service'; -import { HttpHeaders } from '@angular/common/http'; +import { Bitstream } from '../shared/bitstream.model'; +import { Bundle } from '../shared/bundle.model'; import { GenericConstructor } from '../shared/generic-constructor'; -import { PutRequest, PostRequest, DeleteRequest } from './request.models'; -import { getAllCompletedRemoteData } from '../shared/operators'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; import { NoContent } from '../shared/NoContent.model'; +import { getAllCompletedRemoteData } from '../shared/operators'; import { BundleDataService } from './bundle-data.service'; +import { RemoteData } from './remote-data'; +import { + DeleteRequest, + PostRequest, + PutRequest, +} from './request.models'; +import { RequestService } from './request.service'; @Injectable({ providedIn: 'root', @@ -63,8 +71,8 @@ export class PrimaryBitstreamService { requestId, endpointURL, primaryBitstreamSelfLink, - this.getHttpOptions() - ); + this.getHttpOptions(), + ); this.requestService.send(request); @@ -81,7 +89,7 @@ export class PrimaryBitstreamService { return this.createAndSendRequest( PostRequest, bundle._links.primaryBitstream.href, - primaryBitstream.self + primaryBitstream.self, ) as Observable>; } @@ -95,7 +103,7 @@ export class PrimaryBitstreamService { return this.createAndSendRequest( PutRequest, bundle._links.primaryBitstream.href, - primaryBitstream.self + primaryBitstream.self, ) as Observable>; } @@ -107,12 +115,12 @@ export class PrimaryBitstreamService { delete(bundle: Bundle): Observable> { return this.createAndSendRequest( DeleteRequest, - bundle._links.primaryBitstream.href + bundle._links.primaryBitstream.href, ).pipe( getAllCompletedRemoteData(), switchMap((rd: RemoteData) => { return this.bundleDataService.findByHref(bundle.self, rd.hasFailed); - }) + }), ); } diff --git a/src/app/core/data/processes/process-data.service.spec.ts b/src/app/core/data/processes/process-data.service.spec.ts index 88e5bd57915..217567776c2 100644 --- a/src/app/core/data/processes/process-data.service.spec.ts +++ b/src/app/core/data/processes/process-data.service.spec.ts @@ -6,14 +6,197 @@ * http://www.dspace.org/license/ */ -import { testFindAllDataImplementation } from '../base/find-all-data.spec'; -import { ProcessDataService } from './process-data.service'; +import { + fakeAsync, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import { ReducerManager } from '@ngrx/store'; +import { of } from 'rxjs'; +import { TestScheduler } from 'rxjs/testing'; + +import { Process } from '../../../process-page/processes/process.model'; +import { ProcessStatus } from '../../../process-page/processes/process-status.model'; +import { getMockRequestService } from '../../../shared/mocks/request.service.mock'; +import { NotificationsService } from '../../../shared/notifications/notifications.service'; +import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service'; +import { ObjectCacheService } from '../../cache/object-cache.service'; +import { HALEndpointService } from '../../shared/hal-endpoint.service'; import { testDeleteDataImplementation } from '../base/delete-data.spec'; +import { testFindAllDataImplementation } from '../base/find-all-data.spec'; +import { testSearchDataImplementation } from '../base/search-data.spec'; +import { BitstreamFormatDataService } from '../bitstream-format-data.service'; +import { DSOChangeAnalyzer } from '../dso-change-analyzer.service'; +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'; +import { + ProcessDataService, + TIMER_FACTORY, +} from './process-data.service'; describe('ProcessDataService', () => { + let testScheduler; + + const mockTimer = (fn: () => any, interval: number) => { + fn(); + return 555; + }; + describe('composition', () => { - const initService = () => new ProcessDataService(null, null, null, null, null, null); + const initService = () => new ProcessDataService(null, null, null, null, null, null, null, null); testFindAllDataImplementation(initService); testDeleteDataImplementation(initService); + testSearchDataImplementation(initService); + }); + + let requestService = getMockRequestService(); + let processDataService; + let remoteDataBuildService; + + describe('autoRefreshUntilCompletion', () => { + beforeEach(waitForAsync(() => { + testScheduler = new TestScheduler((actual, expected) => { + expect(actual).toEqual(expected); + }); + TestBed.configureTestingModule({ + imports: [], + providers: [ + ProcessDataService, + { provide: RequestService, useValue: null }, + { provide: RemoteDataBuildService, useValue: null }, + { provide: ObjectCacheService, useValue: null }, + { provide: ReducerManager, useValue: null }, + { provide: HALEndpointService, useValue: null }, + { provide: DSOChangeAnalyzer, useValue: null }, + { provide: BitstreamFormatDataService, useValue: null }, + { provide: NotificationsService, useValue: null }, + { provide: TIMER_FACTORY, useValue: mockTimer }, + ], + }); + + processDataService = TestBed.inject(ProcessDataService); + spyOn(processDataService, 'invalidateByHref'); + })); + + it('should not do any polling when the process is already completed', () => { + testScheduler.run(({ cold, expectObservable }) => { + let completedProcess = new Process(); + completedProcess.processStatus = ProcessStatus.COMPLETED; + + const completedProcessRD = new RemoteData(0, 0, 0, RequestEntryState.Success, null, completedProcess); + + spyOn(processDataService, 'findById').and.returnValue( + cold('c', { + 'c': completedProcessRD, + }), + ); + + let process$ = processDataService.autoRefreshUntilCompletion('instantly'); + expectObservable(process$).toBe('c', { + c: completedProcessRD, + }); + }); + + expect(processDataService.findById).toHaveBeenCalledTimes(1); + expect(processDataService.invalidateByHref).not.toHaveBeenCalled(); + }); + + it('should poll until a process completes', () => { + testScheduler.run(({ cold, expectObservable }) => { + const runningProcess = Object.assign(new Process(), { + _links: { + self: { + href: 'https://rest.api/processes/123', + }, + }, + }); + runningProcess.processStatus = ProcessStatus.RUNNING; + const completedProcess = new Process(); + completedProcess.processStatus = ProcessStatus.COMPLETED; + const runningProcessRD = new RemoteData(0, 0, 0, RequestEntryState.Success, null, runningProcess); + const completedProcessRD = new RemoteData(0, 0, 0, RequestEntryState.Success, null, completedProcess); + + spyOn(processDataService, 'findById').and.returnValue( + cold('r 150ms c', { + 'r': runningProcessRD, + 'c': completedProcessRD, + }), + ); + + let process$ = processDataService.autoRefreshUntilCompletion('foo', 100); + expectObservable(process$).toBe('r 150ms c', { + 'r': runningProcessRD, + 'c': completedProcessRD, + }); + }); + + expect(processDataService.findById).toHaveBeenCalledTimes(1); + expect(processDataService.invalidateByHref).toHaveBeenCalledTimes(1); + }); + }); + + describe('autoRefreshingSearchBy', () => { + beforeEach(waitForAsync(() => { + + TestBed.configureTestingModule({ + imports: [], + providers: [ + ProcessDataService, + { provide: RequestService, useValue: requestService }, + { provide: RemoteDataBuildService, useValue: null }, + { provide: ObjectCacheService, useValue: null }, + { provide: ReducerManager, useValue: null }, + { provide: HALEndpointService, useValue: null }, + { provide: DSOChangeAnalyzer, useValue: null }, + { provide: BitstreamFormatDataService, useValue: null }, + { provide: NotificationsService, useValue: null }, + { provide: TIMER_FACTORY, useValue: mockTimer }, + ], + }); + + processDataService = TestBed.inject(ProcessDataService); + })); + + it('should refresh after the specified interval', fakeAsync(() => { + const runningProcess = Object.assign(new Process(), { + _links: { + self: { + href: 'https://rest.api/processes/123', + }, + }, + }); + runningProcess.processStatus = ProcessStatus.RUNNING; + + const runningProcessPagination: PaginatedList = Object.assign(new PaginatedList(), { + page: [runningProcess], + _links: { + self: { + href: 'https://rest.api/processesList/456', + }, + }, + }); + + const runningProcessRD = new RemoteData(0, 0, 0, RequestEntryState.Success, null, runningProcessPagination); + + spyOn(processDataService, 'searchBy').and.returnValue( + of(runningProcessRD), + ); + + expect(processDataService.searchBy).toHaveBeenCalledTimes(0); + expect(requestService.setStaleByHrefSubstring).toHaveBeenCalledTimes(0); + + let sub = processDataService.autoRefreshingSearchBy('id', 'byProperty', new FindListOptions(), 200).subscribe(); + expect(processDataService.searchBy).toHaveBeenCalledTimes(1); + + tick(250); + + expect(requestService.setStaleByHrefSubstring).toHaveBeenCalledTimes(1); + + sub.unsubscribe(); + })); }); }); diff --git a/src/app/core/data/processes/process-data.service.ts b/src/app/core/data/processes/process-data.service.ts index 3bf34eb650d..b39a500c804 100644 --- a/src/app/core/data/processes/process-data.service.ts +++ b/src/app/core/data/processes/process-data.service.ts @@ -1,30 +1,66 @@ -import { Injectable } from '@angular/core'; -import { RequestService } from '../request.service'; +import { + Inject, + Injectable, + InjectionToken, + NgZone, +} from '@angular/core'; +import { + Observable, + Subscription, +} from 'rxjs'; +import { + distinctUntilChanged, + filter, + find, + switchMap, +} from 'rxjs/operators'; +import { ProcessStatus } from 'src/app/process-page/processes/process-status.model'; + +import { Process } from '../../../process-page/processes/process.model'; +import { hasValue } from '../../../shared/empty.util'; +import { NotificationsService } from '../../../shared/notifications/notifications.service'; +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 { HALEndpointService } from '../../shared/hal-endpoint.service'; -import { Process } from '../../../process-page/processes/process.model'; -import { PROCESS } from '../../../process-page/processes/process.resource-type'; -import { Observable } from 'rxjs'; -import { switchMap } from 'rxjs/operators'; -import { PaginatedList } from '../paginated-list.model'; import { Bitstream } from '../../shared/bitstream.model'; -import { RemoteData } from '../remote-data'; -import { BitstreamDataService } from '../bitstream-data.service'; +import { HALEndpointService } from '../../shared/hal-endpoint.service'; +import { NoContent } from '../../shared/NoContent.model'; +import { getAllCompletedRemoteData } from '../../shared/operators'; +import { + DeleteData, + DeleteDataImpl, +} from '../base/delete-data'; +import { + FindAllData, + FindAllDataImpl, +} from '../base/find-all-data'; import { IdentifiableDataService } from '../base/identifiable-data.service'; -import { FollowLinkConfig } from '../../../shared/utils/follow-link-config.model'; -import { FindAllData, FindAllDataImpl } from '../base/find-all-data'; +import { + SearchData, + SearchDataImpl, +} from '../base/search-data'; +import { BitstreamDataService } from '../bitstream-data.service'; import { FindListOptions } from '../find-list-options.model'; -import { dataService } from '../base/data-service.decorator'; -import { DeleteData, DeleteDataImpl } from '../base/delete-data'; -import { NotificationsService } from '../../../shared/notifications/notifications.service'; -import { NoContent } from '../../shared/NoContent.model'; +import { PaginatedList } from '../paginated-list.model'; +import { RemoteData } from '../remote-data'; +import { RequestService } from '../request.service'; -@Injectable() -@dataService(PROCESS) -export class ProcessDataService extends IdentifiableDataService implements FindAllData, DeleteData { +/** + * Create an InjectionToken for the default JS setTimeout function, purely so we can mock it during + * testing. (fakeAsync isn't working for this case) + */ +export const TIMER_FACTORY = new InjectionToken<(callback: (...args: any[]) => void, ms?: number, ...args: any[]) => NodeJS.Timeout>('timer', { + providedIn: 'root', + factory: () => setTimeout, +}); + +@Injectable({ providedIn: 'root' }) +export class ProcessDataService extends IdentifiableDataService implements FindAllData, DeleteData, SearchData { private findAllData: FindAllData; private deleteData: DeleteData; + private searchData: SearchData; + protected activelyBeingPolled: Map = new Map(); + protected subs: Map = new Map(); constructor( protected requestService: RequestService, @@ -33,11 +69,30 @@ export class ProcessDataService extends IdentifiableDataService impleme protected halService: HALEndpointService, protected bitstreamDataService: BitstreamDataService, protected notificationsService: NotificationsService, + protected zone: NgZone, + @Inject(TIMER_FACTORY) protected timer: (callback: (...args: any[]) => void, ms?: number, ...args: any[]) => NodeJS.Timeout, ) { super('processes', requestService, rdbService, objectCache, halService); this.findAllData = new FindAllDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.responseMsToLive); this.deleteData = new DeleteDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, notificationsService, this.responseMsToLive, this.constructIdEndpoint); + this.searchData = new SearchDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.responseMsToLive); + } + + /** + * Return true if the given process has the given status + * @protected + */ + protected static statusIs(process: Process, status: ProcessStatus): boolean { + return hasValue(process) && process.processStatus === status; + } + + /** + * Return true if the given process has the status COMPLETED or FAILED + */ + public static hasCompletedOrFailed(process: Process): boolean { + return ProcessDataService.statusIs(process, ProcessStatus.COMPLETED) || + ProcessDataService.statusIs(process, ProcessStatus.FAILED); } /** @@ -46,7 +101,7 @@ export class ProcessDataService extends IdentifiableDataService impleme */ getFilesEndpoint(processId: string): Observable { return this.getBrowseEndpoint().pipe( - switchMap((href) => this.halService.getEndpoint('files', `${href}/${processId}`)) + switchMap((href) => this.halService.getEndpoint('files', `${href}/${processId}`)), ); } @@ -77,6 +132,71 @@ export class ProcessDataService extends IdentifiableDataService impleme return this.findAllData.findAll(options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow); } + /** + * @param searchMethod The search method for the Process + * @param options The FindListOptions object + * @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's + * no valid cached version. Defaults to true. + * @param reRequestOnStale Whether the request should automatically be re- + * requested after the response becomes stale. + * @param linksToFollow List of {@link FollowLinkConfig} that indicate which + * {@link HALLink}s should automatically be resolved. + * @return {Observable>>} + * Return an observable that emits a paginated list of processes + */ + searchBy(searchMethod: string, options?: FindListOptions, useCachedVersionIfAvailable?: boolean, reRequestOnStale?: boolean, ...linksToFollow: FollowLinkConfig[]): Observable>> { + return this.searchData.searchBy(searchMethod, options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow); + } + + /** + * @param id The id for this auto-refreshing search. Used to stop + * auto-refreshing afterwards, and ensure we're not + * auto-refreshing the same thing multiple times. + * @param searchMethod The search method for the Process + * @param options The FindListOptions object + * @param pollingIntervalInMs The interval by which the search will be repeated + * @param linksToFollow List of {@link FollowLinkConfig} that indicate which + * {@link HALLink}s should automatically be resolved. + * @return {Observable>>} + * Return an observable that emits a paginated list of processes every interval + */ + autoRefreshingSearchBy(id: string, searchMethod: string, options?: FindListOptions, pollingIntervalInMs: number = 5000, ...linksToFollow: FollowLinkConfig[]): Observable>> { + + const result$ = this.searchBy(searchMethod, options, true, true, ...linksToFollow).pipe( + getAllCompletedRemoteData(), + ); + + const sub = result$.pipe( + filter(() => + !this.activelyBeingPolled.has(id), + ), + ).subscribe((processListRd: RemoteData>) => { + this.clearCurrentTimeout(id); + const nextTimeout = this.timer(() => { + this.activelyBeingPolled.delete(id); + this.requestService.setStaleByHrefSubstring(processListRd.payload._links.self.href); + }, pollingIntervalInMs); + + this.activelyBeingPolled.set(id, nextTimeout); + }); + + this.subs.set(id, sub); + + return result$; + } + + /** + * Stop auto-refreshing the request with the given id + * @param id the id of the request to stop automatically refreshing + */ + stopAutoRefreshing(id: string) { + this.clearCurrentTimeout(id); + if (hasValue(this.subs.get(id))) { + this.subs.get(id).unsubscribe(); + this.subs.delete(id); + } + } + /** * Delete an existing object on the server * @param objectId The id of the object to be removed @@ -101,4 +221,74 @@ export class ProcessDataService extends IdentifiableDataService impleme public deleteByHref(href: string, copyVirtualMetadata?: string[]): Observable> { return this.deleteData.deleteByHref(href, copyVirtualMetadata); } + + /** + * Clear the timeout for the given id, if that timeout exists + * @protected + */ + protected clearCurrentTimeout(id: string): void { + const timeout = this.activelyBeingPolled.get(id); + if (hasValue(timeout)) { + clearTimeout(timeout); + } + this.activelyBeingPolled.delete(id); + } + + /** + * Poll the process with the given ID, using the given interval, until that process either + * completes successfully or fails + * + * Return an Observable for the Process. Note that this will also emit while the + * process is still running. It will only emit again when the process (not the RemoteData!) changes + * status. That makes it more convenient to retrieve that process for a component: you can replace + * a findByID call with this method, rather than having to do a separate findById, and then call + * this method + * + * @param processId The ID of the {@link Process} to poll + * @param pollingIntervalInMs The interval for how often the request needs to be polled + * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be + * automatically resolved + */ + public autoRefreshUntilCompletion(processId: string, pollingIntervalInMs = 5000, ...linksToFollow: FollowLinkConfig[]): Observable> { + const process$: Observable> = this.findById(processId, true, true, ...linksToFollow) + .pipe( + getAllCompletedRemoteData(), + ); + + // Create a subscription that marks the data as stale if the process hasn't been completed and + // the polling interval time has been exceeded. + const sub = process$.pipe( + filter((processRD: RemoteData) => + !ProcessDataService.hasCompletedOrFailed(processRD.payload) && + !this.activelyBeingPolled.has(processId), + ), + ).subscribe((processRD: RemoteData) => { + this.clearCurrentTimeout(processId); + if (processRD.hasSucceeded) { + const nextTimeout = this.timer(() => { + this.activelyBeingPolled.delete(processId); + this.invalidateByHref(processRD.payload._links.self.href); + }, pollingIntervalInMs); + + this.activelyBeingPolled.set(processId, nextTimeout); + } + }); + + this.subs.set(processId, sub); + + // When the process completes create a one off subscription (the `find` completes the + // observable) that unsubscribes the previous one, removes the processId from the list of + // processes being polled and clears any running timeouts + process$.pipe( + find((processRD: RemoteData) => ProcessDataService.hasCompletedOrFailed(processRD.payload)), + ).subscribe(() => { + this.stopAutoRefreshing(processId); + }); + + return process$.pipe( + distinctUntilChanged((previous: RemoteData, current: RemoteData) => + previous.payload?.processStatus === current.payload?.processStatus, + ), + ); + } } diff --git a/src/app/core/data/processes/script-data.service.ts b/src/app/core/data/processes/script-data.service.ts index d9c92cb1d21..e61da4db636 100644 --- a/src/app/core/data/processes/script-data.service.ts +++ b/src/app/core/data/processes/script-data.service.ts @@ -1,34 +1,38 @@ import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; +import { + map, + take, +} from 'rxjs/operators'; + +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 { hasValue } from '../../../shared/empty.util'; +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 { HALEndpointService } from '../../shared/hal-endpoint.service'; -import { Script } from '../../../process-page/scripts/script.model'; -import { ProcessParameter } from '../../../process-page/processes/process-parameter.model'; -import { map, take } from 'rxjs/operators'; +import { getFirstCompletedRemoteData } from '../../shared/operators'; import { URLCombiner } from '../../url-combiner/url-combiner'; +import { + FindAllData, + FindAllDataImpl, +} from '../base/find-all-data'; +import { IdentifiableDataService } from '../base/identifiable-data.service'; +import { FindListOptions } from '../find-list-options.model'; +import { PaginatedList } from '../paginated-list.model'; import { RemoteData } from '../remote-data'; import { MultipartPostRequest } from '../request.models'; import { RequestService } from '../request.service'; -import { Observable } from 'rxjs'; -import { SCRIPT } from '../../../process-page/scripts/script.resource-type'; -import { Process } from '../../../process-page/processes/process.model'; -import { hasValue } from '../../../shared/empty.util'; -import { getFirstCompletedRemoteData } from '../../shared/operators'; import { RestRequest } from '../rest-request.model'; -import { IdentifiableDataService } from '../base/identifiable-data.service'; -import { FindAllData, FindAllDataImpl } from '../base/find-all-data'; -import { FindListOptions } from '../find-list-options.model'; -import { FollowLinkConfig } from '../../../shared/utils/follow-link-config.model'; -import { PaginatedList } from '../paginated-list.model'; -import { dataService } from '../base/data-service.decorator'; export const METADATA_IMPORT_SCRIPT_NAME = 'metadata-import'; 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 00000000000..de13acb3036 --- /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 50f772097db..00000000000 --- a/src/app/shared/utils/markdown.pipe.spec.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { MarkdownPipe } from './markdown.pipe'; -import { TestBed } from '@angular/core/testing'; -import { APP_CONFIG } from '../../../config/app-config.interface'; -import { environment } from '../../../environments/environment'; - -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 e494a826138..00000000000 --- a/src/app/shared/utils/markdown.pipe.ts +++ /dev/null @@ -1,103 +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' - ], - 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' - ] - }, - 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 8725246eebe..3112d4870be 100644 --- a/src/app/shared/utils/metadatafield-validator.directive.ts +++ b/src/app/shared/utils/metadatafield-validator.directive.ts @@ -1,7 +1,24 @@ -import { Directive, Injectable } from '@angular/core'; -import { AbstractControl, AsyncValidator, NG_VALIDATORS, ValidationErrors } from '@angular/forms'; -import { map, switchMap, take } from 'rxjs/operators'; -import { of as observableOf, timer as observableTimer, Observable } from 'rxjs'; +import { + Directive, + Injectable, +} from '@angular/core'; +import { + AbstractControl, + AsyncValidator, + NG_VALIDATORS, + ValidationErrors, +} from '@angular/forms'; +import { + Observable, + of as observableOf, + timer as observableTimer, +} from 'rxjs'; +import { + map, + switchMap, + take, +} from 'rxjs/operators'; + import { MetadataFieldDataService } from '../../core/data/metadata-field-data.service'; import { PaginatedList } from '../../core/data/paginated-list.model'; import { RemoteData } from '../../core/data/remote-data'; @@ -15,8 +32,9 @@ import { getFirstSucceededRemoteData } from '../../core/shared/operators'; selector: '[ngModel][dsMetadataFieldValidator]', // We add our directive to the list of existing validators providers: [ - { provide: NG_VALIDATORS, useExisting: MetadataFieldValidator, multi: true } - ] + { provide: NG_VALIDATORS, useExisting: MetadataFieldValidator, multi: true }, + ], + standalone: true, }) @Injectable({ providedIn: 'root' }) export class MetadataFieldValidator implements AsyncValidator { @@ -48,13 +66,13 @@ export class MetadataFieldValidator implements AsyncValidator { } else if (matchingFieldRD.payload.pageInfo.totalElements === 1) { return null; } - }) + }), ); res.pipe(take(1)).subscribe(); return res; - }) + }), ); resTimer.pipe(take(1)).subscribe(); return resTimer; diff --git a/src/app/shared/utils/object-keys-pipe.ts b/src/app/shared/utils/object-keys-pipe.ts index 0d6a2c82229..8ade3f236b3 100644 --- a/src/app/shared/utils/object-keys-pipe.ts +++ b/src/app/shared/utils/object-keys-pipe.ts @@ -1,6 +1,12 @@ -import { PipeTransform, Pipe } from '@angular/core'; +import { + Pipe, + 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 982e3342e00..c52426fef49 100644 --- a/src/app/shared/utils/object-ngfor.pipe.ts +++ b/src/app/shared/utils/object-ngfor.pipe.ts @@ -1,4 +1,7 @@ -import { Pipe, PipeTransform } from '@angular/core'; +import { + Pipe, + PipeTransform, +} from '@angular/core'; /** * Pipe that allows to iterate over an object and to access to entry key and value : @@ -9,10 +12,11 @@ import { Pipe, PipeTransform } from '@angular/core'; * */ @Pipe({ - name: 'dsObjNgFor' + name: 'dsObjNgFor', + standalone: true, }) export class ObjNgFor implements PipeTransform { transform(value: any, args: any[] = null): any { - return Object.keys(value).map((key) => Object.assign({ key }, {value: value[key]})); + return Object.keys(value).map((key) => Object.assign({ key }, { value: value[key] })); } } diff --git a/src/app/shared/utils/object-values-pipe.ts b/src/app/shared/utils/object-values-pipe.ts index 5fab8bf8419..6b862afff95 100644 --- a/src/app/shared/utils/object-values-pipe.ts +++ b/src/app/shared/utils/object-values-pipe.ts @@ -1,9 +1,14 @@ -import { PipeTransform, Pipe } from '@angular/core'; +import { + Pipe, + PipeTransform, +} from '@angular/core'; + import { isNotEmpty } from '../empty.util'; @Pipe({ name: 'dsObjectValues', - pure: true + 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.spec.ts b/src/app/shared/utils/relation-query.utils.spec.ts index f40c3314974..1f2a725a9f7 100644 --- a/src/app/shared/utils/relation-query.utils.spec.ts +++ b/src/app/shared/utils/relation-query.utils.spec.ts @@ -1,4 +1,7 @@ -import { getFilterByRelation, getQueryByRelations } from './relation-query.utils'; +import { + getFilterByRelation, + getQueryByRelations, +} from './relation-query.utils'; describe('Relation Query Utils', () => { const relationtype = 'isAuthorOfPublication'; diff --git a/src/app/shared/utils/relation-query.utils.ts b/src/app/shared/utils/relation-query.utils.ts index 62a69075fcb..158744e78c3 100644 --- a/src/app/shared/utils/relation-query.utils.ts +++ b/src/app/shared/utils/relation-query.utils.ts @@ -1,5 +1,9 @@ -import { followLink, FollowLinkConfig } from './follow-link-config.model'; +import { Item } from '../../core/shared/item.model'; import { Relationship } from '../../core/shared/item-relationships/relationship.model'; +import { + followLink, + FollowLinkConfig, +} from './follow-link-config.model'; /** * Get the query for looking up items by relation type @@ -21,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 ef41ad43094..6825f058dd1 100644 --- a/src/app/shared/utils/require-file.validator.ts +++ b/src/app/shared/utils/require-file.validator.ts @@ -1,22 +1,27 @@ -import {Directive} from '@angular/core'; -import {NG_VALIDATORS, Validator, UntypedFormControl} from '@angular/forms'; +import { Directive } from '@angular/core'; +import { + NG_VALIDATORS, + UntypedFormControl, + Validator, +} from '@angular/forms'; @Directive({ // eslint-disable-next-line @angular-eslint/directive-selector - selector: '[requireFile]', - providers: [ - { provide: NG_VALIDATORS, useExisting: FileValidator, multi: true }, - ] + selector: '[requireFile]', + providers: [ + { provide: NG_VALIDATORS, useExisting: FileValidator, multi: true }, + ], + standalone: true, }) /** * Validator directive to validate if a file is selected */ export class FileValidator implements Validator { - static validate(c: UntypedFormControl): {[key: string]: any} { - return c.value == null || c.value.length === 0 ? { required : true } : null; - } + static validate(c: UntypedFormControl): {[key: string]: any} { + return c.value == null || c.value.length === 0 ? { required : true } : null; + } - validate(c: UntypedFormControl): {[key: string]: any} { - return FileValidator.validate(c); - } + validate(c: UntypedFormControl): {[key: string]: any} { + return FileValidator.validate(c); + } } diff --git a/src/app/shared/utils/route.utils.spec.ts b/src/app/shared/utils/route.utils.spec.ts index 7ec6c879d85..aa91a886dbe 100644 --- a/src/app/shared/utils/route.utils.spec.ts +++ b/src/app/shared/utils/route.utils.spec.ts @@ -7,12 +7,12 @@ describe('Route Utils', () => { primary: { segments: [ { path: 'test' }, - { path: 'path' } - ] - } + { path: 'path' }, + ], + }, - } - } + }, + }, }; const router = { parseUrl: () => urlTree } as any; it('Should return the correct current path based on the router', () => { diff --git a/src/app/shared/utils/route.utils.ts b/src/app/shared/utils/route.utils.ts index 8f7bb9120de..9f45b7ec239 100644 --- a/src/app/shared/utils/route.utils.ts +++ b/src/app/shared/utils/route.utils.ts @@ -1,6 +1,10 @@ -import { ActivatedRouteSnapshot, Router } from '@angular/router'; -import { hasValue } from '../empty.util'; +import { + ActivatedRouteSnapshot, + Router, +} from '@angular/router'; + import { URLCombiner } from '../../core/url-combiner/url-combiner'; +import { hasValue } from '../empty.util'; /** * Util function to retrieve the current path (without query parameters) the user is on diff --git a/src/app/shared/utils/safe-url-pipe.ts b/src/app/shared/utils/safe-url-pipe.ts index 3f35ed92627..6cb9d9fb1bf 100644 --- a/src/app/shared/utils/safe-url-pipe.ts +++ b/src/app/shared/utils/safe-url-pipe.ts @@ -1,4 +1,7 @@ -import { Pipe, PipeTransform } from '@angular/core'; +import { + Pipe, + PipeTransform, +} from '@angular/core'; import { DomSanitizer } from '@angular/platform-browser'; /** @@ -6,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.spec.ts b/src/app/shared/utils/short-number.pipe.spec.ts index 99fb246f54f..9fda54f4706 100644 --- a/src/app/shared/utils/short-number.pipe.spec.ts +++ b/src/app/shared/utils/short-number.pipe.spec.ts @@ -9,7 +9,7 @@ describe('ShortNumber Pipe', () => { beforeEach(() => { TestBed.configureTestingModule({ providers: [ - ShortNumberPipe + ShortNumberPipe, ], }).compileComponents(); @@ -19,78 +19,78 @@ describe('ShortNumber Pipe', () => { it('should not transform with an invalid number', async () => { await testTransform( 'tre', - 'tre' + 'tre', ); }); it('should not transform with an empty string', async () => { await testTransform( '', - '' + '', ); }); it('should not transform with zero', async () => { await testTransform( 0, - '0' + '0', ); }); it('should render 1K', async () => { await testTransform( '1000', - '1K' + '1K', ); }); it('should render 1K', async () => { await testTransform( 1000, - '1K' + '1K', ); }); it('should render 19.3K', async () => { await testTransform( 19300, - '19.3K' + '19.3K', ); }); it('should render 1M', async () => { await testTransform( 1000000, - '1M' + '1M', ); }); it('should render 1B', async () => { await testTransform( 1000000000, - '1B' + '1B', ); }); it('should render 1T', async () => { await testTransform( 1000000000000, - '1T' + '1T', ); }); it('should render 1Q', async () => { await testTransform( 1000000000000000, - '1Q' + '1Q', ); }); async function testTransform(input: any, output: string) { expect( - await shortNumberPipe.transform(input) + await shortNumberPipe.transform(input), ).toMatch( - output + output, ); } }); diff --git a/src/app/shared/utils/short-number.pipe.ts b/src/app/shared/utils/short-number.pipe.ts index e4d5cf83562..f0b458755d0 100644 --- a/src/app/shared/utils/short-number.pipe.ts +++ b/src/app/shared/utils/short-number.pipe.ts @@ -1,9 +1,14 @@ -import { Pipe, PipeTransform } from '@angular/core'; +import { + Pipe, + PipeTransform, +} from '@angular/core'; + import { isEmpty } from '../empty.util'; @Pipe({ - name: 'dsShortNumber' + name: 'dsShortNumber', + standalone: true, }) export class ShortNumberPipe implements PipeTransform { @@ -19,11 +24,11 @@ export class ShortNumberPipe implements PipeTransform { let key = ''; const powers = [ - {key: 'Q', value: Math.pow(10, 15)}, - {key: 'T', value: Math.pow(10, 12)}, - {key: 'B', value: Math.pow(10, 9)}, - {key: 'M', value: Math.pow(10, 6)}, - {key: 'K', value: 1000} + { key: 'Q', value: Math.pow(10, 15) }, + { key: 'T', value: Math.pow(10, 12) }, + { key: 'B', value: Math.pow(10, 9) }, + { key: 'M', value: Math.pow(10, 6) }, + { key: 'K', value: 1000 }, ]; for (let i = 0; i < powers.length; i++) { diff --git a/src/app/shared/utils/split.pipe.ts b/src/app/shared/utils/split.pipe.ts new file mode 100644 index 00000000000..c84e7895f18 --- /dev/null +++ b/src/app/shared/utils/split.pipe.ts @@ -0,0 +1,21 @@ +import { + Pipe, + PipeTransform, +} from '@angular/core'; + +/** + * Custom pipe to split a string into an array of substrings based on a specified separator. + * @param value - The string to be split. + * @param separator - The separator used to split the string. + * @returns An array of substrings. + */ +@Pipe({ + name: 'dsSplit', + standalone: true, +}) +export class SplitPipe implements PipeTransform { + transform(value: string, separator: string): string[] { + return value.split(separator); + } + +} diff --git a/src/app/shared/utils/truncate.pipe.ts b/src/app/shared/utils/truncate.pipe.ts index f841535bb81..2955be8f5d1 100644 --- a/src/app/shared/utils/truncate.pipe.ts +++ b/src/app/shared/utils/truncate.pipe.ts @@ -1,4 +1,8 @@ -import { Pipe, PipeTransform } from '@angular/core'; +import { + Pipe, + PipeTransform, +} from '@angular/core'; + import { hasValue } from '../empty.util'; /** @@ -6,7 +10,8 @@ import { hasValue } from '../empty.util'; * Default value: 10 */ @Pipe({ - name: 'dsTruncate' + name: 'dsTruncate', + standalone: true, }) export class TruncatePipe implements PipeTransform { diff --git a/src/app/shared/utils/validator.functions.ts b/src/app/shared/utils/validator.functions.ts index 164ac7885e3..da3b94d3031 100644 --- a/src/app/shared/utils/validator.functions.ts +++ b/src/app/shared/utils/validator.functions.ts @@ -1,4 +1,8 @@ -import { AbstractControl, ValidatorFn } from '@angular/forms'; +import { + AbstractControl, + ValidatorFn, +} from '@angular/forms'; + import { isNotEmpty } from '../empty.util'; /** diff --git a/src/app/shared/utils/var.directive.ts b/src/app/shared/utils/var.directive.ts index 497fc91a8bb..eaa8a6fdc1a 100644 --- a/src/app/shared/utils/var.directive.ts +++ b/src/app/shared/utils/var.directive.ts @@ -1,8 +1,14 @@ -import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core'; +import { + Directive, + Input, + TemplateRef, + ViewContainerRef, +} from '@angular/core'; /* 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 45d219eb916..74ee22533d4 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 @@ -1,35 +1,38 @@
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 248a8433161..e4d92bece46 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 @@ -1,17 +1,31 @@ -import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; -import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; +import { + ChangeDetectionStrategy, + Component, +} from '@angular/core'; +import { + ComponentFixture, + fakeAsync, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { RouterTestingModule } from '@angular/router/testing'; -import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { + TranslateLoader, + TranslateModule, +} from '@ngx-translate/core'; -import { TranslateLoaderMock } from '../mocks/translate-loader.mock'; import { SearchService } from '../../core/shared/search/search.service'; -import { ViewModeSwitchComponent } from './view-mode-switch.component'; -import { SearchServiceStub } from '../testing/search-service.stub'; import { ViewMode } from '../../core/shared/view-mode.model'; -import { BrowserOnlyMockPipe } from '../testing/browser-only-mock.pipe'; +import { TranslateLoaderMock } from '../mocks/translate-loader.mock'; +import { SearchServiceStub } from '../testing/search-service.stub'; +import { ViewModeSwitchComponent } from './view-mode-switch.component'; -@Component({ template: '' }) +@Component({ + template: '', + standalone: true, +}) class DummyComponent { } @@ -28,23 +42,20 @@ describe('ViewModeSwitchComponent', () => { TranslateModule.forRoot({ loader: { provide: TranslateLoader, - useClass: TranslateLoaderMock - } + useClass: TranslateLoaderMock, + }, }), RouterTestingModule.withRoutes([ { path: 'search', component: DummyComponent, pathMatch: 'full' }, - ]) - ], - declarations: [ + ]), ViewModeSwitchComponent, DummyComponent, - BrowserOnlyMockPipe, ], providers: [ { provide: SearchService, useValue: searchService }, ], }).overrideComponent(ViewModeSwitchComponent, { - set: { changeDetection: ChangeDetectionStrategy.Default } + set: { changeDetection: ChangeDetectionStrategy.Default }, }).compileComponents(); })); 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 95f35abf175..baed3c93363 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,13 +1,29 @@ -import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'; - +import { NgIf } from '@angular/common'; +import { + Component, + EventEmitter, + Input, + OnDestroy, + OnInit, + Output, +} from '@angular/core'; +import { + Router, + RouterLink, + RouterLinkActive, +} from '@angular/router'; +import { TranslateModule } from '@ngx-translate/core'; import { Subscription } from 'rxjs'; +import { filter } from 'rxjs/operators'; import { SearchService } from '../../core/shared/search/search.service'; import { ViewMode } from '../../core/shared/view-mode.model'; -import { isEmpty, isNotEmpty } from '../empty.util'; +import { + isEmpty, + isNotEmpty, +} from '../empty.util'; +import { BrowserOnlyPipe } from '../utils/browser-only.pipe'; import { currentPath } from '../utils/route.utils'; -import { Router } from '@angular/router'; -import { filter } from 'rxjs/operators'; /** * Component to switch between list and grid views. @@ -15,7 +31,9 @@ import { filter } from 'rxjs/operators'; @Component({ selector: 'ds-view-mode-switch', styleUrls: ['./view-mode-switch.component.scss'], - templateUrl: './view-mode-switch.component.html' + templateUrl: './view-mode-switch.component.html', + standalone: true, + imports: [NgIf, RouterLink, RouterLinkActive, TranslateModule, BrowserOnlyPipe], }) export class ViewModeSwitchComponent implements OnInit, OnDestroy { @@ -62,7 +80,7 @@ export class ViewModeSwitchComponent implements OnInit, OnDestroy { } this.sub = this.searchService.getViewMode().pipe( - filter((viewMode: ViewMode) => isNotEmpty(viewMode)) + filter((viewMode: ViewMode) => isNotEmpty(viewMode)), ).subscribe((viewMode: ViewMode) => { this.currentMode = viewMode; }); 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 d5bb80dfde9..c78fef0521e 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 @@ -1,20 +1,27 @@ -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { CollectionStatisticsPageComponent } from './collection-statistics-page.component'; -import { StatisticsTableComponent } from '../statistics-table/statistics-table.component'; -import { TranslateModule } from '@ngx-translate/core'; -import { ActivatedRoute, Router } from '@angular/router'; -import { UsageReportDataService } from '../../core/statistics/usage-report-data.service'; -import { of as observableOf } from 'rxjs'; -import { Collection } from '../../core/shared/collection.model'; +import { CommonModule } from '@angular/common'; import { DebugElement } from '@angular/core'; +import { + ComponentFixture, + TestBed, + waitForAsync, +} from '@angular/core/testing'; import { By } from '@angular/platform-browser'; -import { UsageReport } from '../../core/statistics/models/usage-report.model'; -import { SharedModule } from '../../shared/shared.module'; -import { CommonModule } from '@angular/common'; +import { + ActivatedRoute, + Router, +} from '@angular/router'; +import { TranslateModule } from '@ngx-translate/core'; +import { of as observableOf } from 'rxjs'; + +import { AuthService } from '../../core/auth/auth.service'; import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service'; -import { AuthService } from '../../core/auth/auth.service'; +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 { StatisticsTableComponent } from '../statistics-table/statistics-table.component'; +import { CollectionStatisticsPageComponent } from './collection-statistics-page.component'; describe('CollectionStatisticsPageComponent', () => { @@ -29,9 +36,9 @@ describe('CollectionStatisticsPageComponent', () => { scope: createSuccessfulRemoteDataObject( Object.assign(new Collection(), { id: 'collection_id', - }) - ) - }) + }), + ), + }), }; const router = { @@ -47,9 +54,9 @@ describe('CollectionStatisticsPageComponent', () => { new UsageReport(), { id: `${scope}-${type}-report`, points: [], - } - ) - ) + }, + ), + ), ); const nameService = { @@ -58,16 +65,13 @@ describe('CollectionStatisticsPageComponent', () => { const authService = jasmine.createSpyObj('authService', { isAuthenticated: observableOf(true), - setRedirectUrl: {} + setRedirectUrl: {}, }); TestBed.configureTestingModule({ 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 4875ce39cd4..e07a506b7cf 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,20 +1,24 @@ +import { CommonModule } from '@angular/common'; import { Component } from '@angular/core'; -import { StatisticsPageComponent } from '../statistics-page/statistics-page.component'; -import { UsageReportDataService } from '../../core/statistics/usage-report-data.service'; -import { ActivatedRoute , Router} from '@angular/router'; +import { TranslateModule } from '@ngx-translate/core'; + import { Collection } from '../../core/shared/collection.model'; -import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; -import { AuthService } from '../../core/auth/auth.service'; +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'] + 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. @@ -25,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 2e63f83b8f1..e29e37880f4 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 @@ -1,20 +1,27 @@ -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { CommunityStatisticsPageComponent } from './community-statistics-page.component'; -import { StatisticsTableComponent } from '../statistics-table/statistics-table.component'; -import { TranslateModule } from '@ngx-translate/core'; -import { ActivatedRoute, Router } from '@angular/router'; -import { UsageReportDataService } from '../../core/statistics/usage-report-data.service'; -import { of as observableOf } from 'rxjs'; -import { Community } from '../../core/shared/community.model'; +import { CommonModule } from '@angular/common'; import { DebugElement } from '@angular/core'; +import { + ComponentFixture, + TestBed, + waitForAsync, +} from '@angular/core/testing'; import { By } from '@angular/platform-browser'; -import { UsageReport } from '../../core/statistics/models/usage-report.model'; -import { SharedModule } from '../../shared/shared.module'; -import { CommonModule } from '@angular/common'; +import { + ActivatedRoute, + Router, +} from '@angular/router'; +import { TranslateModule } from '@ngx-translate/core'; +import { of as observableOf } from 'rxjs'; + +import { AuthService } from '../../core/auth/auth.service'; import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service'; -import { AuthService } from '../../core/auth/auth.service'; +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 { StatisticsTableComponent } from '../statistics-table/statistics-table.component'; +import { CommunityStatisticsPageComponent } from './community-statistics-page.component'; describe('CommunityStatisticsPageComponent', () => { @@ -29,9 +36,9 @@ describe('CommunityStatisticsPageComponent', () => { scope: createSuccessfulRemoteDataObject( Object.assign(new Community(), { id: 'community_id', - }) - ) - }) + }), + ), + }), }; const router = { @@ -47,9 +54,9 @@ describe('CommunityStatisticsPageComponent', () => { new UsageReport(), { id: `${scope}-${type}-report`, points: [], - } - ) - ) + }, + ), + ), ); const nameService = { @@ -58,16 +65,13 @@ describe('CommunityStatisticsPageComponent', () => { const authService = jasmine.createSpyObj('authService', { isAuthenticated: observableOf(true), - setRedirectUrl: {} + setRedirectUrl: {}, }); TestBed.configureTestingModule({ 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 de7a8bbf8cc..72a2c46b9e9 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,20 +1,24 @@ +import { CommonModule } from '@angular/common'; import { Component } from '@angular/core'; -import { StatisticsPageComponent } from '../statistics-page/statistics-page.component'; -import { UsageReportDataService } from '../../core/statistics/usage-report-data.service'; -import { ActivatedRoute, Router } from '@angular/router'; +import { TranslateModule } from '@ngx-translate/core'; + import { Community } from '../../core/shared/community.model'; -import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; -import { AuthService } from '../../core/auth/auth.service'; +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'] + 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. @@ -25,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 88bbca3fba4..f5f3361cce1 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 @@ -1,20 +1,27 @@ -import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing'; -import { ItemStatisticsPageComponent } from './item-statistics-page.component'; -import { StatisticsTableComponent } from '../statistics-table/statistics-table.component'; -import { TranslateModule } from '@ngx-translate/core'; -import { ActivatedRoute, Router } from '@angular/router'; -import { UsageReportDataService } from '../../core/statistics/usage-report-data.service'; -import { of as observableOf } from 'rxjs'; -import { Item } from '../../core/shared/item.model'; +import { CommonModule } from '@angular/common'; import { DebugElement } from '@angular/core'; +import { + ComponentFixture, + TestBed, + waitForAsync, +} from '@angular/core/testing'; import { By } from '@angular/platform-browser'; -import { UsageReport } from '../../core/statistics/models/usage-report.model'; -import { SharedModule } from '../../shared/shared.module'; -import { CommonModule } from '@angular/common'; +import { + ActivatedRoute, + Router, +} from '@angular/router'; +import { TranslateModule } from '@ngx-translate/core'; +import { of as observableOf } from 'rxjs'; + +import { AuthService } from '../../core/auth/auth.service'; import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service'; -import { AuthService } from '../../core/auth/auth.service'; +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 { StatisticsTableComponent } from '../statistics-table/statistics-table.component'; +import { ItemStatisticsPageComponent } from './item-statistics-page.component'; describe('ItemStatisticsPageComponent', () => { @@ -29,9 +36,9 @@ describe('ItemStatisticsPageComponent', () => { scope: createSuccessfulRemoteDataObject( Object.assign(new Item(), { id: 'item_id', - }) - ) - }) + }), + ), + }), }; const router = { @@ -47,9 +54,9 @@ describe('ItemStatisticsPageComponent', () => { new UsageReport(), { id: `${scope}-${type}-report`, points: [], - } - ) - ) + }, + ), + ), ); const nameService = { @@ -58,16 +65,13 @@ describe('ItemStatisticsPageComponent', () => { const authService = jasmine.createSpyObj('authService', { isAuthenticated: observableOf(true), - setRedirectUrl: {} + setRedirectUrl: {}, }); TestBed.configureTestingModule({ 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 f5f107af747..702e39806f7 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,20 +1,24 @@ +import { CommonModule } from '@angular/common'; import { Component } from '@angular/core'; -import { StatisticsPageComponent } from '../statistics-page/statistics-page.component'; -import { UsageReportDataService } from '../../core/statistics/usage-report-data.service'; -import { ActivatedRoute, Router } from '@angular/router'; +import { TranslateModule } from '@ngx-translate/core'; + import { Item } from '../../core/shared/item.model'; -import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; -import { AuthService } from '../../core/auth/auth.service'; +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'] + 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. @@ -26,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 50e26329a97..88d4c04d151 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 @@ -1,4 +1,5 @@ import { Component } from '@angular/core'; + import { ThemedComponent } from '../../shared/theme-support/themed.component'; import { ItemStatisticsPageComponent } from './item-statistics-page.component'; @@ -6,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 3c181c18161..9b23afdd74c 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 @@ -1,20 +1,27 @@ -import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing'; -import { SiteStatisticsPageComponent } from './site-statistics-page.component'; -import { StatisticsTableComponent } from '../statistics-table/statistics-table.component'; -import { TranslateModule } from '@ngx-translate/core'; -import { ActivatedRoute, Router } from '@angular/router'; -import { UsageReportDataService } from '../../core/statistics/usage-report-data.service'; -import { of as observableOf } from 'rxjs'; -import { Site } from '../../core/shared/site.model'; +import { CommonModule } from '@angular/common'; import { DebugElement } from '@angular/core'; +import { + ComponentFixture, + TestBed, + waitForAsync, +} from '@angular/core/testing'; import { By } from '@angular/platform-browser'; -import { UsageReport } from '../../core/statistics/models/usage-report.model'; -import { SharedModule } from '../../shared/shared.module'; -import { CommonModule } from '@angular/common'; +import { + ActivatedRoute, + Router, +} from '@angular/router'; +import { TranslateModule } from '@ngx-translate/core'; +import { of as observableOf } from 'rxjs'; + +import { AuthService } from '../../core/auth/auth.service'; import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service'; import { SiteDataService } from '../../core/data/site-data.service'; -import { AuthService } from '../../core/auth/auth.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 { StatisticsTableComponent } from '../statistics-table/statistics-table.component'; +import { SiteStatisticsPageComponent } from './site-statistics-page.component'; describe('SiteStatisticsPageComponent', () => { @@ -36,7 +43,7 @@ describe('SiteStatisticsPageComponent', () => { new UsageReport(), { id: `site_id-TotalVisits-report`, points: [], - } + }, ), ]), }; @@ -53,21 +60,18 @@ describe('SiteStatisticsPageComponent', () => { href: 'test_site_link', }, }, - })) + })), }; const authService = jasmine.createSpyObj('authService', { isAuthenticated: observableOf(true), - setRedirectUrl: {} + setRedirectUrl: {}, }); TestBed.configureTestingModule({ 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 5eb19bec563..eaed9f5401d 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,22 +1,26 @@ +import { CommonModule } from '@angular/common'; import { Component } from '@angular/core'; -import { StatisticsPageComponent } from '../statistics-page/statistics-page.component'; +import { TranslateModule } from '@ngx-translate/core'; +import { switchMap } from 'rxjs/operators'; + import { SiteDataService } from '../../core/data/site-data.service'; -import { UsageReportDataService } from '../../core/statistics/usage-report-data.service'; -import { ActivatedRoute, Router } from '@angular/router'; import { Site } from '../../core/shared/site.model'; -import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; -import { switchMap } from 'rxjs/operators'; -import { AuthService } from '../../core/auth/auth.service'; +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'] + 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. @@ -25,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 3f841163ed2..3792b33c593 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 @@ -1,4 +1,5 @@ import { Component } from '@angular/core'; + import { ThemedComponent } from '../../shared/theme-support/themed.component'; import { SiteStatisticsPageComponent } from './site-statistics-page.component'; @@ -6,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 00000000000..69bcc6b41c1 --- /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 ef6f68d5570..00000000000 --- a/src/app/statistics-page/statistics-page-routing.module.ts +++ /dev/null @@ -1,87 +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 { StatisticsPageModule } from './statistics-page.module'; -import { CollectionPageResolver } from '../collection-page/collection-page.resolver'; -import { CommunityPageResolver } from '../community-page/community-page.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 { ItemResolver } from '../item-page/item.resolver'; -import { StatisticsAdministratorGuard } from '../core/data/feature-authorization/feature-authorization-guard/statistics-administrator.guard'; - -@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 75726de94cc..00000000000 --- a/src/app/statistics-page/statistics-page.module.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { NgModule } from '@angular/core'; -import { CommonModule } from '@angular/common'; - -import { CoreModule } from '../core/core.module'; -import { SharedModule } from '../shared/shared.module'; -import { StatisticsModule } from '../statistics/statistics.module'; -import { UsageReportDataService } from '../core/statistics/usage-report-data.service'; -import { SiteStatisticsPageComponent } from './site-statistics-page/site-statistics-page.component'; -import { StatisticsTableComponent } from './statistics-table/statistics-table.component'; -import { ItemStatisticsPageComponent } from './item-statistics-page/item-statistics-page.component'; -import { CollectionStatisticsPageComponent } from './collection-statistics-page/collection-statistics-page.component'; -import { CommunityStatisticsPageComponent } from './community-statistics-page/community-statistics-page.component'; -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'; - -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 c6938c7582b..78e736bd3ae 100644 --- a/src/app/statistics-page/statistics-page/statistics-page.component.html +++ b/src/app/statistics-page/statistics-page/statistics-page.component.html @@ -1,17 +1,17 @@
-

{{ 'statistics.header' | translate: { scope: getName(scope) } }} -

+
- + @@ -19,7 +19,7 @@ [report]="report" class="m-2 {{ report.id }}"> -
+
{{ 'statistics.page.no-data' | translate }}
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 75% 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 8b91d54ae8f..e781b89487e 100644 --- a/src/app/statistics-page/statistics-page/statistics-page.component.ts +++ b/src/app/statistics-page/statistics-page/statistics-page.directive.ts @@ -1,27 +1,38 @@ -import { Component, OnInit } from '@angular/core'; -import { combineLatest, Observable } from 'rxjs'; -import { UsageReportDataService } from '../../core/statistics/usage-report-data.service'; -import { map, switchMap } from 'rxjs/operators'; -import { UsageReport } from '../../core/statistics/models/usage-report.model'; +import { + Directive, + inject, + OnInit, +} from '@angular/core'; +import { + ActivatedRoute, + Router, +} from '@angular/router'; +import { + combineLatest, + Observable, +} from 'rxjs'; +import { + map, + switchMap, +} from 'rxjs/operators'; + +import { AuthService } from '../../core/auth/auth.service'; +import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; import { RemoteData } from '../../core/data/remote-data'; +import { redirectOn4xx } from '../../core/shared/authorized.operators'; +import { DSpaceObject } from '../../core/shared/dspace-object.model'; import { + getFirstSucceededRemoteData, getRemoteDataPayload, - getFirstSucceededRemoteData } from '../../core/shared/operators'; -import { DSpaceObject } from '../../core/shared/dspace-object.model'; -import { ActivatedRoute, Router } from '@angular/router'; -import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; -import { AuthService } from '../../core/auth/auth.service'; -import { redirectOn4xx } from '../../core/shared/authorized.operators'; +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. @@ -40,21 +51,18 @@ 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$(); this.reports$ = this.getReports$(); this.hasData$ = this.reports$.pipe( map((reports) => reports.some( - (report) => report.points.length > 0 + (report) => report.points.length > 0, )), ); } @@ -78,7 +86,7 @@ export abstract class StatisticsPageComponent implements return this.scope$.pipe( switchMap((scope) => combineLatest( - this.types.map((type) => this.usageReportService.getStatistic(scope.id, type)) + this.types.map((type) => this.usageReportService.getStatistic(scope.id, type)), ), ), ); diff --git a/src/app/statistics-page/statistics-table/statistics-table.component.html b/src/app/statistics-page/statistics-table/statistics-table.component.html index 5a333b96586..efa9ce43d99 100644 --- a/src/app/statistics-page/statistics-table/statistics-table.component.html +++ b/src/app/statistics-page/statistics-table/statistics-table.component.html @@ -1,9 +1,9 @@
-

+

{{ 'statistics.table.title.' + report.reportType | translate }} -

+
{{messagePrefix + '.table.id' | translate}}
{{ePerson.eperson.id}}
{{eperson.id}} - - {{ dsoNameService.getName(ePerson.eperson) }} + + {{ dsoNameService.getName(eperson) }} - {{messagePrefix + '.table.email' | translate}}: {{ ePerson.eperson.email ? ePerson.eperson.email : '-' }}
- {{messagePrefix + '.table.netid' | translate}}: {{ ePerson.eperson.netid ? ePerson.eperson.netid : '-' }} + {{messagePrefix + '.table.email' | translate}}: {{ eperson.email ? eperson.email : '-' }}
+ {{messagePrefix + '.table.netid' | translate}}: {{ eperson.netid ? eperson.netid : '-' }}
- -
@@ -156,9 +136,10 @@

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

- 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 7c8db399bcd..9a6b2b4f053 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 @@ -1,35 +1,74 @@ import { CommonModule } from '@angular/common'; -import { DebugElement, NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, fakeAsync, flush, inject, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { FormsModule, ReactiveFormsModule } from '@angular/forms'; -import { BrowserModule, By } from '@angular/platform-browser'; -import { Router } from '@angular/router'; +import { + DebugElement, + NO_ERRORS_SCHEMA, +} from '@angular/core'; +import { + ComponentFixture, + fakeAsync, + flush, + inject, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import { + FormsModule, + ReactiveFormsModule, +} from '@angular/forms'; +import { + BrowserModule, + By, +} from '@angular/platform-browser'; +import { + ActivatedRoute, + Router, +} from '@angular/router'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; -import { TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core'; -import { Observable, of as observableOf } from 'rxjs'; +import { + TranslateLoader, + TranslateModule, + TranslateService, +} from '@ngx-translate/core'; +import { + Observable, + of as observableOf, +} from 'rxjs'; + +import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service'; import { RestResponse } from '../../../../core/cache/response.models'; -import { buildPaginatedList, 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 { 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 { GroupMock, GroupMock2 } from '../../../../shared/testing/group-mock'; -import { MembersListComponent } from './members-list.component'; -import { EPersonMock, EPersonMock2 } from '../../../../shared/testing/eperson.mock'; +import { PaginationComponent } from '../../../../shared/pagination/pagination.component'; import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils'; -import { getMockTranslateService } from '../../../../shared/mocks/translate.service.mock'; -import { getMockFormBuilderService } from '../../../../shared/mocks/form-builder-service.mock'; -import { TranslateLoaderMock } from '../../../../shared/testing/translate-loader.mock'; +import { ActivatedRouteStub } from '../../../../shared/testing/active-router.stub'; +import { + EPersonMock, + EPersonMock2, +} from '../../../../shared/testing/eperson.mock'; +import { GroupMock } from '../../../../shared/testing/group-mock'; import { NotificationsServiceStub } from '../../../../shared/testing/notifications-service.stub'; -import { RouterMock } from '../../../../shared/mocks/router.mock'; -import { PaginationService } from '../../../../core/pagination/pagination.service'; import { PaginationServiceStub } from '../../../../shared/testing/pagination-service.stub'; -import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service'; -import { DSONameServiceMock } from '../../../../shared/mocks/dso-name.service.mock'; +import { TranslateLoaderMock } from '../../../../shared/testing/translate-loader.mock'; +import { MembersListComponent } from './members-list.component'; + +// todo: optimize imports describe('MembersListComponent', () => { let component: MembersListComponent; @@ -39,28 +78,26 @@ describe('MembersListComponent', () => { let ePersonDataServiceStub: any; let groupsDataServiceStub: any; let activeGroup; - let allEPersons: EPerson[]; - let allGroups: Group[]; let epersonMembers: EPerson[]; - let subgroupMembers: Group[]; + let epersonNonMembers: EPerson[]; let paginationService; beforeEach(waitForAsync(() => { activeGroup = GroupMock; epersonMembers = [EPersonMock2]; - subgroupMembers = [GroupMock2]; - allEPersons = [EPersonMock, EPersonMock2]; - allGroups = [GroupMock, GroupMock2]; + epersonNonMembers = [EPersonMock]; ePersonDataServiceStub = { activeGroup: activeGroup, epersonMembers: epersonMembers, - subgroupMembers: subgroupMembers, + epersonNonMembers: epersonNonMembers, + // This method is used to get all the current members findListByHref(_href: string): Observable>> { return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), groupsDataServiceStub.getEPersonMembers())); }, - searchByScope(scope: string, query: string): Observable>> { + // This method is used to search across *non-members* + searchNonMembers(query: string, group: string): Observable>> { if (query === '') { - return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), allEPersons)); + return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), epersonNonMembers)); } return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), [])); }, @@ -70,29 +107,26 @@ describe('MembersListComponent', () => { clearLinkRequests() { // empty }, - getEPeoplePageRouterLink(): string { - return '/access-control/epeople'; - } }; groupsDataServiceStub = { activeGroup: activeGroup, epersonMembers: epersonMembers, - subgroupMembers: subgroupMembers, - allGroups: allGroups, + epersonNonMembers: epersonNonMembers, getActiveGroup(): Observable { return observableOf(activeGroup); }, getEPersonMembers() { return this.epersonMembers; }, - searchGroups(query: string): Observable>> { - if (query === '') { - return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), this.allGroups)); - } - return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), [])); - }, - addMemberToGroup(parentGroup, eperson: EPerson): Observable { - this.epersonMembers = [...this.epersonMembers, eperson]; + addMemberToGroup(parentGroup, epersonToAdd: EPerson): Observable { + // Add eperson to list of members + this.epersonMembers = [...this.epersonMembers, epersonToAdd]; + // Remove eperson from list of non-members + this.epersonNonMembers.forEach( (eperson: EPerson, index: number) => { + if (eperson.id === epersonToAdd.id) { + this.epersonNonMembers.splice(index, 1); + } + }); return observableOf(new RestResponse(true, 200, 'Success')); }, clearGroupsRequests() { @@ -105,16 +139,16 @@ describe('MembersListComponent', () => { return '/access-control/groups/' + group.id; }, deleteMemberFromGroup(parentGroup, epersonToDelete: EPerson): Observable { - this.epersonMembers = this.epersonMembers.find((eperson: EPerson) => { - if (eperson.id !== epersonToDelete.id) { - return eperson; + // Remove eperson from list of members + this.epersonMembers.forEach( (eperson: EPerson, index: number) => { + if (eperson.id === epersonToDelete.id) { + this.epersonMembers.splice(index, 1); } }); - if (this.epersonMembers === undefined) { - this.epersonMembers = []; - } + // Add eperson to list of non-members + this.epersonNonMembers = [...this.epersonNonMembers, epersonToDelete]; return observableOf(new RestResponse(true, 200, 'Success')); - } + }, }; builderService = getMockFormBuilderService(); translateService = getMockTranslateService(); @@ -125,11 +159,9 @@ describe('MembersListComponent', () => { TranslateModule.forRoot({ loader: { provide: TranslateLoader, - useClass: TranslateLoaderMock - } - }), - ], - declarations: [MembersListComponent], + useClass: TranslateLoaderMock, + }, + }), MembersListComponent], providers: [MembersListComponent, { provide: EPersonDataService, useValue: ePersonDataServiceStub }, { provide: GroupDataService, useValue: groupsDataServiceStub }, @@ -138,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(); + schemas: [NO_ERRORS_SCHEMA], + }) + .overrideComponent(MembersListComponent, { + remove: { + imports: [PaginationComponent, ContextHelpDirective], + }, + }) + .compileComponents(); })); beforeEach(() => { @@ -160,13 +199,37 @@ describe('MembersListComponent', () => { expect(comp).toBeDefined(); })); - it('should show list of eperson members of current active group', () => { - const epersonIdsFound = fixture.debugElement.queryAll(By.css('#ePeopleMembersOfGroup tr td:first-child')); - expect(epersonIdsFound.length).toEqual(1); - epersonMembers.map((eperson: EPerson) => { - expect(epersonIdsFound.find((foundEl) => { - return (foundEl.nativeElement.textContent.trim() === eperson.uuid); - })).toBeTruthy(); + describe('current members list', () => { + it('should show list of eperson members of current active group', () => { + const epersonIdsFound = fixture.debugElement.queryAll(By.css('#ePeopleMembersOfGroup tr td:first-child')); + expect(epersonIdsFound.length).toEqual(1); + epersonMembers.map((eperson: EPerson) => { + expect(epersonIdsFound.find((foundEl) => { + return (foundEl.nativeElement.textContent.trim() === eperson.uuid); + })).toBeTruthy(); + }); + }); + + it('should show a delete button next to each member', () => { + const epersonsFound = fixture.debugElement.queryAll(By.css('#ePeopleMembersOfGroup tbody tr')); + epersonsFound.map((foundEPersonRowElement: DebugElement) => { + const addButton: DebugElement = foundEPersonRowElement.query(By.css('td:last-child .fa-plus')); + const deleteButton: DebugElement = foundEPersonRowElement.query(By.css('td:last-child .fa-trash-alt')); + expect(addButton).toBeNull(); + expect(deleteButton).not.toBeNull(); + }); + }); + + 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('should trigger the search to add the user back to the search table', () => { + expect(component.search).toHaveBeenCalled(); + }); }); }); @@ -174,76 +237,40 @@ describe('MembersListComponent', () => { describe('when searching without query', () => { let epersonsFound: DebugElement[]; beforeEach(fakeAsync(() => { - spyOn(component, 'isMemberOfGroup').and.callFake((ePerson: EPerson) => { - return observableOf(activeGroup.epersons.includes(ePerson)); - }); component.search({ scope: 'metadata', query: '' }); tick(); fixture.detectChanges(); epersonsFound = fixture.debugElement.queryAll(By.css('#epersonsSearch tbody tr')); - // Stop using the fake spy function (because otherwise the clicking on the buttons will not change anything - // because they don't change the value of activeGroup.epersons) - jasmine.getEnv().allowRespy(true); - spyOn(component, 'isMemberOfGroup').and.callThrough(); })); - it('should display all epersons', () => { - expect(epersonsFound.length).toEqual(2); + it('should display only non-members of the group', () => { + const epersonIdsFound = fixture.debugElement.queryAll(By.css('#epersonsSearch tbody tr td:first-child')); + expect(epersonIdsFound.length).toEqual(1); + epersonNonMembers.map((eperson: EPerson) => { + expect(epersonIdsFound.find((foundEl) => { + return (foundEl.nativeElement.textContent.trim() === eperson.uuid); + })).toBeTruthy(); + }); }); - describe('if eperson is already a eperson', () => { - it('should have delete button, else it should have add button', () => { - const memberIds: string[] = activeGroup.epersons.map((ePerson: EPerson) => ePerson.id); - epersonsFound.map((foundEPersonRowElement: DebugElement) => { - const epersonId: DebugElement = foundEPersonRowElement.query(By.css('td:first-child')); - const addButton: DebugElement = foundEPersonRowElement.query(By.css('td:last-child .fa-plus')); - const deleteButton: DebugElement = foundEPersonRowElement.query(By.css('td:last-child .fa-trash-alt')); - if (memberIds.includes(epersonId.nativeElement.textContent)) { - expect(addButton).toBeNull(); - expect(deleteButton).not.toBeNull(); - } else { - expect(deleteButton).toBeNull(); - expect(addButton).not.toBeNull(); - } - }); + it('should display an add button next to non-members, not a delete button', () => { + epersonsFound.map((foundEPersonRowElement: DebugElement) => { + const addButton: DebugElement = foundEPersonRowElement.query(By.css('td:last-child .fa-plus')); + const deleteButton: DebugElement = foundEPersonRowElement.query(By.css('td:last-child .fa-trash-alt')); + expect(addButton).not.toBeNull(); + expect(deleteButton).toBeNull(); }); }); describe('if first add button is pressed', () => { - beforeEach(fakeAsync(() => { + beforeEach(() => { + spyOn(component, 'search').and.callThrough(); const addButton: DebugElement = fixture.debugElement.query(By.css('#epersonsSearch tbody .fa-plus')); addButton.nativeElement.click(); - tick(); fixture.detectChanges(); - })); - it('then all the ePersons are member of the active group', () => { - epersonsFound = fixture.debugElement.queryAll(By.css('#epersonsSearch tbody tr')); - expect(epersonsFound.length).toEqual(2); - epersonsFound.map((foundEPersonRowElement: DebugElement) => { - const addButton: DebugElement = foundEPersonRowElement.query(By.css('td:last-child .fa-plus')); - const deleteButton: DebugElement = foundEPersonRowElement.query(By.css('td:last-child .fa-trash-alt')); - expect(addButton).toBeNull(); - expect(deleteButton).not.toBeNull(); - }); }); - }); - - describe('if first delete button is pressed', () => { - beforeEach(fakeAsync(() => { - const deleteButton: DebugElement = fixture.debugElement.query(By.css('#epersonsSearch tbody .fa-trash-alt')); - deleteButton.nativeElement.click(); - tick(); - fixture.detectChanges(); - })); - it('then no ePerson is member of the active group', () => { - epersonsFound = fixture.debugElement.queryAll(By.css('#epersonsSearch tbody tr')); - expect(epersonsFound.length).toEqual(2); - epersonsFound.map((foundEPersonRowElement: DebugElement) => { - const addButton: DebugElement = foundEPersonRowElement.query(By.css('td:last-child .fa-plus')); - const deleteButton: DebugElement = foundEPersonRowElement.query(By.css('td:last-child .fa-trash-alt')); - expect(deleteButton).toBeNull(); - expect(addButton).not.toBeNull(); - }); + 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 b3e686c0123..9b123ae4470 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,41 +1,74 @@ -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 { - Observable, - of as observableOf, - Subscription, + AsyncPipe, + NgClass, + NgForOf, + NgIf, +} from '@angular/common'; +import { + Component, + Input, + OnDestroy, + OnInit, +} from '@angular/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, mergeMap, switchMap, take } from 'rxjs/operators'; -import { buildPaginatedList, PaginatedList } from '../../../../core/data/paginated-list.model'; +import { + defaultIfEmpty, + map, + switchMap, + take, +} from 'rxjs/operators'; + +import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service'; +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 { - getFirstSucceededRemoteData, - getFirstCompletedRemoteData, getAllCompletedRemoteData, - getRemoteDataPayload + 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 { EpersonDtoModel } from '../../../../core/eperson/models/eperson-dto.model'; -import { PaginationService } from '../../../../core/pagination/pagination.service'; -import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service'; +import { getEPersonEditRoute } from '../../../access-control-routing-paths'; + +// todo: optimize imports /** * Keys to keep track of specific subscriptions */ enum SubKey { ActiveGroup, - MembersDTO, - SearchResultsDTO, + Members, + SearchResults, } /** @@ -69,7 +102,19 @@ export interface EPersonListActionConfig { @Component({ selector: 'ds-members-list', - templateUrl: './members-list.component.html' + 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 @@ -89,18 +134,18 @@ export class MembersListComponent implements OnInit, OnDestroy { remove: { css: 'btn-outline-danger', disabled: false, - icon: 'fas fa-trash-alt fa-fw' + icon: 'fas fa-trash-alt fa-fw', }, }; /** * EPeople being displayed in search result, initially all members, after search result of search */ - ePeopleSearchDtos: BehaviorSubject> = new BehaviorSubject>(undefined); + ePeopleSearch: BehaviorSubject> = new BehaviorSubject>(undefined); /** * List of EPeople members of currently active group being edited */ - ePeopleMembersOfGroupDtos: BehaviorSubject> = new BehaviorSubject>(undefined); + ePeopleMembersOfGroup: BehaviorSubject> = new BehaviorSubject(undefined); /** * Pagination config used to display the list of EPeople that are result of EPeople search @@ -108,7 +153,7 @@ export class MembersListComponent implements OnInit, OnDestroy { configSearch: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), { id: 'sml', pageSize: 5, - currentPage: 1 + currentPage: 1, }); /** * Pagination config used to display the list of EPerson Membes of active group being edited @@ -116,7 +161,7 @@ export class MembersListComponent implements OnInit, OnDestroy { config: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), { id: 'ml', pageSize: 5, - currentPage: 1 + currentPage: 1, }); /** @@ -129,7 +174,6 @@ export class MembersListComponent implements OnInit, OnDestroy { // Current search in edit group - epeople search form currentSearchQuery: string; - currentSearchScope: string; // Whether or not user has done a EPeople search yet searchDone: boolean; @@ -137,6 +181,8 @@ export class MembersListComponent implements OnInit, OnDestroy { // current active group being edited groupBeingEdited: Group; + readonly getEPersonEditRoute = getEPersonEditRoute; + constructor( protected groupDataService: GroupDataService, public ePersonDataService: EPersonDataService, @@ -148,18 +194,17 @@ export class MembersListComponent implements OnInit, OnDestroy { public dsoNameService: DSONameService, ) { this.currentSearchQuery = ''; - this.currentSearchScope = 'metadata'; } ngOnInit(): void { this.searchForm = this.formBuilder.group(({ - scope: 'metadata', query: '', })); this.subs.set(SubKey.ActiveGroup, this.groupDataService.getActiveGroup().subscribe((activeGroup: Group) => { if (activeGroup != null) { this.groupBeingEdited = activeGroup; this.retrieveMembers(this.config.currentPage); + this.search({ query: '' }); } })); } @@ -171,14 +216,14 @@ export class MembersListComponent implements OnInit, OnDestroy { * @private */ retrieveMembers(page: number): void { - this.unsubFrom(SubKey.MembersDTO); - this.subs.set(SubKey.MembersDTO, + this.unsubFrom(SubKey.Members); + this.subs.set(SubKey.Members, this.paginationService.getCurrentPagination(this.config.id, this.config).pipe( switchMap((currentPagination) => { return this.ePersonDataService.findListByHref(this.groupBeingEdited._links.epersons.href, { - currentPage: currentPagination.currentPage, - elementsPerPage: currentPagination.pageSize - } + currentPage: currentPagination.currentPage, + elementsPerPage: currentPagination.pageSize, + }, ); }), getAllCompletedRemoteData(), @@ -195,7 +240,7 @@ export class MembersListComponent implements OnInit, OnDestroy { this.isMemberOfGroup(member), (isMember: ObservedValueOf>) => { const epersonDtoModel: EpersonDtoModel = new EpersonDtoModel(); epersonDtoModel.eperson = member; - epersonDtoModel.memberOfGroup = isMember; + epersonDtoModel.ableToDelete = isMember; return epersonDtoModel; }); return dto$; @@ -203,33 +248,21 @@ export class MembersListComponent implements OnInit, OnDestroy { return dtos$.pipe(defaultIfEmpty([]), map((dtos: EpersonDtoModel[]) => { return buildPaginatedList(epersonListRD.payload.pageInfo, dtos); })); - })) - .subscribe((paginatedListOfDTOs: PaginatedList) => { - this.ePeopleMembersOfGroupDtos.next(paginatedListOfDTOs); - })); + }), + ).subscribe((paginatedListOfDTOs: PaginatedList) => { + this.ePeopleMembersOfGroup.next(paginatedListOfDTOs); + }), + ); } /** - * Whether the given ePerson is a member of the group currently being edited + * 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 this.groupDataService.getActiveGroup().pipe(take(1), - mergeMap((group: Group) => { - if (group != null) { - return this.ePersonDataService.findListByHref(group._links.epersons.href, { - currentPage: 1, - elementsPerPage: 9999 - }) - .pipe( - getFirstSucceededRemoteData(), - getRemoteDataPayload(), - map((listEPeopleInGroup: PaginatedList) => listEPeopleInGroup.page.filter((ePersonInList: EPerson) => ePersonInList.id === possibleMember.id)), - map((epeople: EPerson[]) => epeople.length > 0)); - } else { - return observableOf(false); - } - })); + return observableOf(true); } /** @@ -248,14 +281,18 @@ export class MembersListComponent implements OnInit, OnDestroy { /** * Deletes a given EPerson from the members list of the group currently being edited - * @param ePerson EPerson we want to delete as member from group that is currently being edited + * @param eperson EPerson we want to delete as member from group that is currently being edited */ - deleteMemberFromGroup(ePerson: EpersonDtoModel) { - ePerson.memberOfGroup = false; + deleteMemberFromGroup(eperson: EPerson) { this.groupDataService.getActiveGroup().pipe(take(1)).subscribe((activeGroup: Group) => { if (activeGroup != null) { - const response = this.groupDataService.deleteMemberFromGroup(activeGroup, ePerson.eperson); - this.showNotifications('deleteMember', response, this.dsoNameService.getName(ePerson.eperson), activeGroup); + const response = this.groupDataService.deleteMemberFromGroup(activeGroup, eperson); + this.showNotifications('deleteMember', response, this.dsoNameService.getName(eperson), activeGroup); + // Reload search results (if there is an active query). + // This will potentially add this deleted subgroup into the list of search results. + if (this.currentSearchQuery != null) { + this.search({ query: this.currentSearchQuery }); + } } else { this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure.noActiveGroup')); } @@ -264,14 +301,18 @@ export class MembersListComponent implements OnInit, OnDestroy { /** * Adds a given EPerson to the members list of the group currently being edited - * @param ePerson EPerson we want to add as member to group that is currently being edited + * @param eperson EPerson we want to add as member to group that is currently being edited */ - addMemberToGroup(ePerson: EpersonDtoModel) { - ePerson.memberOfGroup = true; + addMemberToGroup(eperson: EPerson) { this.groupDataService.getActiveGroup().pipe(take(1)).subscribe((activeGroup: Group) => { if (activeGroup != null) { - const response = this.groupDataService.addMemberToGroup(activeGroup, ePerson.eperson); - this.showNotifications('addMember', response, this.dsoNameService.getName(ePerson.eperson), activeGroup); + const response = this.groupDataService.addMemberToGroup(activeGroup, eperson); + this.showNotifications('addMember', response, this.dsoNameService.getName(eperson), activeGroup); + // Reload search results (if there is an active query). + // This will potentially add this deleted subgroup into the list of search results. + if (this.currentSearchQuery != null) { + this.search({ query: this.currentSearchQuery }); + } } else { this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure.noActiveGroup')); } @@ -279,37 +320,25 @@ export class MembersListComponent implements OnInit, OnDestroy { } /** - * Search in the EPeople by name, email or metadata - * @param data Contains scope and query param + * Search all EPeople who are NOT a member of the current group by name, email or metadata + * @param data Contains query param */ search(data: any) { - this.unsubFrom(SubKey.SearchResultsDTO); - this.subs.set(SubKey.SearchResultsDTO, + this.unsubFrom(SubKey.SearchResults); + this.subs.set(SubKey.SearchResults, this.paginationService.getCurrentPagination(this.configSearch.id, this.configSearch).pipe( switchMap((paginationOptions) => { - const query: string = data.query; - const scope: string = data.scope; if (query != null && this.currentSearchQuery !== query && this.groupBeingEdited) { - this.router.navigate([], { - queryParamsHandling: 'merge' - }); this.currentSearchQuery = query; this.paginationService.resetPage(this.configSearch.id); } - if (scope != null && this.currentSearchScope !== scope && this.groupBeingEdited) { - this.router.navigate([], { - queryParamsHandling: 'merge' - }); - this.currentSearchScope = scope; - this.paginationService.resetPage(this.configSearch.id); - } this.searchDone = true; - return this.ePersonDataService.searchByScope(this.currentSearchScope, this.currentSearchQuery, { + return this.ePersonDataService.searchNonMembers(this.currentSearchQuery, this.groupBeingEdited.id, { currentPage: paginationOptions.currentPage, - elementsPerPage: paginationOptions.pageSize - }); + elementsPerPage: paginationOptions.pageSize, + }, false, true); }), getAllCompletedRemoteData(), map((rd: RemoteData) => { @@ -319,23 +348,9 @@ export class MembersListComponent implements OnInit, OnDestroy { return rd; } }), - 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.memberOfGroup = isMember; - return epersonDtoModel; - }); - return dto$; - })]); - return dtos$.pipe(defaultIfEmpty([]), map((dtos: EpersonDtoModel[]) => { - return buildPaginatedList(epersonListRD.payload.pageInfo, dtos); - })); - })) - .subscribe((paginatedListOfDTOs: PaginatedList) => { - this.ePeopleSearchDtos.next(paginatedListOfDTOs); + getRemoteDataPayload()) + .subscribe((paginatedListOfEPersons: PaginatedList) => { + this.ePeopleSearch.next(paginatedListOfEPersons); })); } 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 d009f0283eb..66404bde0d1 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 @@ -1,6 +1,54 @@

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

+

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

+ + + +
+ + + + + + + + + + + + + + + + + +
{{messagePrefix + '.table.id' | translate}}{{messagePrefix + '.table.name' | translate}}{{messagePrefix + '.table.collectionOrCommunity' | translate}}{{messagePrefix + '.table.edit' | translate}}
{{group.id}} + + {{ dsoNameService.getName(group) }} + + {{ dsoNameService.getName((group.object | async)?.payload)}} +
+ +
+
+
+
+ + +
{{ dsoNameService.getName((group.object | async)?.payload) }}
- - - {{ messagePrefix + '.table.edit.currentGroup' | translate }} - -
- -

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

- - - -
- - - - - - - - - - - - - - - - - -
{{messagePrefix + '.table.id' | translate}}{{messagePrefix + '.table.name' | translate}}{{messagePrefix + '.table.collectionOrCommunity' | translate}}{{messagePrefix + '.table.edit' | translate}}
{{group.id}} - - {{ dsoNameService.getName(group) }} - - {{ dsoNameService.getName((group.object | async)?.payload)}} -
- -
-
-
-
- - - 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 ac5750dcaca..5b39102ca8a 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 @@ -1,36 +1,69 @@ import { CommonModule } from '@angular/common'; -import { NO_ERRORS_SCHEMA, DebugElement } from '@angular/core'; -import { ComponentFixture, fakeAsync, flush, inject, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { FormsModule, ReactiveFormsModule } from '@angular/forms'; -import { BrowserModule, By } from '@angular/platform-browser'; -import { Router } from '@angular/router'; +import { + DebugElement, + NO_ERRORS_SCHEMA, +} from '@angular/core'; +import { + ComponentFixture, + fakeAsync, + flush, + inject, + TestBed, + waitForAsync, +} from '@angular/core/testing'; +import { + FormsModule, + ReactiveFormsModule, +} from '@angular/forms'; +import { + BrowserModule, + By, +} from '@angular/platform-browser'; +import { + ActivatedRoute, + Router, +} from '@angular/router'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; -import { TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core'; -import { Observable, of as observableOf, BehaviorSubject } from 'rxjs'; +import { + TranslateLoader, + TranslateModule, + TranslateService, +} from '@ngx-translate/core'; +import { + Observable, + of as observableOf, +} from 'rxjs'; +import { EPersonMock2 } from 'src/app/shared/testing/eperson.mock'; + +import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service'; import { RestResponse } from '../../../../core/cache/response.models'; -import { buildPaginatedList, PaginatedList } from '../../../../core/data/paginated-list.model'; +import { + buildPaginatedList, + PaginatedList, +} from '../../../../core/data/paginated-list.model'; import { RemoteData } from '../../../../core/data/remote-data'; 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 { NotificationsService } from '../../../../shared/notifications/notifications.service'; -import { GroupMock, GroupMock2 } from '../../../../shared/testing/group-mock'; -import { SubgroupsListComponent } from './subgroups-list.component'; -import { - createSuccessfulRemoteDataObject$, - createSuccessfulRemoteDataObject -} from '../../../../shared/remote-data.utils'; -import { RouterMock } from '../../../../shared/mocks/router.mock'; +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 { TranslateLoaderMock } from '../../../../shared/testing/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 { + GroupMock, + GroupMock2, +} from '../../../../shared/testing/group-mock'; import { NotificationsServiceStub } from '../../../../shared/testing/notifications-service.stub'; -import { map } from 'rxjs/operators'; -import { PaginationService } from '../../../../core/pagination/pagination.service'; import { PaginationServiceStub } from '../../../../shared/testing/pagination-service.stub'; -import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service'; -import { DSONameServiceMock } from '../../../../shared/mocks/dso-name.service.mock'; +import { TranslateLoaderMock } from '../../../../shared/testing/translate-loader.mock'; +import { SubgroupsListComponent } from './subgroups-list.component'; describe('SubgroupsListComponent', () => { let component: SubgroupsListComponent; @@ -39,44 +72,72 @@ describe('SubgroupsListComponent', () => { let builderService: FormBuilderService; let ePersonDataServiceStub: any; let groupsDataServiceStub: any; - let activeGroup; + let activeGroup: Group; let subgroups: Group[]; - let allGroups: Group[]; + let groupNonMembers: Group[]; let routerStub; let paginationService; + // Define a new mock activegroup for all tests below + let mockActiveGroup: Group = Object.assign(new Group(), { + handle: null, + subgroups: [GroupMock2], + epersons: [EPersonMock2], + selfRegistered: false, + permanent: false, + _links: { + self: { + href: 'https://rest.api/server/api/eperson/groups/activegroupid', + }, + subgroups: { href: 'https://rest.api/server/api/eperson/groups/activegroupid/subgroups' }, + object: { href: 'https://rest.api/server/api/eperson/groups/activegroupid/object' }, + epersons: { href: 'https://rest.api/server/api/eperson/groups/activegroupid/epersons' }, + }, + _name: 'activegroupname', + id: 'activegroupid', + uuid: 'activegroupid', + type: 'group', + }); beforeEach(waitForAsync(() => { - activeGroup = GroupMock; + activeGroup = mockActiveGroup; subgroups = [GroupMock2]; - allGroups = [GroupMock, GroupMock2]; + groupNonMembers = [GroupMock]; ePersonDataServiceStub = {}; groupsDataServiceStub = { activeGroup: activeGroup, - subgroups$: new BehaviorSubject(subgroups), + subgroups: subgroups, + groupNonMembers: groupNonMembers, getActiveGroup(): Observable { return observableOf(this.activeGroup); }, getSubgroups(): Group { - return this.activeGroup; + return this.subgroups; }, + // This method is used to get all the current subgroups findListByHref(_href: string): Observable>> { - return this.subgroups$.pipe( - map((currentGroups: Group[]) => { - return createSuccessfulRemoteDataObject(buildPaginatedList(new PageInfo(), currentGroups)); - }) - ); + return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), groupsDataServiceStub.getSubgroups())); }, getGroupEditPageRouterLink(group: Group): string { return '/access-control/groups/' + group.id; }, - searchGroups(query: string): Observable>> { + // This method is used to get all groups which are NOT currently a subgroup member + searchNonMemberGroups(query: string, group: string): Observable>> { if (query === '') { - return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), allGroups)); + return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), groupNonMembers)); } - return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), [])); + return createSuccessfulRemoteDataObject$( + buildPaginatedList(new PageInfo(), []), + ); }, - addSubGroupToGroup(parentGroup, subgroup: Group): Observable { - this.subgroups$.next([...this.subgroups$.getValue(), subgroup]); + addSubGroupToGroup(parentGroup, subgroupToAdd: Group): Observable { + // Add group to list of subgroups + this.subgroups = [...this.subgroups, subgroupToAdd]; + // Remove group from list of non-members + this.groupNonMembers.forEach( (group: Group, index: number) => { + if (group.id === subgroupToAdd.id) { + this.groupNonMembers.splice(index, 1); + } + }); return observableOf(new RestResponse(true, 200, 'Success')); }, clearGroupsRequests() { @@ -85,40 +146,59 @@ describe('SubgroupsListComponent', () => { clearGroupLinkRequests() { // empty }, - deleteSubGroupFromGroup(parentGroup, subgroup: Group): Observable { - this.subgroups$.next(this.subgroups$.getValue().filter((group: Group) => { - if (group.id !== subgroup.id) { - return group; + deleteSubGroupFromGroup(parentGroup, subgroupToDelete: Group): Observable { + // Remove group from list of subgroups + this.subgroups.forEach( (group: Group, index: number) => { + if (group.id === subgroupToDelete.id) { + this.subgroups.splice(index, 1); } - })); + }); + // Add group to list of non-members + this.groupNonMembers = [...this.groupNonMembers, subgroupToDelete]; return observableOf(new RestResponse(true, 200, 'Success')); - } + }, }; routerStub = new RouterMock(); builderService = getMockFormBuilderService(); translateService = getMockTranslateService(); - paginationService = new PaginationServiceStub(); - TestBed.configureTestingModule({ - imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule, + return TestBed.configureTestingModule({ + imports: [ + CommonModule, + NgbModule, + FormsModule, + ReactiveFormsModule, + BrowserModule, + // ContextHelpDirective, TranslateModule.forRoot({ loader: { provide: TranslateLoader, - useClass: TranslateLoaderMock - } + 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(); + schemas: [NO_ERRORS_SCHEMA], + }) + .overrideComponent(SubgroupsListComponent, { + remove: { + imports: [ContextHelpDirective, PaginationComponent], + }, + }) + .compileComponents(); })); beforeEach(() => { @@ -137,30 +217,38 @@ describe('SubgroupsListComponent', () => { expect(comp).toBeDefined(); })); - it('should show list of subgroups of current active group', () => { - const groupIdsFound = fixture.debugElement.queryAll(By.css('#subgroupsOfGroup tr td:first-child')); - expect(groupIdsFound.length).toEqual(1); - activeGroup.subgroups.map((group: Group) => { - expect(groupIdsFound.find((foundEl) => { - return (foundEl.nativeElement.textContent.trim() === group.uuid); - })).toBeTruthy(); + describe('current subgroup list', () => { + it('should show list of subgroups of current active group', () => { + const groupIdsFound = fixture.debugElement.queryAll(By.css('#subgroupsOfGroup tr td:first-child')); + expect(groupIdsFound.length).toEqual(1); + subgroups.map((group: Group) => { + expect(groupIdsFound.find((foundEl) => { + return (foundEl.nativeElement.textContent.trim() === group.uuid); + })).toBeTruthy(); + }); }); - }); - describe('if first group delete button is pressed', () => { - let groupsFound: DebugElement[]; - beforeEach(fakeAsync(() => { - const addButton = fixture.debugElement.query(By.css('#subgroupsOfGroup tbody .deleteButton')); - addButton.triggerEventHandler('click', { - preventDefault: () => {/**/ - } + it('should show a delete button next to each subgroup', () => { + const subgroupsFound = fixture.debugElement.queryAll(By.css('#subgroupsOfGroup tbody tr')); + subgroupsFound.map((foundGroupRowElement: DebugElement) => { + const addButton: DebugElement = foundGroupRowElement.query(By.css('td:last-child .fa-plus')); + const deleteButton: DebugElement = foundGroupRowElement.query(By.css('td:last-child .fa-trash-alt')); + expect(addButton).toBeNull(); + expect(deleteButton).not.toBeNull(); + }); + }); + + describe('if first group delete button is pressed', () => { + let groupsFound: DebugElement[]; + beforeEach(() => { + const deleteButton = fixture.debugElement.query(By.css('#subgroupsOfGroup tbody .deleteButton')); + deleteButton.nativeElement.click(); + fixture.detectChanges(); + }); + it('then no subgroup remains as a member of the active group', () => { + groupsFound = fixture.debugElement.queryAll(By.css('#subgroupsOfGroup tbody tr')); + expect(groupsFound.length).toEqual(0); }); - tick(); - fixture.detectChanges(); - })); - it('one less subgroup in list from 1 to 0 (of 2 total groups)', () => { - groupsFound = fixture.debugElement.queryAll(By.css('#subgroupsOfGroup tbody tr')); - expect(groupsFound.length).toEqual(0); }); }); @@ -169,54 +257,38 @@ describe('SubgroupsListComponent', () => { let groupsFound: DebugElement[]; beforeEach(fakeAsync(() => { component.search({ query: '' }); + fixture.detectChanges(); groupsFound = fixture.debugElement.queryAll(By.css('#groupsSearch tbody tr')); })); - it('should display all groups', () => { - fixture.detectChanges(); - groupsFound = fixture.debugElement.queryAll(By.css('#groupsSearch tbody tr')); - expect(groupsFound.length).toEqual(2); - groupsFound = fixture.debugElement.queryAll(By.css('#groupsSearch tbody tr')); + it('should display only non-member groups (i.e. groups that are not a subgroup)', () => { const groupIdsFound: DebugElement[] = fixture.debugElement.queryAll(By.css('#groupsSearch tbody tr td:first-child')); - allGroups.map((group: Group) => { + expect(groupIdsFound.length).toEqual(1); + groupNonMembers.map((group: Group) => { expect(groupIdsFound.find((foundEl: DebugElement) => { return (foundEl.nativeElement.textContent.trim() === group.uuid); })).toBeTruthy(); }); }); - describe('if group is already a subgroup', () => { - it('should have delete button, else it should have add button', () => { + it('should display an add button next to non-member groups, not a delete button', () => { + groupsFound.map((foundGroupRowElement: DebugElement) => { + const addButton: DebugElement = foundGroupRowElement.query(By.css('td:last-child .fa-plus')); + const deleteButton: DebugElement = foundGroupRowElement.query(By.css('td:last-child .fa-trash-alt')); + expect(addButton).not.toBeNull(); + expect(deleteButton).toBeNull(); + }); + }); + + describe('if first add button is pressed', () => { + beforeEach(() => { + const addButton: DebugElement = fixture.debugElement.query(By.css('#groupsSearch tbody .fa-plus')); + addButton.nativeElement.click(); fixture.detectChanges(); + }); + it('then all (two) Groups are subgroups of the active group. No non-members left', () => { groupsFound = fixture.debugElement.queryAll(By.css('#groupsSearch tbody tr')); - const getSubgroups = groupsDataServiceStub.getSubgroups().subgroups; - if (getSubgroups !== undefined && getSubgroups.length > 0) { - groupsFound.map((foundGroupRowElement: DebugElement) => { - const groupId: DebugElement = foundGroupRowElement.query(By.css('td:first-child')); - const addButton: DebugElement = foundGroupRowElement.query(By.css('td:last-child .fa-plus')); - const deleteButton: DebugElement = foundGroupRowElement.query(By.css('td:last-child .fa-trash-alt')); - expect(addButton).toBeNull(); - if (activeGroup.id === groupId.nativeElement.textContent) { - expect(deleteButton).toBeNull(); - } else { - expect(deleteButton).not.toBeNull(); - } - }); - } else { - const subgroupIds: string[] = activeGroup.subgroups.map((group: Group) => group.id); - groupsFound.map((foundGroupRowElement: DebugElement) => { - const groupId: DebugElement = foundGroupRowElement.query(By.css('td:first-child')); - const addButton: DebugElement = foundGroupRowElement.query(By.css('td:last-child .fa-plus')); - const deleteButton: DebugElement = foundGroupRowElement.query(By.css('td:last-child .fa-trash-alt')); - if (subgroupIds.includes(groupId.nativeElement.textContent)) { - expect(addButton).toBeNull(); - expect(deleteButton).not.toBeNull(); - } else { - expect(deleteButton).toBeNull(); - expect(addButton).not.toBeNull(); - } - }); - } + expect(groupsFound.length).toEqual(0); }); }); }); 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 0cff730c628..95c1b7f249b 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,24 +1,54 @@ -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 { BehaviorSubject, Observable, of as observableOf, Subscription } from 'rxjs'; -import { map, mergeMap, switchMap, take } from 'rxjs/operators'; +import { + AsyncPipe, + NgForOf, + NgIf, +} from '@angular/common'; +import { + Component, + Input, + OnDestroy, + OnInit, +} from '@angular/core'; +import { + ReactiveFormsModule, + UntypedFormBuilder, +} from '@angular/forms'; +import { + Router, + RouterLink, +} from '@angular/router'; +import { + TranslateModule, + TranslateService, +} from '@ngx-translate/core'; +import { + BehaviorSubject, + Observable, + Subscription, +} from 'rxjs'; +import { + map, + switchMap, + take, +} from 'rxjs/operators'; + +import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service'; import { PaginatedList } from '../../../../core/data/paginated-list.model'; import { RemoteData } from '../../../../core/data/remote-data'; import { GroupDataService } from '../../../../core/eperson/group-data.service'; import { Group } from '../../../../core/eperson/models/group.model'; +import { PaginationService } from '../../../../core/pagination/pagination.service'; +import { NoContent } from '../../../../core/shared/NoContent.model'; import { + getAllCompletedRemoteData, getFirstCompletedRemoteData, - getFirstSucceededRemoteData, - getRemoteDataPayload } 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 { NoContent } from '../../../../core/shared/NoContent.model'; -import { PaginationService } from '../../../../core/pagination/pagination.service'; import { followLink } from '../../../../shared/utils/follow-link-config.model'; -import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service'; /** * Keys to keep track of specific subscriptions @@ -31,7 +61,18 @@ enum SubKey { @Component({ selector: 'ds-subgroups-list', - templateUrl: './subgroups-list.component.html' + 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 @@ -50,6 +91,8 @@ export class SubgroupsListComponent implements OnInit, OnDestroy { */ subGroups$: BehaviorSubject>> = new BehaviorSubject(undefined); + subGroupsPageInfoState$: Observable; + /** * Map of active subscriptions */ @@ -61,7 +104,7 @@ export class SubgroupsListComponent implements OnInit, OnDestroy { configSearch: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), { id: 'ssgl', pageSize: 5, - currentPage: 1 + currentPage: 1, }); /** * Pagination config used to display the list of subgroups of currently active group being edited @@ -69,7 +112,7 @@ export class SubgroupsListComponent implements OnInit, OnDestroy { config: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), { id: 'sgl', pageSize: 5, - currentPage: 1 + currentPage: 1, }); // The search form @@ -103,8 +146,12 @@ export class SubgroupsListComponent implements OnInit, OnDestroy { if (activeGroup != null) { this.groupBeingEdited = activeGroup; this.retrieveSubGroups(); + this.search({ query: '' }); } })); + this.subGroupsPageInfoState$ = this.subGroups$.pipe( + map(subGroupsRD => subGroupsRD?.payload?.pageInfo), + ); } /** @@ -119,59 +166,18 @@ export class SubgroupsListComponent implements OnInit, OnDestroy { SubKey.Members, this.paginationService.getCurrentPagination(this.config.id, this.config).pipe( switchMap((config) => this.groupDataService.findListByHref(this.groupBeingEdited._links.subgroups.href, { - currentPage: config.currentPage, - elementsPerPage: config.pageSize - }, - true, - true, - followLink('object') - )) + currentPage: config.currentPage, + elementsPerPage: config.pageSize, + }, + true, + true, + followLink('object'), + )), ).subscribe((rd: RemoteData>) => { this.subGroups$.next(rd); })); } - /** - * Whether or not the given group is a subgroup of the group currently being edited - * @param possibleSubgroup Group that is a possible subgroup (being tested) of the group currently being edited - */ - isSubgroupOfGroup(possibleSubgroup: Group): Observable { - return this.groupDataService.getActiveGroup().pipe(take(1), - mergeMap((activeGroup: Group) => { - if (activeGroup != null) { - if (activeGroup.uuid === possibleSubgroup.uuid) { - return observableOf(false); - } else { - return this.groupDataService.findListByHref(activeGroup._links.subgroups.href, { - currentPage: 1, - elementsPerPage: 9999 - }) - .pipe( - getFirstSucceededRemoteData(), - getRemoteDataPayload(), - map((listTotalGroups: PaginatedList) => listTotalGroups.page.filter((groupInList: Group) => groupInList.id === possibleSubgroup.id)), - map((groups: Group[]) => groups.length > 0)); - } - } else { - return observableOf(false); - } - })); - } - - /** - * Whether or not the given group is the current group being edited - * @param group Group that is possibly the current group being edited - */ - isActiveGroup(group: Group): Observable { - return this.groupDataService.getActiveGroup().pipe(take(1), - mergeMap((activeGroup: Group) => { - if (activeGroup != null && activeGroup.uuid === group.uuid) { - return observableOf(true); - } - return observableOf(false); - })); - } - /** * Deletes given subgroup from the group currently being edited * @param subgroup Group we want to delete from the subgroups of the group currently being edited @@ -181,6 +187,11 @@ export class SubgroupsListComponent implements OnInit, OnDestroy { if (activeGroup != null) { const response = this.groupDataService.deleteSubGroupFromGroup(activeGroup, subgroup); this.showNotifications('deleteSubgroup', response, this.dsoNameService.getName(subgroup), activeGroup); + // Reload search results (if there is an active query). + // This will potentially add this deleted subgroup into the list of search results. + if (this.currentSearchQuery != null) { + this.search({ query: this.currentSearchQuery }); + } } else { this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure.noActiveGroup')); } @@ -197,6 +208,11 @@ export class SubgroupsListComponent implements OnInit, OnDestroy { if (activeGroup.uuid !== subgroup.uuid) { const response = this.groupDataService.addSubGroupToGroup(activeGroup, subgroup); this.showNotifications('addSubgroup', response, this.dsoNameService.getName(subgroup), activeGroup); + // Reload search results (if there is an active query). + // This will potentially remove this added subgroup from search results. + if (this.currentSearchQuery != null) { + this.search({ query: this.currentSearchQuery }); + } } else { this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure.subgroupToAddIsActiveGroup')); } @@ -207,28 +223,38 @@ export class SubgroupsListComponent implements OnInit, OnDestroy { } /** - * Search in the groups (searches by group name and by uuid exact match) + * Search all non-member groups (searches by group name and by uuid exact match). Used to search for + * groups that could be added to current group as a subgroup. * @param data Contains query param */ search(data: any) { - const query: string = data.query; - if (query != null && this.currentSearchQuery !== query) { - this.router.navigateByUrl(this.groupDataService.getGroupEditPageRouterLink(this.groupBeingEdited)); - this.currentSearchQuery = query; - this.configSearch.currentPage = 1; - } - this.searchDone = true; - this.unsubFrom(SubKey.SearchResults); - this.subs.set(SubKey.SearchResults, this.paginationService.getCurrentPagination(this.configSearch.id, this.configSearch).pipe( - switchMap((config) => this.groupDataService.searchGroups(this.currentSearchQuery, { - currentPage: config.currentPage, - elementsPerPage: config.pageSize - }, true, true, followLink('object') - )) - ).subscribe((rd: RemoteData>) => { - this.searchResults$.next(rd); - })); + this.subs.set(SubKey.SearchResults, + this.paginationService.getCurrentPagination(this.configSearch.id, this.configSearch).pipe( + switchMap((paginationOptions) => { + const query: string = data.query; + if (query != null && this.currentSearchQuery !== query && this.groupBeingEdited) { + this.currentSearchQuery = query; + this.paginationService.resetPage(this.configSearch.id); + } + this.searchDone = true; + + return this.groupDataService.searchNonMemberGroups(this.currentSearchQuery, this.groupBeingEdited.id, { + currentPage: paginationOptions.currentPage, + elementsPerPage: paginationOptions.pageSize, + }, false, true, followLink('object')); + }), + getAllCompletedRemoteData(), + map((rd: RemoteData) => { + if (rd.hasFailed) { + this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure', { cause: rd.errorMessage })); + } else { + return rd; + } + })) + .subscribe((rd: RemoteData>) => { + this.searchResults$.next(rd); + })); } /** diff --git a/src/app/access-control/group-registry/group-form/validators/group-exists.validator.ts b/src/app/access-control/group-registry/group-form/validators/group-exists.validator.ts index 88f22413e93..056c54bf6a8 100644 --- a/src/app/access-control/group-registry/group-form/validators/group-exists.validator.ts +++ b/src/app/access-control/group-registry/group-form/validators/group-exists.validator.ts @@ -1,10 +1,13 @@ -import { AbstractControl, ValidationErrors } from '@angular/forms'; +import { + AbstractControl, + ValidationErrors, +} from '@angular/forms'; import { Observable } from 'rxjs'; -import { map} from 'rxjs/operators'; +import { map } from 'rxjs/operators'; import { GroupDataService } from '../../../../core/eperson/group-data.service'; -import { getFirstSucceededRemoteListPayload } from '../../../../core/shared/operators'; import { Group } from '../../../../core/eperson/models/group.model'; +import { getFirstSucceededRemoteListPayload } from '../../../../core/shared/operators'; export class ValidateGroupExists { @@ -16,9 +19,9 @@ export class ValidateGroupExists { static createValidator(groupDataService: GroupDataService) { return (control: AbstractControl): Promise | Observable => { return groupDataService.searchGroups(control.value, { - currentPage: 1, - elementsPerPage: 100 - }) + currentPage: 1, + elementsPerPage: 100, + }) .pipe( getFirstSucceededRemoteListPayload(), map( (groups: Group[]) => { 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 48fa124c077..3024e42d64a 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,10 +1,24 @@ -import { GroupPageGuard } from './group-page.guard'; -import { HALEndpointService } from '../../core/shared/hal-endpoint.service'; -import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; -import { ActivatedRouteSnapshot, Router } from '@angular/router'; -import { of as observableOf } from 'rxjs'; +import { + TestBed, + waitForAsync, +} from '@angular/core/testing'; +import { + ActivatedRouteSnapshot, + Router, + UrlTree, +} from '@angular/router'; +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'; + +jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000; // Increase timeout to 10 seconds describe('GroupPageGuard', () => { const groupsEndpointUrl = 'https://test.org/api/eperson/groups'; @@ -13,47 +27,59 @@ describe('GroupPageGuard', () => { const routeSnapshotWithGroupId = { params: { groupId: groupUuid, - } + }, } 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 + FeatureID.CanManageGroup, groupEndpointUrl, undefined, ); expect(result).toBeTrue(); done(); @@ -67,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 + 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 057f67ddeb9..c52bed9c48b 100644 --- a/src/app/access-control/group-registry/group-page.guard.ts +++ b/src/app/access-control/group-registry/group-page.guard.ts @@ -1,35 +1,38 @@ -import { Injectable } from '@angular/core'; -import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router'; -import { Observable, of as observableOf } from 'rxjs'; -import { FeatureID } from '../../core/data/feature-authorization/feature-id'; -import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; -import { AuthService } from '../../core/auth/auth.service'; -import { SomeFeatureAuthorizationGuard } from '../../core/data/feature-authorization/feature-authorization-guard/some-feature-authorization.guard'; -import { HALEndpointService } from '../../core/shared/hal-endpoint.service'; +import { inject } from '@angular/core'; +import { + ActivatedRouteSnapshot, + CanActivateFn, + RouterStateSnapshot, +} from '@angular/router'; +import { + Observable, + of as observableOf, +} from 'rxjs'; import { map } from 'rxjs/operators'; -@Injectable({ - providedIn: 'root' -}) -export class GroupPageGuard extends SomeFeatureAuthorizationGuard { - - protected groupsEndpoint = 'groups'; - - constructor(protected halEndpointService: HALEndpointService, - protected authorizationService: AuthorizationDataService, - protected router: Router, - protected authService: AuthService) { - super(authorizationService, router, authService); - } +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'; - getFeatureIDs(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { - return observableOf([FeatureID.CanManageGroup]); - } +const defaultGroupPageGetObjectUrl: StringGuardParamFn = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, +): Observable => { + const halEndpointService = inject(HALEndpointService); + const groupsEndpoint = 'groups'; - getObjectUrl(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { - return this.halEndpointService.getEndpoint(this.groupsEndpoint).pipe( - map(groupsUrl => `${groupsUrl}/${route?.params?.groupId}`) - ); - } + return halEndpointService.getEndpoint(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/group-registry.actions.ts b/src/app/access-control/group-registry/group-registry.actions.ts index 8144bd05995..d1bc62a95cc 100644 --- a/src/app/access-control/group-registry/group-registry.actions.ts +++ b/src/app/access-control/group-registry/group-registry.actions.ts @@ -1,5 +1,6 @@ /* eslint-disable max-classes-per-file */ import { Action } from '@ngrx/store'; + import { Group } from '../../core/eperson/models/group.model'; import { type } from '../../shared/ngrx/type'; diff --git a/src/app/access-control/group-registry/group-registry.reducers.spec.ts b/src/app/access-control/group-registry/group-registry.reducers.spec.ts index de5b65f5ba6..83a6df580d5 100644 --- a/src/app/access-control/group-registry/group-registry.reducers.spec.ts +++ b/src/app/access-control/group-registry/group-registry.reducers.spec.ts @@ -1,6 +1,12 @@ import { GroupMock } from '../../shared/testing/group-mock'; -import { GroupRegistryCancelGroupAction, GroupRegistryEditGroupAction } from './group-registry.actions'; -import { groupRegistryReducer, GroupRegistryState } from './group-registry.reducers'; +import { + GroupRegistryCancelGroupAction, + GroupRegistryEditGroupAction, +} from './group-registry.actions'; +import { + groupRegistryReducer, + GroupRegistryState, +} from './group-registry.reducers'; const initialState: GroupRegistryState = { editGroup: null, diff --git a/src/app/access-control/group-registry/group-registry.reducers.ts b/src/app/access-control/group-registry/group-registry.reducers.ts index 8e288b7f3a4..0bb3ad4b5c2 100644 --- a/src/app/access-control/group-registry/group-registry.reducers.ts +++ b/src/app/access-control/group-registry/group-registry.reducers.ts @@ -1,5 +1,9 @@ import { Group } from '../../core/eperson/models/group.model'; -import { GroupRegistryAction, GroupRegistryActionTypes, GroupRegistryEditGroupAction } from './group-registry.actions'; +import { + GroupRegistryAction, + GroupRegistryActionTypes, + GroupRegistryEditGroupAction, +} from './group-registry.actions'; /** * The metadata registry state. @@ -27,13 +31,13 @@ export function groupRegistryReducer(state = initialState, action: GroupRegistry case GroupRegistryActionTypes.EDIT_GROUP: { return Object.assign({}, state, { - editGroup: (action as GroupRegistryEditGroupAction).group + editGroup: (action as GroupRegistryEditGroupAction).group, }); } case GroupRegistryActionTypes.CANCEL_EDIT_GROUP: { return Object.assign({}, state, { - editGroup: null + editGroup: null, }); } 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 828aadc95a4..c2d998d9541 100644 --- a/src/app/access-control/group-registry/groups-registry.component.html +++ b/src/app/access-control/group-registry/groups-registry.component.html @@ -2,17 +2,17 @@
- +

{{messagePrefix + 'head' | translate}}

- +
@@ -33,11 +33,10 @@
- + @@ -91,7 +90,7 @@
-
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 c7900cd2785..105a7623d6b 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 @@ -1,12 +1,16 @@ -import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing'; - -import { StatisticsTableComponent } from './statistics-table.component'; -import { UsageReport } from '../../core/statistics/models/usage-report.model'; import { DebugElement } from '@angular/core'; +import { + ComponentFixture, + TestBed, + waitForAsync, +} from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { TranslateModule } from '@ngx-translate/core'; -import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service'; + import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; +import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service'; +import { UsageReport } from '../../core/statistics/models/usage-report.model'; +import { StatisticsTableComponent } from './statistics-table.component'; describe('StatisticsTableComponent', () => { @@ -18,8 +22,6 @@ describe('StatisticsTableComponent', () => { TestBed.configureTestingModule({ imports: [ TranslateModule.forRoot(), - ], - declarations: [ StatisticsTableComponent, ], providers: [ @@ -27,7 +29,7 @@ describe('StatisticsTableComponent', () => { { provide: DSONameService, useValue: {} }, ], }) - .compileComponents(); + .compileComponents(); })); beforeEach(() => { @@ -69,8 +71,8 @@ describe('StatisticsTableComponent', () => { views: 8, downloads: 8, }, - } - ] + }, + ], }); component.ngOnInit(); fixture.detectChanges(); 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 45924caa8d1..cd717305681 100644 --- a/src/app/statistics-page/statistics-table/statistics-table.component.ts +++ b/src/app/statistics-page/statistics-table/statistics-table.component.ts @@ -1,11 +1,33 @@ -import { Component, Input, OnInit } from '@angular/core'; -import { Point, UsageReport } from '../../core/statistics/models/usage-report.model'; -import { Observable, of } from 'rxjs'; -import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; +import { + AsyncPipe, + NgFor, + NgIf, +} from '@angular/common'; +import { + Component, + Input, + OnInit, +} from '@angular/core'; +import { + TranslateModule, + TranslateService, +} from '@ngx-translate/core'; +import { + Observable, + of, +} from 'rxjs'; import { map } from 'rxjs/operators'; -import { getRemoteDataPayload, getFinishedRemoteData } from '../../core/shared/operators'; + +import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service'; -import { TranslateService } from '@ngx-translate/core'; +import { + getFinishedRemoteData, + getRemoteDataPayload, +} from '../../core/shared/operators'; +import { + Point, + UsageReport, +} from '../../core/statistics/models/usage-report.model'; import { isEmpty } from '../../shared/empty.util'; /** @@ -14,7 +36,9 @@ import { isEmpty } from '../../shared/empty.util'; @Component({ selector: 'ds-statistics-table', templateUrl: './statistics-table.component.html', - styleUrls: ['./statistics-table.component.scss'] + styleUrls: ['./statistics-table.component.scss'], + standalone: true, + imports: [NgIf, NgFor, AsyncPipe, TranslateModule], }) export class StatisticsTableComponent implements OnInit { diff --git a/src/app/statistics/angulartics/dspace-provider.spec.ts b/src/app/statistics/angulartics/dspace-provider.spec.ts index 1ec7a9f06bc..80176c3e949 100644 --- a/src/app/statistics/angulartics/dspace-provider.spec.ts +++ b/src/app/statistics/angulartics/dspace-provider.spec.ts @@ -1,8 +1,9 @@ -import { Angulartics2DSpace } from './dspace-provider'; import { Angulartics2 } from 'angulartics2'; -import { StatisticsService } from '../statistics.service'; -import { filter } from 'rxjs/operators'; import { of as observableOf } from 'rxjs'; +import { filter } from 'rxjs/operators'; + +import { StatisticsService } from '../statistics.service'; +import { Angulartics2DSpace } from './dspace-provider'; describe('Angulartics2DSpace', () => { let provider: Angulartics2DSpace; @@ -11,13 +12,13 @@ describe('Angulartics2DSpace', () => { beforeEach(() => { angulartics2 = { - eventTrack: observableOf({action: 'page_view', properties: { + eventTrack: observableOf({ action: 'page_view', properties: { object: 'mock-object', - referrer: 'https://www.referrer.com' - }}), - filterDeveloperMode: () => filter(() => true) + referrer: 'https://www.referrer.com', + } }), + filterDeveloperMode: () => filter(() => true), } as any; - statisticsService = jasmine.createSpyObj('statisticsService', {trackViewEvent: null}); + statisticsService = jasmine.createSpyObj('statisticsService', { trackViewEvent: null }); provider = new Angulartics2DSpace(angulartics2, statisticsService); }); diff --git a/src/app/statistics/angulartics/dspace-provider.ts b/src/app/statistics/angulartics/dspace-provider.ts index 86c16b5c011..c4143bbf970 100644 --- a/src/app/statistics/angulartics/dspace-provider.ts +++ b/src/app/statistics/angulartics/dspace-provider.ts @@ -1,11 +1,15 @@ import { Injectable } from '@angular/core'; -import { Angulartics2 } from 'angulartics2'; +import { + Angulartics2, + EventTrack, +} from 'angulartics2'; + import { StatisticsService } from '../statistics.service'; /** * Angulartics2DSpace is a angulartics2 plugin that provides DSpace with the events. */ -@Injectable({providedIn: 'root'}) +@Injectable({ providedIn: 'root' }) export class Angulartics2DSpace { constructor( @@ -23,7 +27,7 @@ export class Angulartics2DSpace { .subscribe((event) => this.eventTrack(event)); } - private eventTrack(event) { + private eventTrack(event: Partial): void { if (event.action === 'page_view') { this.statisticsService.trackViewEvent(event.properties.object, event.properties.referrer); } else if (event.action === 'search') { @@ -32,7 +36,7 @@ export class Angulartics2DSpace { event.properties.page, event.properties.sort, event.properties.filters, - event.properties.clickedObject, + event.properties.clickedObject?.split('?')[0], ); } } diff --git a/src/app/statistics/angulartics/dspace/view-tracker.component.ts b/src/app/statistics/angulartics/dspace/view-tracker.component.ts index 805d311cfd9..801f4fd2cb9 100644 --- a/src/app/statistics/angulartics/dspace/view-tracker.component.ts +++ b/src/app/statistics/angulartics/dspace/view-tracker.component.ts @@ -1,10 +1,16 @@ -import { Component, Input, OnInit, OnDestroy } from '@angular/core'; +import { + Component, + Input, + OnDestroy, + OnInit, +} from '@angular/core'; import { Angulartics2 } from 'angulartics2'; -import { DSpaceObject } from '../../../core/shared/dspace-object.model'; -import { Subscription } from 'rxjs/internal/Subscription'; +import { Subscription } from 'rxjs'; import { take } from 'rxjs/operators'; -import { hasValue } from '../../../shared/empty.util'; + import { ReferrerService } from '../../../core/services/referrer.service'; +import { DSpaceObject } from '../../../core/shared/dspace-object.model'; +import { hasValue } from '../../../shared/empty.util'; /** * This component triggers a page view statistic @@ -13,6 +19,7 @@ import { ReferrerService } from '../../../core/services/referrer.service'; selector: 'ds-view-tracker', styleUrls: ['./view-tracker.component.scss'], templateUrl: './view-tracker.component.html', + standalone: true, }) export class ViewTrackerComponent implements OnInit, OnDestroy { /** @@ -28,7 +35,7 @@ export class ViewTrackerComponent implements OnInit, OnDestroy { constructor( public angulartics2: Angulartics2, - public referrerService: ReferrerService + public referrerService: ReferrerService, ) { } @@ -40,7 +47,7 @@ export class ViewTrackerComponent implements OnInit, OnDestroy { action: 'page_view', properties: { object: this.object, - referrer + referrer, }, }); }); diff --git a/src/app/statistics/google-analytics.service.spec.ts b/src/app/statistics/google-analytics.service.spec.ts index 2465e4db0ef..1b8783f3bf7 100644 --- a/src/app/statistics/google-analytics.service.spec.ts +++ b/src/app/statistics/google-analytics.service.spec.ts @@ -4,12 +4,15 @@ import { } from 'angulartics2'; import { of } from 'rxjs'; -import { GoogleAnalyticsService } from './google-analytics.service'; import { ConfigurationDataService } from '../core/data/configuration-data.service'; -import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils'; 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$, +} from '../shared/remote-data.utils'; +import { GoogleAnalyticsService } from './google-analytics.service'; describe('GoogleAnalyticsService', () => { const trackingIdProp = 'google.analytics.key'; @@ -21,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; @@ -44,8 +47,8 @@ describe('GoogleAnalyticsService', () => { 'startTracking', ]); - klaroServiceSpy = jasmine.createSpyObj('KlaroService', { - 'getSavedPreferences': jasmine.createSpy('getSavedPreferences') + orejimeServiceSpy = jasmine.createSpyObj('OrejimeService', { + 'getSavedPreferences': jasmine.createSpy('getSavedPreferences'), }); configSpy = createConfigSuccessSpy(trackingIdV4TestValue); @@ -54,7 +57,7 @@ describe('GoogleAnalyticsService', () => { set src(newVal) { /* noop */ }, get src() { return innerHTMLTestValue; }, set innerHTML(newVal) { /* noop */ }, - get innerHTML() { return srcTestValue; } + get innerHTML() { return srcTestValue; }, }; innerHTMLSpy = spyOnProperty(scriptElementMock, 'innerHTML', 'set'); @@ -70,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', () => { @@ -94,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', () => { @@ -117,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', () => { @@ -138,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', () => { @@ -158,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', () => { @@ -180,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', () => { @@ -217,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 9d32a610936..d638b0ce391 100644 --- a/src/app/statistics/google-analytics.service.ts +++ b/src/app/statistics/google-analytics.service.ts @@ -1,6 +1,8 @@ import { DOCUMENT } from '@angular/common'; -import { Inject, Injectable } from '@angular/core'; - +import { + Inject, + Injectable, +} from '@angular/core'; import { Angulartics2GoogleAnalytics, Angulartics2GoogleGlobalSiteTag, @@ -9,9 +11,9 @@ import { combineLatest } from 'rxjs'; import { ConfigurationDataService } from '../core/data/configuration-data.service'; import { getFirstCompletedRemoteData } from '../core/shared/operators'; +import { OrejimeService } from '../shared/cookies/orejime.service'; +import { GOOGLE_ANALYTICS_OREJIME_KEY } from '../shared/cookies/orejime-configuration'; import { isEmpty } from '../shared/empty.util'; -import { KlaroService } from '../shared/cookies/klaro.service'; -import { GOOGLE_ANALYTICS_KLARO_KEY } from '../shared/cookies/klaro-configuration'; /** * Set up Google Analytics on the client side. @@ -23,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, ) { @@ -39,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 0a3671432e4..8bee88c7dab 100644 --- a/src/app/statistics/statistics-endpoint.model.ts +++ b/src/app/statistics/statistics-endpoint.model.ts @@ -1,10 +1,14 @@ -import { HALLink } from '../core/shared/hal-link.model'; +import { + autoserialize, + deserialize, +} from 'cerialize'; + import { typedObject } from '../core/cache/builders/build-decorators'; -import { excludeFromEquals } from '../core/utilities/equals.decorators'; -import { autoserialize, deserialize } from 'cerialize'; +import { CacheableObject } from '../core/cache/cacheable-object.model'; +import { HALLink } from '../core/shared/hal-link.model'; import { ResourceType } from '../core/shared/resource-type'; +import { excludeFromEquals } from '../core/utilities/equals.decorators'; import { STATISTICS_ENDPOINT } from './statistics-endpoint.resource-type'; -import { CacheableObject } from '../core/cache/cacheable-object.model'; /** * Model class for the statistics endpoint diff --git a/src/app/statistics/statistics.module.ts b/src/app/statistics/statistics.module.ts deleted file mode 100644 index 4870e4fbf0b..00000000000 --- a/src/app/statistics/statistics.module.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { ModuleWithProviders, NgModule } from '@angular/core'; -import { CommonModule } from '@angular/common'; -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/statistics/statistics.service.spec.ts b/src/app/statistics/statistics.service.spec.ts index 45e954c51c4..ff57e319703 100644 --- a/src/app/statistics/statistics.service.spec.ts +++ b/src/app/statistics/statistics.service.spec.ts @@ -1,11 +1,12 @@ -import { StatisticsService } from './statistics.service'; -import { RequestService } from '../core/data/request.service'; -import { HALEndpointServiceStub } from '../shared/testing/hal-endpoint-service.stub'; -import { getMockRequestService } from '../shared/mocks/request.service.mock'; import isEqual from 'lodash/isEqual'; + +import { RequestService } from '../core/data/request.service'; +import { RestRequest } from '../core/data/rest-request.model'; import { DSpaceObjectType } from '../core/shared/dspace-object-type.model'; +import { getMockRequestService } from '../shared/mocks/request.service.mock'; import { SearchOptions } from '../shared/search/models/search-options.model'; -import { RestRequest } from '../core/data/rest-request.model'; +import { HALEndpointServiceStub } from '../shared/testing/hal-endpoint-service.stub'; +import { StatisticsService } from './statistics.service'; describe('StatisticsService', () => { let service: StatisticsService; @@ -25,7 +26,7 @@ describe('StatisticsService', () => { service = initTestService(); it('should send a request to track an item view ', () => { - const mockItem: any = {uuid: 'mock-item-uuid', type: 'item'}; + const mockItem: any = { uuid: 'mock-item-uuid', type: 'item' }; service.trackViewEvent(mockItem, 'https://www.referrer.com'); const request: RestRequest = requestService.send.calls.mostRecent().args[0]; expect(request.body).toBeDefined('request.body'); @@ -48,9 +49,9 @@ describe('StatisticsService', () => { size: 10, totalElements: 248, totalPages: 25, - number: 4 + number: 4, }; - const sort = {by: 'search-field', order: 'ASC'}; + const sort = { by: 'search-field', order: 'ASC' }; service.trackSearchEvent(mockSearch, page, sort); const request: RestRequest = requestService.send.calls.mostRecent().args[0]; const body = JSON.parse(request.body); @@ -64,14 +65,14 @@ describe('StatisticsService', () => { size: 10, totalElements: 248, totalPages: 25, - number: 4 + number: 4, }); }); it('should specify the sort options', () => { expect(body.sort).toEqual({ by: 'search-field', - order: 'asc' + order: 'asc', }); }); }); @@ -84,29 +85,29 @@ describe('StatisticsService', () => { query: 'mock-query', configuration: 'mock-configuration', dsoTypes: [DSpaceObjectType.ITEM], - scope: 'mock-scope' + scope: 'mock-scope', }); const page = { size: 10, totalElements: 248, totalPages: 25, - number: 4 + number: 4, }; - const sort = {by: 'search-field', order: 'ASC'}; + const sort = { by: 'search-field', order: 'ASC' }; const filters = [ { filter: 'title', operator: 'notcontains', value: 'dolor sit', - label: 'dolor sit' + label: 'dolor sit', }, { filter: 'author', operator: 'authority', value: '9zvxzdm4qru17or5a83wfgac', - label: 'Amet, Consectetur' - } + label: 'Amet, Consectetur', + }, ]; service.trackSearchEvent(mockSearch, page, sort, filters); const request: RestRequest = requestService.send.calls.mostRecent().args[0]; @@ -130,14 +131,14 @@ describe('StatisticsService', () => { filter: 'title', operator: 'notcontains', value: 'dolor sit', - label: 'dolor sit' + label: 'dolor sit', }, { filter: 'author', operator: 'authority', value: '9zvxzdm4qru17or5a83wfgac', - label: 'Amet, Consectetur' - } + label: 'Amet, Consectetur', + }, ])).toBe(true); }); }); diff --git a/src/app/statistics/statistics.service.ts b/src/app/statistics/statistics.service.ts index 56181d233c8..9c63b67be73 100644 --- a/src/app/statistics/statistics.service.ts +++ b/src/app/statistics/statistics.service.ts @@ -1,17 +1,24 @@ -import { RequestService } from '../core/data/request.service'; import { Injectable } from '@angular/core'; +import { + map, + take, +} from 'rxjs/operators'; + +import { RequestService } from '../core/data/request.service'; +import { RestRequest } from '../core/data/rest-request.model'; import { DSpaceObject } from '../core/shared/dspace-object.model'; -import { map, take } from 'rxjs/operators'; -import { TrackRequest } from './track-request.model'; -import { hasValue, isNotEmpty } from '../shared/empty.util'; import { HALEndpointService } from '../core/shared/hal-endpoint.service'; +import { + hasValue, + isNotEmpty, +} from '../shared/empty.util'; import { SearchOptions } from '../shared/search/models/search-options.model'; -import { RestRequest } from '../core/data/rest-request.model'; +import { TrackRequest } from './track-request.model'; /** * The statistics service */ -@Injectable({providedIn: 'root'}) +@Injectable({ providedIn: 'root' }) export class StatisticsService { constructor( @@ -24,7 +31,7 @@ export class StatisticsService { const requestId = this.requestService.generateRequestId(); this.halService.getEndpoint(linkPath).pipe( map((endpoint: string) => new TrackRequest(requestId, endpoint, JSON.stringify(body))), - take(1) // otherwise the previous events will fire again + take(1), // otherwise the previous events will fire again ).subscribe((request: RestRequest) => this.requestService.send(request)); } @@ -35,12 +42,12 @@ export class StatisticsService { */ trackViewEvent( dso: DSpaceObject, - referrer: string + referrer: string, ) { this.sendEvent('/statistics/viewevents', { targetId: dso.uuid, targetType: (dso as any).type, - referrer + referrer, }); } @@ -65,11 +72,11 @@ export class StatisticsService { size: page.size, totalElements: page.totalElements, totalPages: page.totalPages, - number: page.number + number: page.number, }, sort: { by: sort.by, - order: sort.order.toLowerCase() + order: sort.order.toLowerCase(), }, }; if (hasValue(searchOptions.configuration)) { @@ -89,7 +96,7 @@ export class StatisticsService { filter: filter.filter, operator: filter.operator, value: filter.value, - label: filter.label + label: filter.label, }); } Object.assign(body, { appliedFilters: bodyFilters }); diff --git a/src/app/store.actions.ts b/src/app/store.actions.ts index 59ce00a243a..5839778ac05 100644 --- a/src/app/store.actions.ts +++ b/src/app/store.actions.ts @@ -1,6 +1,7 @@ -import { type } from './shared/ngrx/type'; import { Action } from '@ngrx/store'; + import { AppState } from './app.reducer'; +import { type } from './shared/ngrx/type'; export const StoreActionTypes = { REHYDRATE: type('dspace/ngrx/REHYDRATE'), diff --git a/src/app/store.effects.ts b/src/app/store.effects.ts index c7add76add6..4efb940c3d0 100644 --- a/src/app/store.effects.ts +++ b/src/app/store.effects.ts @@ -1,17 +1,24 @@ +import { Injectable } from '@angular/core'; +import { + Actions, + createEffect, + ofType, +} from '@ngrx/effects'; +import { + Action, + Store, +} from '@ngrx/store'; import { of as observableOf } from 'rxjs'; import { map } from 'rxjs/operators'; -import { Injectable } from '@angular/core'; -import { Action, Store } from '@ngrx/store'; -import { Actions, createEffect, ofType } from '@ngrx/effects'; import { AppState } from './app.reducer'; -import { StoreActionTypes } from './store.actions'; import { HostWindowResizeAction } from './shared/host-window.actions'; +import { StoreActionTypes } from './store.actions'; @Injectable() export class StoreEffects { - replay = createEffect(() => this.actions.pipe( + replay = createEffect(() => this.actions.pipe( ofType(StoreActionTypes.REPLAY), map((replayAction: Action) => { // TODO: should be able to replay all actions before the browser attempts to @@ -21,9 +28,9 @@ export class StoreEffects { return observableOf({}); })), { dispatch: false }); - resize = createEffect(() => this.actions.pipe( + resize = createEffect(() => this.actions.pipe( ofType(StoreActionTypes.REPLAY, StoreActionTypes.REHYDRATE), - map(() => new HostWindowResizeAction(window.innerWidth, window.innerHeight)) + map(() => new HostWindowResizeAction(window.innerWidth, window.innerHeight)), )); constructor(private actions: Actions, private store: Store) { diff --git a/src/app/submission/edit/submission-edit.component.spec.ts b/src/app/submission/edit/submission-edit.component.spec.ts index 8013162d85c..59f9883f19b 100644 --- a/src/app/submission/edit/submission-edit.component.spec.ts +++ b/src/app/submission/edit/submission-edit.component.spec.ts @@ -1,25 +1,44 @@ -import { waitForAsync, ComponentFixture, fakeAsync, TestBed } from '@angular/core/testing'; -import { RouterTestingModule } from '@angular/router/testing'; -import { ActivatedRoute, Router } from '@angular/router'; import { NO_ERRORS_SCHEMA } from '@angular/core'; - -import { of as observableOf } from 'rxjs'; -import { TranslateModule, TranslateService } from '@ngx-translate/core'; - -import { SubmissionEditComponent } from './submission-edit.component'; +import { + ComponentFixture, + fakeAsync, + TestBed, + waitForAsync, +} from '@angular/core/testing'; +import { + ActivatedRoute, + Router, +} from '@angular/router'; +import { RouterTestingModule } from '@angular/router/testing'; +import { provideMockStore } from '@ngrx/store/testing'; +import { TranslateModule } from '@ngx-translate/core'; +import { + 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 { 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 { SubmissionService } from '../submission.service'; -import { SubmissionServiceStub } from '../../shared/testing/submission-service.stub'; -import { getMockTranslateService } from '../../shared/mocks/translate.service.mock'; - import { RouterStub } from '../../shared/testing/router.stub'; -import { ActivatedRouteStub } from '../../shared/testing/active-router.stub'; -import { mockSubmissionObject } from '../../shared/mocks/submission.mock'; -import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; -import { ItemDataService } from '../../core/data/item-data.service'; +import { SectionsServiceStub } from '../../shared/testing/sections-service.stub'; import { SubmissionJsonPatchOperationsServiceStub } from '../../shared/testing/submission-json-patch-operations-service.stub'; -import { SubmissionJsonPatchOperationsService } from '../../core/submission/submission-json-patch-operations.service'; +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'; describe('SubmissionEditComponent Component', () => { @@ -29,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(); @@ -38,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] + schemas: [NO_ERRORS_SCHEMA], + }).overrideComponent(SubmissionEditComponent, { + remove: { + imports: [ SubmissionFormComponent ], + }, }).compileComponents(); })); @@ -78,8 +114,11 @@ describe('SubmissionEditComponent Component', () => { route.testParams = { id: submissionId }; submissionServiceStub.retrieveSubmission.and.returnValue( - createSuccessfulRemoteDataObject$(submissionObject) + createSuccessfulRemoteDataObject$(submissionObject), ); + submissionServiceStub.getSubmissionObject.and.returnValue(observableOf(submissionObject)); + submissionServiceStub.getSubmissionStatus.and.returnValue(observableOf(true)); + fixture.detectChanges(); @@ -94,7 +133,7 @@ describe('SubmissionEditComponent Component', () => { it('should redirect to mydspace when an empty SubmissionObject has been retrieved',() => { route.testParams = { id: submissionId }; - submissionServiceStub.retrieveSubmission.and.returnValue(createSuccessfulRemoteDataObject$({}) + submissionServiceStub.retrieveSubmission.and.returnValue(createSuccessfulRemoteDataObject$({}), ); fixture.detectChanges(); diff --git a/src/app/submission/edit/submission-edit.component.ts b/src/app/submission/edit/submission-edit.component.ts index 93200e5e5c6..822818ee243 100644 --- a/src/app/submission/edit/submission-edit.component.ts +++ b/src/app/submission/edit/submission-edit.component.ts @@ -1,32 +1,57 @@ -import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; -import { ActivatedRoute, ParamMap, Router } from '@angular/router'; - -import { BehaviorSubject, Subscription } from 'rxjs'; -import { debounceTime, filter, switchMap } from 'rxjs/operators'; +import { + ChangeDetectorRef, + Component, + OnDestroy, + OnInit, +} from '@angular/core'; +import { + ActivatedRoute, + ParamMap, + Router, +} from '@angular/router'; import { TranslateService } from '@ngx-translate/core'; +import { + BehaviorSubject, + Subscription, +} from 'rxjs'; +import { + debounceTime, + filter, + switchMap, +} from 'rxjs/operators'; -import { WorkspaceitemSectionsObject } from '../../core/submission/models/workspaceitem-sections.model'; -import { hasValue, isEmpty, isNotEmptyOperator, isNotNull } from '../../shared/empty.util'; import { SubmissionDefinitionsModel } from '../../core/config/models/config-submission-definitions.model'; -import { SubmissionService } from '../submission.service'; -import { NotificationsService } from '../../shared/notifications/notifications.service'; -import { SubmissionObject } from '../../core/submission/models/submission-object.model'; -import { Collection } from '../../core/shared/collection.model'; +import { ItemDataService } from '../../core/data/item-data.service'; import { RemoteData } from '../../core/data/remote-data'; +import { Collection } from '../../core/shared/collection.model'; import { Item } from '../../core/shared/item.model'; import { getAllSucceededRemoteData } from '../../core/shared/operators'; -import { ItemDataService } from '../../core/data/item-data.service'; +import { SubmissionObject } from '../../core/submission/models/submission-object.model'; +import { WorkspaceitemSectionsObject } from '../../core/submission/models/workspaceitem-sections.model'; import { SubmissionJsonPatchOperationsService } from '../../core/submission/submission-json-patch-operations.service'; -import parseSectionErrors from '../utils/parseSectionErrors'; +import { + hasValue, + isEmpty, + isNotEmptyOperator, + 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'; /** * 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' + templateUrl: './submission-edit.component.html', + standalone: true, + imports: [ + SubmissionFormComponent, + ], }) export class SubmissionEditComponent implements OnDestroy, OnInit { @@ -123,7 +148,7 @@ export class SubmissionEditComponent implements OnDestroy, OnInit { this.route.paramMap.pipe( switchMap((params: ParamMap) => this.submissionService.retrieveSubmission(params.get('id'))), // NOTE new submission is retrieved on the browser side only, so get null on server side rendering - filter((submissionObjectRD: RemoteData) => isNotNull(submissionObjectRD)) + filter((submissionObjectRD: RemoteData) => isNotNull(submissionObjectRD)), ).subscribe((submissionObjectRD: RemoteData) => { if (submissionObjectRD.hasSucceeded) { if (isEmpty(submissionObjectRD.payload)) { @@ -151,7 +176,7 @@ export class SubmissionEditComponent implements OnDestroy, OnInit { this.itemLink$.pipe( isNotEmptyOperator(), switchMap((itemLink: string) => - this.itemDataService.findByHref(itemLink) + this.itemDataService.findByHref(itemLink), ), getAllSucceededRemoteData(), // Multiple sources can update the item in quick succession. diff --git a/src/app/submission/edit/themed-submission-edit.component.ts b/src/app/submission/edit/themed-submission-edit.component.ts index bbaf432c134..e1abb0634e1 100644 --- a/src/app/submission/edit/themed-submission-edit.component.ts +++ b/src/app/submission/edit/themed-submission-edit.component.ts @@ -2,13 +2,16 @@ * Themed wrapper for SubmissionEditComponent */ import { Component } from '@angular/core'; + 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' + 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 a78d737640c..16370a5873b 100644 --- a/src/app/submission/form/collection/submission-form-collection.component.html +++ b/src/app/submission/form/collection/submission-form-collection.component.html @@ -1,6 +1,6 @@
{{ 'submission.sections.general.collection' | translate }} @@ -25,19 +25,19 @@ class="btn btn-outline-primary" (blur)="onClose()" (click)="onClose()" - [disabled]="(processingChange$ | async) || collectionModifiable == false || isReadonly" + [disabled]="(processingChange$ | async) || collectionModifiable === false || isReadonly" ngbDropdownToggle> - {{ selectedCollectionName$ | async }} + {{ selectedCollectionName$ | async }}
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 c76de83b833..540c2ba561a 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 @@ -1,30 +1,48 @@ -import { ChangeDetectorRef, Component, CUSTOM_ELEMENTS_SCHEMA, DebugElement } from '@angular/core'; -import { ComponentFixture, fakeAsync, inject, TestBed, tick, waitForAsync } from '@angular/core/testing'; +import { + ChangeDetectorRef, + Component, + CUSTOM_ELEMENTS_SCHEMA, + DebugElement, +} from '@angular/core'; +import { + ComponentFixture, + fakeAsync, + inject, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import { + FormsModule, + ReactiveFormsModule, +} from '@angular/forms'; import { By } from '@angular/platform-browser'; -import { FormsModule, ReactiveFormsModule } from '@angular/forms'; - -import { TranslateModule } from '@ngx-translate/core'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { Store } from '@ngrx/store'; +import { TranslateModule } from '@ngx-translate/core'; import { cold } from 'jasmine-marbles'; import { of } from 'rxjs'; -import { SubmissionServiceStub } from '../../../shared/testing/submission-service.stub'; -import { mockSubmissionId, mockSubmissionRestResponse } from '../../../shared/mocks/submission.mock'; -import { SubmissionService } from '../../submission.service'; -import { SubmissionFormCollectionComponent } from './submission-form-collection.component'; +import { DSONameService } from '../../../core/breadcrumbs/dso-name.service'; +import { CollectionDataService } from '../../../core/data/collection-data.service'; import { CommunityDataService } from '../../../core/data/community-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 { 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, + mockSubmissionRestResponse, +} from '../../../shared/mocks/submission.mock'; +import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; import { SubmissionJsonPatchOperationsServiceStub } from '../../../shared/testing/submission-json-patch-operations-service.stub'; -import { JsonPatchOperationsBuilder } from '../../../core/json-patch/builder/json-patch-operations-builder'; -import { JsonPatchOperationPathCombiner } from '../../../core/json-patch/builder/json-patch-operation-path-combiner'; +import { SubmissionServiceStub } from '../../../shared/testing/submission-service.stub'; import { createTestComponent } from '../../../shared/testing/utils.test'; -import { CollectionDataService } from '../../../core/data/collection-data.service'; import { SectionsService } from '../../sections/sections.service'; -import { Collection } from '../../../core/shared/collection.model'; -import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; -import { DSONameService } from '../../../core/breadcrumbs/dso-name.service'; -import { DSONameServiceMock } from '../../../shared/mocks/dso-name.service.mock'; +import { SubmissionService } from '../../submission.service'; +import { SubmissionFormCollectionComponent } from './submission-form-collection.component'; describe('SubmissionFormCollectionComponent Component', () => { @@ -46,11 +64,11 @@ describe('SubmissionFormCollectionComponent Component', () => { { key: 'dc.title', language: 'en_US', - value: 'Community 1-Collection 1' + value: 'Community 1-Collection 1', }], _links: { - defaultAccessConditions: collectionId + '/defaultAccessConditions' - } + defaultAccessConditions: collectionId + '/defaultAccessConditions', + }, }); const mockCollectionList = [ @@ -58,71 +76,71 @@ describe('SubmissionFormCollectionComponent Component', () => { communities: [ { id: '123456789-1', - name: 'Community 1' - } + name: 'Community 1', + }, ], collection: { id: '1234567890-1', - name: 'Community 1-Collection 1' - } + name: 'Community 1-Collection 1', + }, }, { communities: [ { id: '123456789-1', - name: 'Community 1' - } + name: 'Community 1', + }, ], collection: { id: '1234567890-2', - name: 'Community 1-Collection 2' - } + name: 'Community 1-Collection 2', + }, }, { communities: [ { id: '123456789-2', - name: 'Community 2' - } + name: 'Community 2', + }, ], collection: { id: '1234567890-3', - name: 'Community 2-Collection 1' - } + name: 'Community 2-Collection 1', + }, }, { communities: [ { id: '123456789-2', - name: 'Community 2' - } + name: 'Community 2', + }, ], collection: { id: '1234567890-4', - name: 'Community 2-Collection 2' - } - } + name: 'Community 2-Collection 2', + }, + }, ]; const communityDataService: any = jasmine.createSpyObj('communityDataService', { - findAll: jasmine.createSpy('findAll') + findAll: jasmine.createSpy('findAll'), }); const collectionDataService: any = jasmine.createSpyObj('collectionDataService', { findById: jasmine.createSpy('findById'), - getAuthorizedCollectionByCommunity: jasmine.createSpy('getAuthorizedCollectionByCommunity') + getAuthorizedCollectionByCommunity: jasmine.createSpy('getAuthorizedCollectionByCommunity'), }); const store: any = jasmine.createSpyObj('store', { dispatch: jasmine.createSpy('dispatch'), - select: jasmine.createSpy('select') + select: jasmine.createSpy('select'), }); const jsonPatchOpBuilder: any = jasmine.createSpyObj('jsonPatchOpBuilder', { - replace: jasmine.createSpy('replace') + replace: jasmine.createSpy('replace'), }); const sectionsService: any = jasmine.createSpyObj('sectionsService', { - isSectionTypeAvailable: of(true) + isSectionTypeAvailable: of(true), }); beforeEach(waitForAsync(() => { @@ -131,11 +149,9 @@ describe('SubmissionFormCollectionComponent Component', () => { FormsModule, ReactiveFormsModule, NgbModule, - TranslateModule.forRoot() - ], - declarations: [ + TranslateModule.forRoot(), SubmissionFormCollectionComponent, - TestComponent + TestComponent, ], providers: [ { provide: DSONameService, useValue: new DSONameServiceMock() }, @@ -147,10 +163,14 @@ describe('SubmissionFormCollectionComponent Component', () => { { provide: Store, useValue: store }, { provide: SectionsService, useValue: sectionsService }, ChangeDetectorRef, - SubmissionFormCollectionComponent + SubmissionFormCollectionComponent, ], - schemas: [CUSTOM_ELEMENTS_SCHEMA] - }).compileComponents(); + schemas: [CUSTOM_ELEMENTS_SCHEMA], + }) + .overrideComponent(SubmissionFormCollectionComponent, { + remove: { imports: [ThemedCollectionDropdownComponent] }, + }) + .compileComponents(); })); describe('', () => { @@ -281,7 +301,7 @@ describe('SubmissionFormCollectionComponent Component', () => { expect(submissionServiceStub.changeSubmissionCollection).toHaveBeenCalled(); expect(comp.selectedCollectionId).toBe(mockCollectionList[1].collection.id); expect(comp.selectedCollectionName$).toBeObservable(cold('(a|)', { - a: mockCollectionList[1].collection.name + a: mockCollectionList[1].collection.name, })); }); }); @@ -292,7 +312,11 @@ describe('SubmissionFormCollectionComponent Component', () => { // declare a test component @Component({ selector: 'ds-test-cmp', - template: `` + 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 964f86577af..2aec1c9e30b 100644 --- a/src/app/submission/form/collection/submission-form-collection.component.ts +++ b/src/app/submission/form/collection/submission-form-collection.component.ts @@ -1,35 +1,48 @@ +import { CommonModule } from '@angular/common'; import { ChangeDetectorRef, Component, EventEmitter, Input, OnChanges, + OnDestroy, OnInit, Output, SimpleChanges, - ViewChild + ViewChild, } from '@angular/core'; - -import { BehaviorSubject, Observable, of as observableOf, Subscription } from 'rxjs'; +import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'; +import { TranslateModule } from '@ngx-translate/core'; +import { + BehaviorSubject, + Observable, + of as observableOf, + Subscription, +} from 'rxjs'; import { find, - map, mergeMap + map, + mergeMap, } from 'rxjs/operators'; -import { Collection } from '../../../core/shared/collection.model'; -import { hasValue, isNotEmpty } from '../../../shared/empty.util'; +import { DSONameService } from '../../../core/breadcrumbs/dso-name.service'; +import { CollectionDataService } from '../../../core/data/collection-data.service'; import { RemoteData } from '../../../core/data/remote-data'; import { JsonPatchOperationPathCombiner } from '../../../core/json-patch/builder/json-patch-operation-path-combiner'; import { JsonPatchOperationsBuilder } from '../../../core/json-patch/builder/json-patch-operations-builder'; -import { SubmissionService } from '../../submission.service'; +import { Collection } from '../../../core/shared/collection.model'; +import { getFirstSucceededRemoteDataPayload } from '../../../core/shared/operators'; import { SubmissionObject } from '../../../core/submission/models/submission-object.model'; import { SubmissionJsonPatchOperationsService } from '../../../core/submission/submission-json-patch-operations.service'; -import { CollectionDataService } from '../../../core/data/collection-data.service'; import { CollectionDropdownComponent } from '../../../shared/collection-dropdown/collection-dropdown.component'; +import { ThemedCollectionDropdownComponent } from '../../../shared/collection-dropdown/themed-collection-dropdown.component'; +import { + hasValue, + isNotEmpty, +} from '../../../shared/empty.util'; import { SectionsService } from '../../sections/sections.service'; -import { getFirstSucceededRemoteDataPayload } from '../../../core/shared/operators'; import { SectionsType } from '../../sections/sections-type'; -import { DSONameService } from '../../../core/breadcrumbs/dso-name.service'; +import { SubmissionService } from '../../submission.service'; /** * This component allows to show the current collection the submission belonging to and to change it. @@ -37,9 +50,16 @@ import { DSONameService } from '../../../core/breadcrumbs/dso-name.service'; @Component({ selector: 'ds-submission-form-collection', styleUrls: ['./submission-form-collection.component.scss'], - templateUrl: './submission-form-collection.component.html' + 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 @@ -137,7 +157,7 @@ export class SubmissionFormCollectionComponent implements OnChanges, OnInit { this.selectedCollectionName$ = this.collectionDataService.findById(this.currentCollectionId).pipe( find((collectionRD: RemoteData) => isNotEmpty(collectionRD.payload)), - map((collectionRD: RemoteData) => this.dsoNameService.getName(collectionRD.payload)) + map((collectionRD: RemoteData) => this.dsoNameService.getName(collectionRD.payload)), ); } } @@ -147,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); } /** @@ -171,20 +191,20 @@ export class SubmissionFormCollectionComponent implements OnChanges, OnInit { this.submissionId, 'sections', 'collection').pipe( - mergeMap((submissionObject: SubmissionObject[]) => { - // retrieve the full submission object with embeds - return this.submissionService.retrieveSubmission(submissionObject[0].id).pipe( - getFirstSucceededRemoteDataPayload() - ); - }) - ).subscribe((submissionObject: SubmissionObject) => { - this.selectedCollectionId = event.collection.id; - this.selectedCollectionName$ = observableOf(event.collection.name); - this.collectionChange.emit(submissionObject); - this.submissionService.changeSubmissionCollection(this.submissionId, event.collection.id); - this.processingChange$.next(false); - this.cdr.detectChanges(); - }) + mergeMap((submissionObject: SubmissionObject[]) => { + // retrieve the full submission object with embeds + return this.submissionService.retrieveSubmission(submissionObject[0].id).pipe( + getFirstSucceededRemoteDataPayload(), + ); + }), + ).subscribe((submissionObject: SubmissionObject) => { + this.selectedCollectionId = event.collection.id; + this.selectedCollectionName$ = observableOf(event.collection.name); + this.collectionChange.emit(submissionObject); + this.submissionService.changeSubmissionCollection(this.submissionId, event.collection.id); + this.processingChange$.next(false); + this.cdr.detectChanges(); + }), ); } diff --git a/src/app/submission/form/footer/submission-form-footer.component.html b/src/app/submission/form/footer/submission-form-footer.component.html index e898e1c2207..f1162cea4de 100644 --- a/src/app/submission/form/footer/submission-form-footer.component.html +++ b/src/app/submission/form/footer/submission-form-footer.component.html @@ -11,10 +11,10 @@
- + {{'submission.general.info.saved' | translate}} - + {{'submission.general.info.pending-changes' | translate}}
@@ -28,12 +28,12 @@ class="btn btn-secondary" id="save" [attr.data-test]="'save' | dsBrowserOnly" - [disabled]="(processingSaveStatus | async) || !(hasUnsavedModification | async)" + [disabled]="(processingSaveStatus | async) || (hasUnsavedModification | async) !== true" (click)="save($event)"> {{'submission.general.save' | translate}} +
-
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 cd6d82c1416..989726e7e4c 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 @@ -1,32 +1,54 @@ -import { Component, NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, fakeAsync, inject, TestBed, waitForAsync } from '@angular/core/testing'; +import { + Component, + NO_ERRORS_SCHEMA, +} from '@angular/core'; +import { + ComponentFixture, + inject, + TestBed, + waitForAsync, +} from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +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'; -import { CollectionListEntry } from '../../../shared/collection-dropdown/collection-dropdown.component'; -import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; -import { By } from '@angular/platform-browser'; 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 + SubmissionImportExternalCollectionComponent, + { provide: ThemeService, useValue: themeService }, ], - schemas: [NO_ERRORS_SCHEMA] - }).compileComponents().then(); + schemas: [NO_ERRORS_SCHEMA], + }) + .overrideComponent(SubmissionImportExternalCollectionComponent, { + remove: { + imports: [ + ThemedLoadingComponent, + ThemedCollectionDropdownComponent, + ], + }, + }) + .compileComponents().then(); })); // First test to check the correct component creation @@ -70,11 +92,11 @@ describe('SubmissionImportExternalCollectionComponent test suite', () => { const entry = { communities: [ { id: 'community1' }, - { id: 'community2' } + { id: 'community2' }, ], collection: { - id: 'collection' - } + id: 'collection', + }, } as CollectionListEntry; comp.selectObject(entry); @@ -117,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'); }); })); @@ -134,7 +156,8 @@ describe('SubmissionImportExternalCollectionComponent test suite', () => { // declare a test component @Component({ selector: 'ds-test-cmp', - template: `` + 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 22430196d43..03bd10cfc29 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,6 +1,18 @@ -import { Component, EventEmitter, Output } from '@angular/core'; -import { CollectionListEntry } from '../../../shared/collection-dropdown/collection-dropdown.component'; +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'. @@ -8,7 +20,15 @@ import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; @Component({ selector: 'ds-submission-import-external-collection', styleUrls: ['./submission-import-external-collection.component.scss'], - templateUrl: './submission-import-external-collection.component.html' + templateUrl: './submission-import-external-collection.component.html', + imports: [ + ThemedLoadingComponent, + ThemedCollectionDropdownComponent, + TranslateModule, + NgClass, + NgIf, + ], + standalone: true, }) export class SubmissionImportExternalCollectionComponent { /** @@ -31,7 +51,7 @@ export class SubmissionImportExternalCollectionComponent { * @param {NgbActiveModal} activeModal */ constructor( - private activeModal: NgbActiveModal + private activeModal: NgbActiveModal, ) { } /** 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 bbb0dbcc942..beecd68d700 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 @@

{{'submission.import-external.preview.title.' + labelPrefix | translate}}

-
- {{'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 f7ff88c3b17..06a048709ab 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 @@ -1,24 +1,34 @@ -import { Component, NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, inject, TestBed, waitForAsync } from '@angular/core/testing'; +import { + Component, + NO_ERRORS_SCHEMA, +} from '@angular/core'; +import { + ComponentFixture, + inject, + TestBed, + waitForAsync, +} from '@angular/core/testing'; import { Router } from '@angular/router'; - +import { + NgbActiveModal, + NgbModal, +} from '@ng-bootstrap/ng-bootstrap'; import { TranslateModule } from '@ngx-translate/core'; -import { NgbActiveModal, NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { TestScheduler } from 'rxjs/testing'; -import { of as observableOf } from 'rxjs'; import { getTestScheduler } from 'jasmine-marbles'; +import { of as observableOf } from 'rxjs'; +import { TestScheduler } from 'rxjs/testing'; -import { SubmissionImportExternalPreviewComponent } from './submission-import-external-preview.component'; +import { ExternalSourceEntry } from '../../../core/shared/external-source-entry.model'; +import { Metadata } from '../../../core/shared/metadata.utils'; +import { CollectionListEntry } from '../../../shared/collection-dropdown/collection-dropdown.component'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; +import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub'; import { RouterStub } from '../../../shared/testing/router.stub'; -import { SubmissionService } from '../../submission.service'; -import { createTestComponent } from '../../../shared/testing/utils.test'; import { SubmissionServiceStub } from '../../../shared/testing/submission-service.stub'; -import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub'; -import { ExternalSourceEntry } from '../../../core/shared/external-source-entry.model'; -import { Metadata } from '../../../core/shared/metadata.utils'; +import { createTestComponent } from '../../../shared/testing/utils.test'; +import { SubmissionService } from '../../submission.service'; import { SubmissionImportExternalCollectionComponent } from '../import-external-collection/submission-import-external-collection.component'; -import { CollectionListEntry } from '../../../shared/collection-dropdown/collection-dropdown.component'; +import { SubmissionImportExternalPreviewComponent } from './submission-import-external-preview.component'; const externalEntry = Object.assign(new ExternalSourceEntry(), { id: '0001-0001-0001-0001', @@ -27,11 +37,11 @@ const externalEntry = Object.assign(new ExternalSourceEntry(), { metadata: { 'dc.identifier.uri': [ { - value: 'https://orcid.org/0001-0001-0001-0001' - } - ] + value: 'https://orcid.org/0001-0001-0001-0001', + }, + ], }, - _links: { self: { href: 'http://test-rest.com/server/api/integration/externalSources/orcidV2/entryValues/0000-0003-4851-8004' } } + _links: { self: { href: 'http://test-rest.com/server/api/integration/externalSources/orcidV2/entryValues/0000-0003-4851-8004' } }, }); describe('SubmissionImportExternalPreviewComponent test suite', () => { @@ -48,11 +58,9 @@ describe('SubmissionImportExternalPreviewComponent test suite', () => { scheduler = getTestScheduler(); TestBed.configureTestingModule({ imports: [ - TranslateModule.forRoot() - ], - declarations: [ + TranslateModule.forRoot(), SubmissionImportExternalPreviewComponent, - TestComponent + TestComponent, ], providers: [ { provide: Router, useValue: new RouterStub() }, @@ -60,9 +68,9 @@ describe('SubmissionImportExternalPreviewComponent test suite', () => { { provide: NotificationsService, useValue: new NotificationsServiceStub() }, { provide: NgbModal, useValue: ngbModal }, { provide: NgbActiveModal, useValue: ngbActiveModal }, - SubmissionImportExternalPreviewComponent + SubmissionImportExternalPreviewComponent, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents().then(); })); @@ -105,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(); @@ -126,23 +134,23 @@ describe('SubmissionImportExternalPreviewComponent test suite', () => { id: 'dummy', uuid: 'dummy', name: 'dummy', - } + }, ], collection: { id: 'ce64f48e-2c9b-411a-ac36-ee429c0e6a88', uuid: 'ce64f48e-2c9b-411a-ac36-ee429c0e6a88', name: 'Collection 1', - } + }, }; const submissionObjects = [ - { id: 'jk11k13o-9v4z-632i-sr88-wq071n0h1d47' } + { id: 'jk11k13o-9v4z-632i-sr88-wq071n0h1d47' }, ]; comp.externalSourceEntry = externalEntry; ngbModal.open.and.returnValue({ componentInstance: { selectedEvent: observableOf(emittedEvent) }, close: () => { return; - } + }, }); spyOn(comp, 'closeMetadataModal'); submissionServiceStub.createSubmissionFromExternalSource.and.returnValue(observableOf(submissionObjects)); @@ -162,7 +170,8 @@ describe('SubmissionImportExternalPreviewComponent test suite', () => { // declare a test component @Component({ selector: 'ds-test-cmp', - template: `` + 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 ccc9ede1427..c7f434b23ce 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,14 +1,25 @@ -import { Component, Input, OnInit } from '@angular/core'; +import { NgFor } from '@angular/common'; +import { + Component, + Input, + OnInit, +} from '@angular/core'; import { Router } from '@angular/router'; -import { NgbActiveModal, NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; +import { + NgbActiveModal, + 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'; import { MetadataValue } from '../../../core/shared/metadata.models'; import { Metadata } from '../../../core/shared/metadata.utils'; -import { CollectionListEntry } from '../../../shared/collection-dropdown/collection-dropdown.component'; -import { mergeMap } from 'rxjs/operators'; -import { SubmissionService } from '../../submission.service'; import { SubmissionObject } from '../../../core/submission/models/submission-object.model'; +import { CollectionListEntry } from '../../../shared/collection-dropdown/collection-dropdown.component'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; +import { SubmissionService } from '../../submission.service'; import { SubmissionImportExternalCollectionComponent } from '../import-external-collection/submission-import-external-collection.component'; /** @@ -17,7 +28,12 @@ import { SubmissionImportExternalCollectionComponent } from '../import-external- @Component({ selector: 'ds-submission-import-external-preview', styleUrls: ['./submission-import-external-preview.component.scss'], - templateUrl: './submission-import-external-preview.component.html' + templateUrl: './submission-import-external-preview.component.html', + imports: [ + NgFor, + TranslateModule, + ], + standalone: true, }) export class SubmissionImportExternalPreviewComponent implements OnInit { /** @@ -27,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 */ @@ -50,7 +66,7 @@ export class SubmissionImportExternalPreviewComponent implements OnInit { private submissionService: SubmissionService, private modalService: NgbModal, private router: Router, - private notificationService: NotificationsService + private notificationService: NotificationsService, ) { } /** @@ -62,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), }); }); } @@ -87,7 +103,7 @@ export class SubmissionImportExternalPreviewComponent implements OnInit { this.modalRef.componentInstance.selectedEvent.pipe( mergeMap((collectionListEntry: CollectionListEntry) => { return this.submissionService.createSubmissionFromExternalSource(this.externalSourceEntry._links.self.href, collectionListEntry.collection.id); - }) + }), ).subscribe((submissionObjects: SubmissionObject[]) => { let isValid = false; if (submissionObjects.length === 1) { diff --git a/src/app/submission/import-external/import-external-searchbar/submission-import-external-searchbar.component.html b/src/app/submission/import-external/import-external-searchbar/submission-import-external-searchbar.component.html index 535395e5345..7b5a2004d71 100644 --- a/src/app/submission/import-external/import-external-searchbar/submission-import-external-searchbar.component.html +++ b/src/app/submission/import-external/import-external-searchbar/submission-import-external-searchbar.component.html @@ -1,25 +1,28 @@
- +
- -
- -
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 e1f59c3a5ea..9005dc15893 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 @@ -1,29 +1,42 @@ -import { ChangeDetectorRef, Component, NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, inject, TestBed, waitForAsync } from '@angular/core/testing'; +import { + ChangeDetectorRef, + Component, + NO_ERRORS_SCHEMA, +} from '@angular/core'; +import { + ComponentFixture, + inject, + TestBed, + waitForAsync, +} from '@angular/core/testing'; import { TranslateModule } from '@ngx-translate/core'; +import { getTestScheduler } from 'jasmine-marbles'; import { of as observableOf } from 'rxjs'; -import { - SourceElement, - SubmissionImportExternalSearchbarComponent -} from './submission-import-external-searchbar.component'; +import { TestScheduler } from 'rxjs/testing'; + +import { RequestParam } from '../../../core/cache/models/request-param.model'; import { ExternalSourceDataService } from '../../../core/data/external-source-data.service'; -import { createTestComponent } from '../../../shared/testing/utils.test'; +import { FindListOptions } from '../../../core/data/find-list-options.model'; +import { + buildPaginatedList, + PaginatedList, +} from '../../../core/data/paginated-list.model'; +import { ExternalSource } from '../../../core/shared/external-source.model'; +import { PageInfo } from '../../../core/shared/page-info.model'; +import { HostWindowService } from '../../../shared/host-window.service'; import { externalSourceCiencia, externalSourceMyStaffDb, externalSourceOrcid, - getMockExternalSourceService + getMockExternalSourceService, } from '../../../shared/mocks/external-source.service.mock'; -import { PageInfo } from '../../../core/shared/page-info.model'; -import { buildPaginatedList, PaginatedList } from '../../../core/data/paginated-list.model'; import { createSuccessfulRemoteDataObject } from '../../../shared/remote-data.utils'; -import { ExternalSource } from '../../../core/shared/external-source.model'; -import { HostWindowService } from '../../../shared/host-window.service'; import { HostWindowServiceStub } from '../../../shared/testing/host-window-service.stub'; -import { getTestScheduler } from 'jasmine-marbles'; -import { TestScheduler } from 'rxjs/testing'; -import { RequestParam } from '../../../core/cache/models/request-param.model'; -import { FindListOptions } from '../../../core/data/find-list-options.model'; +import { createTestComponent } from '../../../shared/testing/utils.test'; +import { + SourceElement, + SubmissionImportExternalSearchbarComponent, +} from './submission-import-external-searchbar.component'; describe('SubmissionImportExternalSearchbarComponent test suite', () => { let comp: SubmissionImportExternalSearchbarComponent; @@ -42,8 +55,6 @@ describe('SubmissionImportExternalSearchbarComponent test suite', () => { TestBed.configureTestingModule({ imports: [ TranslateModule.forRoot(), - ], - declarations: [ SubmissionImportExternalSearchbarComponent, TestComponent, ], @@ -51,9 +62,9 @@ describe('SubmissionImportExternalSearchbarComponent test suite', () => { { provide: ExternalSourceDataService, useValue: mockExternalSourceService }, ChangeDetectorRef, { provide: HostWindowService, useValue: new HostWindowServiceStub(800) }, - SubmissionImportExternalSearchbarComponent + SubmissionImportExternalSearchbarComponent, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents().then(); })); @@ -91,9 +102,9 @@ describe('SubmissionImportExternalSearchbarComponent test suite', () => { paginatedListRD = createSuccessfulRemoteDataObject(paginatedList); compAsAny.externalService.searchBy.and.returnValue(observableOf(paginatedListRD)); sourceList = [ - {id: 'orcid', name: 'orcid'}, - {id: 'ciencia', name: 'ciencia'}, - {id: 'my_staff_db', name: 'my_staff_db'}, + { id: 'orcid', name: 'orcid' }, + { id: 'ciencia', name: 'ciencia' }, + { id: 'my_staff_db', name: 'my_staff_db' }, ]; }); @@ -124,7 +135,7 @@ describe('SubmissionImportExternalSearchbarComponent test suite', () => { }); it('Variable \'selectedElement\' should be assigned', () => { - const selectedElement = {id: 'orcid', name: 'orcid'}; + const selectedElement = { id: 'orcid', name: 'orcid' }; comp.makeSourceSelection(selectedElement); expect(comp.selectedElement).toEqual(selectedElement); }); @@ -136,14 +147,14 @@ describe('SubmissionImportExternalSearchbarComponent test suite', () => { elementsPerPage: 3, totalElements: 6, totalPages: 2, - currentPage: 0 + currentPage: 0, }); compAsAny.findListOptions = Object.assign({}, new FindListOptions(), { elementsPerPage: 3, currentPage: 0, searchParams: [ - new RequestParam('entityType', 'Publication') - ] + new RequestParam('entityType', 'Publication'), + ], }); comp.sourceList = sourceList; const expected = sourceList.concat(sourceList); @@ -170,7 +181,8 @@ describe('SubmissionImportExternalSearchbarComponent test suite', () => { // declare a test component @Component({ selector: 'ds-test-cmp', - template: `` + 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 522d71cc228..aace4de79a8 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,19 +1,44 @@ -import { ChangeDetectorRef, Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'; - -import { Observable, of as observableOf, Subscription } from 'rxjs'; -import { catchError, tap } from 'rxjs/operators'; +import { CommonModule } from '@angular/common'; +import { + ChangeDetectorRef, + Component, + EventEmitter, + Input, + OnDestroy, + 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, + Subscription, +} from 'rxjs'; +import { + catchError, + tap, +} from 'rxjs/operators'; import { RequestParam } from '../../../core/cache/models/request-param.model'; import { ExternalSourceDataService } from '../../../core/data/external-source-data.service'; -import { ExternalSource } from '../../../core/shared/external-source.model'; -import { buildPaginatedList, PaginatedList } from '../../../core/data/paginated-list.model'; +import { FindListOptions } from '../../../core/data/find-list-options.model'; +import { + buildPaginatedList, + PaginatedList, +} from '../../../core/data/paginated-list.model'; import { RemoteData } from '../../../core/data/remote-data'; +import { ExternalSource } from '../../../core/shared/external-source.model'; +import { + getFirstSucceededRemoteData, + getFirstSucceededRemoteDataPayload, +} from '../../../core/shared/operators'; import { PageInfo } from '../../../core/shared/page-info.model'; -import { createSuccessfulRemoteDataObject } from '../../../shared/remote-data.utils'; -import { getFirstSucceededRemoteData, getFirstSucceededRemoteDataPayload } from '../../../core/shared/operators'; -import { HostWindowService } from '../../../shared/host-window.service'; import { hasValue } from '../../../shared/empty.util'; -import { FindListOptions } from '../../../core/data/find-list-options.model'; +import { HostWindowService } from '../../../shared/host-window.service'; +import { createSuccessfulRemoteDataObject } from '../../../shared/remote-data.utils'; /** * Interface for the selected external source element. @@ -38,7 +63,15 @@ export interface ExternalSourceData { @Component({ selector: 'ds-submission-import-external-searchbar', styleUrls: ['./submission-import-external-searchbar.component.scss'], - templateUrl: './submission-import-external-searchbar.component.html' + templateUrl: './submission-import-external-searchbar.component.html', + imports: [ + CommonModule, + TranslateModule, + InfiniteScrollModule, + NgbDropdownModule, + FormsModule, + ], + standalone: true, }) export class SubmissionImportExternalSearchbarComponent implements OnInit, OnDestroy { /** @@ -93,7 +126,7 @@ export class SubmissionImportExternalSearchbarComponent implements OnInit, OnDes constructor( private externalService: ExternalSourceDataService, private cdr: ChangeDetectorRef, - protected windowService: HostWindowService + protected windowService: HostWindowService, ) { } @@ -103,7 +136,7 @@ export class SubmissionImportExternalSearchbarComponent implements OnInit, OnDes ngOnInit() { this.selectedElement = { id: '', - name: 'loading' + name: 'loading', }; this.searchString = ''; this.sourceList = []; @@ -111,8 +144,8 @@ export class SubmissionImportExternalSearchbarComponent implements OnInit, OnDes elementsPerPage: 5, currentPage: 1, searchParams: [ - new RequestParam('entityType', this.initExternalSourceData.entity) - ] + new RequestParam('entityType', this.initExternalSourceData.entity), + ], }); this.externalService.searchBy('findByEntityType', this.findListOptions).pipe( catchError(() => { @@ -156,8 +189,8 @@ export class SubmissionImportExternalSearchbarComponent implements OnInit, OnDes elementsPerPage: 5, currentPage: this.findListOptions.currentPage + 1, searchParams: [ - new RequestParam('entityType', this.initExternalSourceData.entity) - ] + new RequestParam('entityType', this.initExternalSourceData.entity), + ], }); this.externalService.searchBy('findByEntityType', this.findListOptions).pipe( catchError(() => { @@ -167,7 +200,7 @@ export class SubmissionImportExternalSearchbarComponent implements OnInit, OnDes return observableOf(paginatedListRD); }), getFirstSucceededRemoteData(), - tap(() => this.sourceListLoading = false) + tap(() => this.sourceListLoading = false), ).subscribe((externalSource: RemoteData>) => { externalSource.payload.page.forEach((element) => { this.sourceList.push({ id: element.id, name: element.name }); @@ -186,8 +219,8 @@ export class SubmissionImportExternalSearchbarComponent implements OnInit, OnDes { entity: this.initExternalSourceData.entity, sourceId: this.selectedElement.id, - query: this.searchString - } + query: this.searchString, + }, ); } 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 dc46e6758fd..21783f9a2e6 100644 --- a/src/app/submission/import-external/submission-import-external.component.html +++ b/src/app/submission/import-external/submission-import-external.component.html @@ -1,7 +1,7 @@
- +

{{'submission.import-external.title' + ((label) ? '.' + label : '') | translate}}

@@ -11,8 +11,8 @@ + {{ 'submission.s [importConfig]="importConfig" (importObject)="import($event)"> - -
- {{ 'search.results.empty' | translate }} + +
+ {{ 'search.results.empty' | translate }}
-
- {{ 'search.results.response.500' | translate }} +
+ {{ 'search.results.response.500' | translate }}
- +

{{'submission.import-external.page.hint' | 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 29b75bab446..0364040c51e 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 @@ -1,33 +1,55 @@ -import { Component, NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, inject, TestBed, waitForAsync } from '@angular/core/testing'; - -import { getTestScheduler } from 'jasmine-marbles'; -import { TranslateModule } from '@ngx-translate/core'; -import { Router } from '@angular/router'; +import { + Component, + NO_ERRORS_SCHEMA, +} from '@angular/core'; +import { + ComponentFixture, + inject, + TestBed, + waitForAsync, +} from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { + ActivatedRoute, + Router, +} from '@angular/router'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import { TranslateModule } from '@ngx-translate/core'; +import { getTestScheduler } from 'jasmine-marbles'; import { of as observableOf } from 'rxjs'; import { TestScheduler } from 'rxjs/testing'; -import { SubmissionImportExternalComponent } from './submission-import-external.component'; import { ExternalSourceDataService } from '../../core/data/external-source-data.service'; -import { getMockExternalSourceService } from '../../shared/mocks/external-source.service.mock'; -import { SearchConfigurationService } from '../../core/shared/search/search-configuration.service'; import { RouteService } from '../../core/services/route.service'; -import { createPaginatedList, createTestComponent } from '../../shared/testing/utils.test'; -import { RouterStub } from '../../shared/testing/router.stub'; -import { VarDirective } from '../../shared/utils/var.directive'; -import { routeServiceStub } from '../../shared/testing/route-service.stub'; -import { PaginatedSearchOptions } from '../../shared/search/models/paginated-search-options.model'; +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$, createSuccessfulRemoteDataObject, - createSuccessfulRemoteDataObject$ + createSuccessfulRemoteDataObject$, } from '../../shared/remote-data.utils'; -import { ExternalSourceEntry } from '../../core/shared/external-source-entry.model'; +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 { By } from '@angular/platform-browser'; -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { SubmissionImportExternalSearchbarComponent } from './import-external-searchbar/submission-import-external-searchbar.component'; +import { SubmissionImportExternalComponent } from './submission-import-external.component'; describe('SubmissionImportExternalComponent test suite', () => { let comp: SubmissionImportExternalComponent; @@ -38,12 +60,12 @@ describe('SubmissionImportExternalComponent test suite', () => { const mockSearchOptions = observableOf(new PaginatedSearchOptions({ pagination: Object.assign(new PaginationComponentOptions(), { pageSize: 10, - currentPage: 0 + currentPage: 0, }), - query: 'test' + query: 'test', })); const searchConfigServiceStub = { - paginatedSearchOptions: mockSearchOptions + paginatedSearchOptions: mockSearchOptions, }; const mockExternalSourceService: any = getMockExternalSourceService(); @@ -51,23 +73,35 @@ describe('SubmissionImportExternalComponent test suite', () => { TestBed.configureTestingModule({ imports: [ TranslateModule.forRoot(), - BrowserAnimationsModule - ], - declarations: [ + BrowserAnimationsModule, SubmissionImportExternalComponent, TestComponent, - VarDirective + VarDirective, ], providers: [ { provide: ExternalSourceDataService, useValue: mockExternalSourceService }, { provide: SearchConfigurationService, useValue: searchConfigServiceStub }, { provide: RouteService, useValue: routeServiceStub }, { provide: Router, useValue: new RouterStub() }, + { provide: ActivatedRoute, useValue: new ActivatedRouteStub() }, { provide: NgbModal, useValue: ngbModal }, - SubmissionImportExternalComponent + { provide: HostWindowService, useValue: new HostWindowServiceStub(800) }, + { provide: ThemeService, useValue: getMockThemeService() }, + SubmissionImportExternalComponent, ], - schemas: [NO_ERRORS_SCHEMA] - }).compileComponents().then(); + schemas: [NO_ERRORS_SCHEMA], + }) + .overrideComponent(SubmissionImportExternalComponent, { + remove: { + imports: [ + ObjectCollectionComponent, + ThemedLoadingComponent, + AlertComponent, + SubmissionImportExternalSearchbarComponent, + ], + }, + }) + .compileComponents().then(); })); // First test to check the correct component creation @@ -109,17 +143,17 @@ describe('SubmissionImportExternalComponent test suite', () => { it('Should init component properly (without route data)', () => { const expectedEntries = createSuccessfulRemoteDataObject(createPaginatedList([])); - comp.routeData = {entity: '', sourceId: '', query: '' }; + comp.routeData = { entity: '', sourceId: '', query: '' }; spyOn(compAsAny.routeService, 'getQueryParameterValue').and.returnValue(observableOf('')); fixture.detectChanges(); - expect(comp.routeData).toEqual({entity: '', sourceId: '', query: '' }); + expect(comp.routeData).toEqual({ entity: '', sourceId: '', query: '' }); expect(comp.isLoading$.value).toBe(false); expect(comp.entriesRD$.value).toEqual(expectedEntries); }); it('Should init component properly (with route data)', () => { - comp.routeData = {entity: '', sourceId: '', query: '' }; + comp.routeData = { entity: '', sourceId: '', query: '' }; spyOn(compAsAny, 'retrieveExternalSources'); spyOn(compAsAny.routeService, 'getQueryParameterValue').and.returnValues(observableOf('entity'), observableOf('source'), observableOf('dummy')); fixture.detectChanges(); @@ -145,10 +179,10 @@ describe('SubmissionImportExternalComponent test suite', () => { }); it('Should call \'router.navigate\'', () => { - comp.routeData = {entity: 'Person', sourceId: '', query: '' }; + comp.routeData = { entity: 'Person', sourceId: '', query: '' }; spyOn(compAsAny, 'retrieveExternalSources').and.callFake(() => null); compAsAny.router.navigate.and.returnValue( new Promise(() => {return;})); - const event = {entity: 'Person', sourceId: 'orcidV2', query: 'dummy' }; + const event = { entity: 'Person', sourceId: 'orcidV2', query: 'dummy' }; scheduler.schedule(() => comp.getExternalSourceData(event)); scheduler.flush(); @@ -164,12 +198,12 @@ describe('SubmissionImportExternalComponent test suite', () => { metadata: { 'dc.identifier.uri': [ { - value: 'https://orcid.org/0001-0001-0001-0001' - } - ] - } + value: 'https://orcid.org/0001-0001-0001-0001', + }, + ], + }, }); - ngbModal.open.and.returnValue({componentInstance: { externalSourceEntry: null}}); + ngbModal.open.and.returnValue({ componentInstance: { externalSourceEntry: null } }); comp.import(entry); expect(compAsAny.modalService.open).toHaveBeenCalledWith(SubmissionImportExternalPreviewComponent, { size: 'lg' }); @@ -193,32 +227,32 @@ describe('SubmissionImportExternalComponent test suite', () => { 'errorMessage': null, 'payload': { 'type': { - 'value': 'paginated-list' + 'value': 'paginated-list', }, 'pageInfo': { 'elementsPerPage': 10, 'totalElements': 11971608, 'totalPages': 1197161, - 'currentPage': 1 + 'currentPage': 1, }, '_links': { 'first': { - 'href': 'https://example.com/server/api/integration/externalsources/scopus/entries?query=test&page=0&size=10&sort=id,asc' + 'href': 'https://example.com/server/api/integration/externalsources/scopus/entries?query=test&page=0&size=10&sort=id,asc', }, 'self': { - 'href': 'https://example.com/server/api/integration/externalsources/scopus/entries?sort=id,ASC&page=0&size=10&query=test' + 'href': 'https://example.com/server/api/integration/externalsources/scopus/entries?sort=id,ASC&page=0&size=10&query=test', }, 'next': { - 'href': 'https://example.com/server/api/integration/externalsources/scopus/entries?query=test&page=1&size=10&sort=id,asc' + 'href': 'https://example.com/server/api/integration/externalsources/scopus/entries?query=test&page=1&size=10&sort=id,asc', }, 'last': { - 'href': 'https://example.com/server/api/integration/externalsources/scopus/entries?query=test&page=1197160&size=10&sort=id,asc' + 'href': 'https://example.com/server/api/integration/externalsources/scopus/entries?query=test&page=1197160&size=10&sort=id,asc', }, 'page': [ { - 'href': 'https://example.com/server/api/integration/externalsources/scopus/entryValues/2-s2.0-85130258665' - } - ] + 'href': 'https://example.com/server/api/integration/externalsources/scopus/entryValues/2-s2.0-85130258665', + }, + ], }, 'page': [ { @@ -235,8 +269,8 @@ describe('SubmissionImportExternalComponent test suite', () => { 'value': 'Silva I.M.M.', 'place': -1, 'authority': null, - 'confidence': -1 - } + 'confidence': -1, + }, ], 'dc.date.issued': [ { @@ -245,8 +279,8 @@ describe('SubmissionImportExternalComponent test suite', () => { 'value': '2024-01-01', 'place': -1, 'authority': null, - 'confidence': -1 - } + 'confidence': -1, + }, ], 'dc.description.abstract': [ { @@ -255,8 +289,8 @@ describe('SubmissionImportExternalComponent test suite', () => { 'value': 'This systematic review integrates the data available in the literature regarding the biological activities of the extracts of endophytic fungi isolated from Annona muricata and their secondary metabolites. The search was performed using four electronic databases, and studies’ quality was evaluated using an adapted assessment tool. The initial database search yielded 436 results; ten studies were selected for inclusion. The leaf was the most studied part of the plant (in nine studies); Periconia sp. was the most tested fungus (n = 4); the most evaluated biological activity was anticancer (n = 6), followed by antiviral (n = 3). Antibacterial, antifungal, and antioxidant activities were also tested. Terpenoids or terpenoid hybrid compounds were the most abundant chemical metabolites. Phenolic compounds, esters, alkaloids, saturated and unsaturated fatty acids, aromatic compounds, and peptides were also reported. The selected studies highlighted the biotechnological potentiality of the endophytic fungi extracts from A. muricata. Consequently, it can be considered a promising source of biological compounds with antioxidant effects and active against different microorganisms and cancer cells. Further research is needed involving different plant tissues, other microorganisms, such as SARS-CoV-2, and different cancer cells.', 'place': -1, 'authority': null, - 'confidence': -1 - } + 'confidence': -1, + }, ], 'dc.identifier.doi': [ { @@ -265,8 +299,8 @@ describe('SubmissionImportExternalComponent test suite', () => { 'value': '10.1590/1519-6984.259525', 'place': -1, 'authority': null, - 'confidence': -1 - } + 'confidence': -1, + }, ], 'dc.identifier.pmid': [ { @@ -275,8 +309,8 @@ describe('SubmissionImportExternalComponent test suite', () => { 'value': '35588520', 'place': -1, 'authority': null, - 'confidence': -1 - } + 'confidence': -1, + }, ], 'dc.identifier.scopus': [ { @@ -285,8 +319,8 @@ describe('SubmissionImportExternalComponent test suite', () => { 'value': '2-s2.0-85130258665', 'place': -1, 'authority': null, - 'confidence': -1 - } + 'confidence': -1, + }, ], 'dc.relation.grantno': [ { @@ -295,8 +329,8 @@ describe('SubmissionImportExternalComponent test suite', () => { 'value': 'undefined', 'place': -1, 'authority': null, - 'confidence': -1 - } + 'confidence': -1, + }, ], 'dc.relation.ispartof': [ { @@ -305,8 +339,8 @@ describe('SubmissionImportExternalComponent test suite', () => { 'value': 'Brazilian Journal of Biology', 'place': -1, 'authority': null, - 'confidence': -1 - } + 'confidence': -1, + }, ], 'dc.relation.ispartofseries': [ { @@ -315,8 +349,8 @@ describe('SubmissionImportExternalComponent test suite', () => { 'value': 'Brazilian Journal of Biology', 'place': -1, 'authority': null, - 'confidence': -1 - } + 'confidence': -1, + }, ], 'dc.relation.issn': [ { @@ -325,8 +359,8 @@ describe('SubmissionImportExternalComponent test suite', () => { 'value': '15196984', 'place': -1, 'authority': null, - 'confidence': -1 - } + 'confidence': -1, + }, ], 'dc.subject': [ { @@ -335,8 +369,8 @@ describe('SubmissionImportExternalComponent test suite', () => { 'value': 'biological products | biotechnology | mycology | soursop', 'place': -1, 'authority': null, - 'confidence': -1 - } + 'confidence': -1, + }, ], 'dc.title': [ { @@ -345,8 +379,8 @@ describe('SubmissionImportExternalComponent test suite', () => { 'value': 'Biological activities of endophytic fungi isolated from Annona muricata Linnaeus: a systematic review', 'place': -1, 'authority': null, - 'confidence': -1 - } + 'confidence': -1, + }, ], 'dc.type': [ { @@ -355,8 +389,8 @@ describe('SubmissionImportExternalComponent test suite', () => { 'value': 'Journal', 'place': -1, 'authority': null, - 'confidence': -1 - } + 'confidence': -1, + }, ], 'oaire.citation.volume': [ { @@ -365,8 +399,8 @@ describe('SubmissionImportExternalComponent test suite', () => { 'value': '84', 'place': -1, 'authority': null, - 'confidence': -1 - } + 'confidence': -1, + }, ], 'oairecerif.affiliation.orgunit': [ { @@ -375,8 +409,8 @@ describe('SubmissionImportExternalComponent test suite', () => { 'value': 'Universidade Federal do Reconcavo da Bahia', 'place': -1, 'authority': null, - 'confidence': -1 - } + 'confidence': -1, + }, ], 'oairecerif.citation.number': [ { @@ -385,8 +419,8 @@ describe('SubmissionImportExternalComponent test suite', () => { 'value': 'e259525', 'place': -1, 'authority': null, - 'confidence': -1 - } + 'confidence': -1, + }, ], 'person.identifier.orcid': [ { @@ -395,8 +429,8 @@ describe('SubmissionImportExternalComponent test suite', () => { 'value': '#PLACEHOLDER_PARENT_METADATA_VALUE#', 'place': -1, 'authority': null, - 'confidence': -1 - } + 'confidence': -1, + }, ], 'person.identifier.scopus-author-id': [ { @@ -405,19 +439,19 @@ describe('SubmissionImportExternalComponent test suite', () => { 'value': '42561627000', 'place': -1, 'authority': null, - 'confidence': -1 - } - ] + 'confidence': -1, + }, + ], }, '_links': { 'self': { - 'href': 'https://example.com/server/api/integration/externalsources/scopus/entryValues/2-s2.0-85130258665' - } - } - } - ] + 'href': 'https://example.com/server/api/integration/externalsources/scopus/entryValues/2-s2.0-85130258665', + }, + }, + }, + ], }, - 'statusCode': 200 + 'statusCode': 200, }; const errorObj = { errorMessage: 'Http failure response for ' + @@ -425,8 +459,8 @@ describe('SubmissionImportExternalComponent test suite', () => { statusCode: 500, timeCompleted: 1656950434666, errors: [{ - 'message': 'Internal Server Error', 'paths': ['/server/api/integration/externalsources/pubmed/entries'] - }] + 'message': 'Internal Server Error', 'paths': ['/server/api/integration/externalsources/pubmed/entries'], + }], }; beforeEach(() => { fixture = TestBed.createComponent(SubmissionImportExternalComponent); @@ -483,7 +517,7 @@ describe('SubmissionImportExternalComponent test suite', () => { mockExternalSourceService.getExternalSourceEntries.and.returnValue(createFailedRemoteDataObject$( errorObj.errorMessage, errorObj.statusCode, - errorObj.timeCompleted + errorObj.timeCompleted, )); spyOn(routeServiceStub, 'getQueryParameterValue').and.callFake((param) => { if (param === 'entity') { @@ -509,7 +543,8 @@ describe('SubmissionImportExternalComponent test suite', () => { // declare a test component @Component({ selector: 'ds-test-cmp', - template: `` + 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 25b1d5d1aa2..47b5c09d66c 100644 --- a/src/app/submission/import-external/submission-import-external.component.ts +++ b/src/app/submission/import-external/submission-import-external.component.ts @@ -1,37 +1,85 @@ -import { Component, OnDestroy, OnInit } from '@angular/core'; -import { Router } from '@angular/router'; - -import { BehaviorSubject, combineLatest, Subscription } from 'rxjs'; -import { filter, mergeMap, switchMap, take, tap } from 'rxjs/operators'; -import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; +import { + AsyncPipe, + NgIf, +} from '@angular/common'; +import { + Component, + OnDestroy, + OnInit, +} from '@angular/core'; +import { + Router, + RouterLink, +} from '@angular/router'; +import { + NgbModal, + NgbModalRef, +} from '@ng-bootstrap/ng-bootstrap'; +import { TranslateModule } from '@ngx-translate/core'; +import { + BehaviorSubject, + combineLatest, + Subscription, +} from 'rxjs'; +import { + filter, + mergeMap, + switchMap, + take, + tap, +} from 'rxjs/operators'; +import { AlertType } from 'src/app/shared/alert/alert-type'; import { ExternalSourceDataService } from '../../core/data/external-source-data.service'; -import { ExternalSourceData } from './import-external-searchbar/submission-import-external-searchbar.component'; +import { + buildPaginatedList, + PaginatedList, +} from '../../core/data/paginated-list.model'; import { RemoteData } from '../../core/data/remote-data'; -import { buildPaginatedList, PaginatedList } from '../../core/data/paginated-list.model'; +import { RouteService } from '../../core/services/route.service'; +import { Context } from '../../core/shared/context.model'; import { ExternalSourceEntry } from '../../core/shared/external-source-entry.model'; +import { NONE_ENTITY_TYPE } from '../../core/shared/item-relationships/item-type.resource-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 { Context } from '../../core/shared/context.model'; +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 { RouteService } from '../../core/services/route.service'; 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 { - SubmissionImportExternalPreviewComponent -} from './import-external-preview/submission-import-external-preview.component'; -import { fadeIn } from '../../shared/animations/fade'; -import { PageInfo } from '../../core/shared/page-info.model'; -import { hasValue, isNotEmpty } from '../../shared/empty.util'; -import { getFinishedRemoteData } from '../../core/shared/operators'; -import { NONE_ENTITY_TYPE } from '../../core/shared/item-relationships/item-type.resource-type'; + 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] + animations: [fadeIn], + imports: [ + ObjectCollectionComponent, + ThemedLoadingComponent, + AlertComponent, + NgIf, + AsyncPipe, + SubmissionImportExternalSearchbarComponent, + TranslateModule, + VarDirective, + RouterLink, + ], + standalone: true, }) export class SubmissionImportExternalComponent implements OnInit, OnDestroy { @@ -51,7 +99,7 @@ export class SubmissionImportExternalComponent implements OnInit, OnDestroy { public reload$: BehaviorSubject = new BehaviorSubject({ entity: '', query: '', - sourceId: '' + sourceId: '', }); /** * Configuration to use for the import buttons @@ -74,7 +122,7 @@ export class SubmissionImportExternalComponent implements OnInit, OnDestroy { */ public initialPagination = Object.assign(new PaginationComponentOptions(), { id: 'spc', - pageSize: 10 + pageSize: 10, }); /** * The context to displaying lists for @@ -92,6 +140,8 @@ export class SubmissionImportExternalComponent implements OnInit, OnDestroy { private retrieveExternalSourcesSub: Subscription; + public readonly AlertType = AlertType; + /** * Initialize the component variables. * @param {SearchConfigurationService} searchConfigService @@ -116,9 +166,9 @@ export class SubmissionImportExternalComponent implements OnInit, OnDestroy { this.listId = 'list-submission-external-sources'; this.context = Context.EntitySearchModalWithNameVariants; this.repeatable = false; - this.routeData = {entity: '', sourceId: '', query: ''}; + this.routeData = { entity: '', sourceId: '', query: '' }; this.importConfig = { - buttonLabel: 'submission.sections.describe.relationship-lookup.external-source.import-button-title.' + this.label + buttonLabel: 'submission.sections.describe.relationship-lookup.external-source.import-button-title.' + this.label, }; this.entriesRD$ = new BehaviorSubject(createSuccessfulRemoteDataObject(buildPaginatedList(new PageInfo(), []))); this.isLoading$ = new BehaviorSubject(false); @@ -126,11 +176,11 @@ export class SubmissionImportExternalComponent implements OnInit, OnDestroy { [ this.routeService.getQueryParameterValue('entity'), this.routeService.getQueryParameterValue('sourceId'), - this.routeService.getQueryParameterValue('query') + this.routeService.getQueryParameterValue('query'), ]).pipe( - take(1) + take(1), ).subscribe(([entity, sourceId, query]: [string, string, string]) => { - this.reload$.next({entity: entity || NONE_ENTITY_TYPE, query: query, sourceId: sourceId}); + this.reload$.next({ entity: entity || NONE_ENTITY_TYPE, query: query, sourceId: sourceId }); this.selectLabel(entity); this.retrieveExternalSources(); })); @@ -144,8 +194,8 @@ export class SubmissionImportExternalComponent implements OnInit, OnDestroy { [], { queryParams: event, - replaceUrl: true - } + replaceUrl: true, + }, ).then(() => { this.reload$.next(event); this.retrieveExternalSources(); @@ -188,16 +238,16 @@ export class SubmissionImportExternalComponent implements OnInit, OnDestroy { this.retrieveExternalSourcesSub = this.reload$.pipe( filter((sourceQueryObject: ExternalSourceData) => isNotEmpty(sourceQueryObject.sourceId) && isNotEmpty(sourceQueryObject.query)), switchMap((sourceQueryObject: ExternalSourceData) => { - const query = sourceQueryObject.query; - this.routeData = sourceQueryObject; - return this.searchConfigService.paginatedSearchOptions.pipe( - tap(() => this.isLoading$.next(true)), - filter((searchOptions) => searchOptions.query === query), - mergeMap((searchOptions) => this.externalService.getExternalSourceEntries(this.routeData.sourceId, searchOptions).pipe( - getFinishedRemoteData(), - )) - ); - } + const query = sourceQueryObject.query; + this.routeData = sourceQueryObject; + return this.searchConfigService.paginatedSearchOptions.pipe( + tap(() => this.isLoading$.next(true)), + filter((searchOptions) => searchOptions.query === query), + mergeMap((searchOptions) => this.externalService.getExternalSourceEntries(this.routeData.sourceId, searchOptions).pipe( + getFinishedRemoteData(), + )), + ); + }, ), ).subscribe((rdData) => { this.entriesRD$.next(rdData); @@ -213,7 +263,7 @@ export class SubmissionImportExternalComponent implements OnInit, OnDestroy { private selectLabel(entity: string): void { this.label = entity; this.importConfig = { - buttonLabel: 'submission.sections.describe.relationship-lookup.external-source.import-button-title.' + this.label + buttonLabel: 'submission.sections.describe.relationship-lookup.external-source.import-button-title.' + this.label, }; } 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 698a64842d1..bd7242293d9 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 @@ -1,4 +1,5 @@ import { Component } from '@angular/core'; + import { ThemedComponent } from '../../shared/theme-support/themed.component'; import { SubmissionImportExternalComponent } from './submission-import-external.component'; @@ -6,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' + 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/submission-objects.actions.ts b/src/app/submission/objects/submission-objects.actions.ts index 9182611e479..e075ce5e5ba 100644 --- a/src/app/submission/objects/submission-objects.actions.ts +++ b/src/app/submission/objects/submission-objects.actions.ts @@ -1,16 +1,16 @@ /* eslint-disable max-classes-per-file */ import { Action } from '@ngrx/store'; -import { type } from '../../shared/ngrx/type'; +import { SubmissionDefinitionsModel } from '../../core/config/models/config-submission-definitions.model'; +import { Item } from '../../core/shared/item.model'; +import { SubmissionObject } from '../../core/submission/models/submission-object.model'; import { WorkspaceitemSectionUploadFileObject } from '../../core/submission/models/workspaceitem-section-upload-file.model'; import { WorkspaceitemSectionDataType, - WorkspaceitemSectionsObject + WorkspaceitemSectionsObject, } from '../../core/submission/models/workspaceitem-sections.model'; -import { SubmissionObject } from '../../core/submission/models/submission-object.model'; -import { SubmissionDefinitionsModel } from '../../core/config/models/config-submission-definitions.model'; +import { type } from '../../shared/ngrx/type'; import { SectionsType } from '../sections/sections-type'; -import { Item } from '../../core/shared/item.model'; import { SectionVisibility } from './section-visibility.model'; import { SubmissionError } from './submission-error.model'; import { SubmissionSectionError } from './submission-section-error.model'; @@ -56,9 +56,13 @@ export const SubmissionObjectActionTypes = { DISCARD_SUBMISSION_SUCCESS: type('dspace/submission/DISCARD_SUBMISSION_SUCCESS'), DISCARD_SUBMISSION_ERROR: type('dspace/submission/DISCARD_SUBMISSION_ERROR'), + // Clearing active section types + CLEAN_DUPLICATE_DETECTION: type('dspace/submission/CLEAN_DUPLICATE_DETECTION'), + // Upload file types NEW_FILE: type('dspace/submission/NEW_FILE'), EDIT_FILE_DATA: type('dspace/submission/EDIT_FILE_DATA'), + EDIT_FILE_PRIMARY_BITSTREAM_DATA: type('dspace/submission/EDIT_FILE_PRIMARY_BITSTREAM_DATA'), DELETE_FILE: type('dspace/submission/DELETE_FILE'), // Errors @@ -148,15 +152,15 @@ export class InitSectionAction implements Action { * the section's errors */ constructor(submissionId: string, - sectionId: string, - header: string, - config: string, - mandatory: boolean, - sectionType: SectionsType, - visibility: SectionVisibility, - enabled: boolean, - data: WorkspaceitemSectionDataType, - errors: SubmissionSectionError[]) { + sectionId: string, + header: string, + config: string, + mandatory: boolean, + sectionType: SectionsType, + visibility: SectionVisibility, + enabled: boolean, + data: WorkspaceitemSectionDataType, + errors: SubmissionSectionError[]) { this.payload = { submissionId, sectionId, header, config, mandatory, sectionType, visibility, enabled, data, errors }; } } @@ -177,7 +181,7 @@ export class EnableSectionAction implements Action { * the section's ID to add */ constructor(submissionId: string, - sectionId: string) { + sectionId: string) { this.payload = { submissionId, sectionId }; } } @@ -230,15 +234,34 @@ export class UpdateSectionDataAction implements Action { * the section's metadata */ constructor(submissionId: string, - sectionId: string, - data: WorkspaceitemSectionDataType, - errorsToShow: SubmissionSectionError[], - serverValidationErrors: SubmissionSectionError[], - metadata?: string[]) { + sectionId: string, + data: WorkspaceitemSectionDataType, + errorsToShow: SubmissionSectionError[], + serverValidationErrors: SubmissionSectionError[], + metadata?: string[]) { this.payload = { submissionId, sectionId, data, errorsToShow, serverValidationErrors, metadata }; } } +/** + * Removes data and makes 'detect-duplicate' section not visible. + */ +export class CleanDuplicateDetectionAction implements Action { + type = SubmissionObjectActionTypes.CLEAN_DUPLICATE_DETECTION; + payload: { + submissionId: string; + }; + + /** + * creates a new CleanDetectDuplicateAction + * + * @param submissionId Id of the submission on which perform the action + */ + constructor(submissionId: string ) { + this.payload = { submissionId }; + } +} + export class UpdateSectionDataSuccessAction implements Action { type = SubmissionObjectActionTypes.UPDATE_SECTION_DATA_SUCCESS; } @@ -334,12 +357,12 @@ export class InitSubmissionFormAction implements Action { * the submission's sections errors */ constructor(collectionId: string, - submissionId: string, - selfUrl: string, - submissionDefinition: SubmissionDefinitionsModel, - sections: WorkspaceitemSectionsObject, - item: Item, - errors: SubmissionError) { + submissionId: string, + selfUrl: string, + submissionDefinition: SubmissionDefinitionsModel, + sections: WorkspaceitemSectionsObject, + item: Item, + errors: SubmissionError) { this.payload = { collectionId, submissionId, selfUrl, submissionDefinition, sections, item, errors }; } } @@ -760,6 +783,29 @@ export class NewUploadedFileAction implements Action { } } +export class EditFilePrimaryBitstreamAction implements Action { + type = SubmissionObjectActionTypes.EDIT_FILE_PRIMARY_BITSTREAM_DATA; + payload: { + submissionId: string; + sectionId: string; + fileId: string | null; + }; + + /** + * Edit a file data + * + * @param submissionId + * the submission's ID + * @param sectionId + * the section's ID + * @param fileId + * the file's ID + */ + constructor(submissionId: string, sectionId: string, fileId: string | null) { + this.payload = { submissionId, sectionId, fileId: fileId }; + } +} + export class EditFileDataAction implements Action { type = SubmissionObjectActionTypes.EDIT_FILE_DATA; payload: { @@ -821,6 +867,7 @@ export type SubmissionObjectAction = DisableSectionAction | InitSubmissionFormAction | ResetSubmissionFormAction | CancelSubmissionFormAction + | CleanDuplicateDetectionAction | CompleteInitSubmissionFormAction | ChangeSubmissionCollectionAction | SaveAndDepositSubmissionAction @@ -833,6 +880,7 @@ export type SubmissionObjectAction = DisableSectionAction | SectionStatusChangeAction | NewUploadedFileAction | EditFileDataAction + | EditFilePrimaryBitstreamAction | DeleteUploadedFileAction | InertSectionErrorsAction | DeleteSectionErrorsAction diff --git a/src/app/submission/objects/submission-objects.effects.spec.ts b/src/app/submission/objects/submission-objects.effects.spec.ts index a1bb878aa59..4a3e08384d4 100644 --- a/src/app/submission/objects/submission-objects.effects.spec.ts +++ b/src/app/submission/objects/submission-objects.effects.spec.ts @@ -1,29 +1,35 @@ import { TestBed } from '@angular/core/testing'; - -import { cold, hot } from 'jasmine-marbles'; import { provideMockActions } from '@ngrx/effects/testing'; -import { Store, StoreModule } from '@ngrx/store'; -import { Observable, of as observableOf, throwError as observableThrowError } from 'rxjs'; -import { TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core'; +import { + Store, + StoreModule, +} from '@ngrx/store'; +import { + TranslateLoader, + TranslateModule, + TranslateService, +} from '@ngx-translate/core'; +import { + cold, + hot, +} from 'jasmine-marbles'; +import { + Observable, + of as observableOf, + throwError as observableThrowError, +} from 'rxjs'; -import { SubmissionObjectEffects } from './submission-objects.effects'; import { - CompleteInitSubmissionFormAction, - DepositSubmissionAction, - DepositSubmissionErrorAction, - DepositSubmissionSuccessAction, - DiscardSubmissionErrorAction, - DiscardSubmissionSuccessAction, - InitSectionAction, - InitSubmissionFormAction, - SaveForLaterSubmissionFormSuccessAction, - SaveSubmissionFormErrorAction, - SaveSubmissionFormSuccessAction, - SaveSubmissionSectionFormErrorAction, - SaveSubmissionSectionFormSuccessAction, - SubmissionObjectActionTypes, - UpdateSectionDataAction -} from './submission-objects.actions'; + AppState, + storeModuleConfig, +} from '../../app.reducer'; +import { SubmissionSectionModel } from '../../core/config/models/config-submission-section.model'; +import { HALEndpointService } from '../../core/shared/hal-endpoint.service'; +import { Item } from '../../core/shared/item.model'; +import { SubmissionJsonPatchOperationsService } from '../../core/submission/submission-json-patch-operations.service'; +import { SubmissionObjectDataService } from '../../core/submission/submission-object-data.service'; +import { WorkflowItemDataService } from '../../core/submission/workflowitem-data.service'; +import { WorkspaceitemDataService } from '../../core/submission/workspaceitem-data.service'; import { mockSectionsData, mockSectionsDataTwo, @@ -35,27 +41,37 @@ import { mockSubmissionId, mockSubmissionRestResponse, mockSubmissionSelfUrl, - mockSubmissionState + mockSubmissionState, } from '../../shared/mocks/submission.mock'; -import { SubmissionSectionModel } from '../../core/config/models/config-submission-section.model'; -import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub'; +import { TranslateLoaderMock } from '../../shared/mocks/translate-loader.mock'; import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub'; +import { SectionsServiceStub } from '../../shared/testing/sections-service.stub'; +import { StoreMock } from '../../shared/testing/store.mock'; import { SubmissionJsonPatchOperationsServiceStub } from '../../shared/testing/submission-json-patch-operations-service.stub'; -import { SubmissionJsonPatchOperationsService } from '../../core/submission/submission-json-patch-operations.service'; +import { mockSubmissionObjectDataService } from '../../shared/testing/submission-oject-data-service.mock'; +import { SubmissionServiceStub } from '../../shared/testing/submission-service.stub'; import { SectionsService } from '../sections/sections.service'; -import { SectionsServiceStub } from '../../shared/testing/sections-service.stub'; import { SubmissionService } from '../submission.service'; -import { SubmissionServiceStub } from '../../shared/testing/submission-service.stub'; -import { TranslateLoaderMock } from '../../shared/mocks/translate-loader.mock'; -import { StoreMock } from '../../shared/testing/store.mock'; -import { AppState, storeModuleConfig } from '../../app.reducer'; import parseSectionErrors from '../utils/parseSectionErrors'; -import { Item } from '../../core/shared/item.model'; -import { WorkspaceitemDataService } from '../../core/submission/workspaceitem-data.service'; -import { WorkflowItemDataService } from '../../core/submission/workflowitem-data.service'; -import { HALEndpointService } from '../../core/shared/hal-endpoint.service'; -import { SubmissionObjectDataService } from '../../core/submission/submission-object-data.service'; -import { mockSubmissionObjectDataService } from '../../shared/testing/submission-oject-data-service.mock'; +import { + CompleteInitSubmissionFormAction, + DepositSubmissionAction, + DepositSubmissionErrorAction, + DepositSubmissionSuccessAction, + DiscardSubmissionErrorAction, + DiscardSubmissionSuccessAction, + InitSectionAction, + InitSubmissionFormAction, + SaveForLaterSubmissionFormSuccessAction, + SaveSubmissionFormErrorAction, + SaveSubmissionFormSuccessAction, + SaveSubmissionSectionFormErrorAction, + SaveSubmissionSectionFormSuccessAction, + SubmissionObjectActionTypes, + UpdateSectionDataAction, +} from './submission-objects.actions'; +import { SubmissionObjectEffects } from './submission-objects.effects'; describe('SubmissionObjectEffects test suite', () => { let submissionObjectEffects: SubmissionObjectEffects; @@ -66,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; @@ -82,14 +100,18 @@ describe('SubmissionObjectEffects test suite', () => { submissionServiceStub.hasUnsavedModification.and.returnValue(observableOf(true)); + workspaceItemDataService = jasmine.createSpyObj('WorkspaceItemDataService', { + invalidateById: observableOf(true), + }); + TestBed.configureTestingModule({ imports: [ StoreModule.forRoot({}, storeModuleConfig), TranslateModule.forRoot({ loader: { provide: TranslateLoader, - useClass: TranslateLoaderMock - } + useClass: TranslateLoaderMock, + }, }), ], providers: [ @@ -106,6 +128,7 @@ describe('SubmissionObjectEffects test suite', () => { { provide: WorkflowItemDataService, useValue: {} }, { provide: HALEndpointService, useValue: {} }, { provide: SubmissionObjectDataService, useValue: submissionObjectDataServiceStub }, + { provide: WorkspaceitemDataService, useValue: workspaceItemDataService }, ], }); @@ -124,10 +147,10 @@ describe('SubmissionObjectEffects test suite', () => { selfUrl: selfUrl, submissionDefinition: submissionDefinition, sections: {}, - item: {metadata: {}}, + item: { metadata: {} }, errors: [], - } - } + }, + }, }); const mappedActions = []; @@ -159,7 +182,7 @@ describe('SubmissionObjectEffects test suite', () => { e: mappedActions[3], f: mappedActions[4], g: mappedActions[5], - h: mappedActions[6] + h: mappedActions[6], }); expect(submissionObjectEffects.loadForm$).toBeObservable(expected); @@ -179,8 +202,8 @@ describe('SubmissionObjectEffects test suite', () => { sections: {}, item: new Item(), errors: [], - } - } + }, + }, }); const expected = cold('--b-', { @@ -191,8 +214,8 @@ describe('SubmissionObjectEffects test suite', () => { submissionDefinition, {}, new Item(), - null - ) + null, + ), }); expect(submissionObjectEffects.resetForm$).toBeObservable(expected); @@ -205,17 +228,17 @@ describe('SubmissionObjectEffects test suite', () => { a: { type: SubmissionObjectActionTypes.SAVE_SUBMISSION_FORM, payload: { - submissionId: submissionId - } - } + submissionId: submissionId, + }, + }, }); submissionJsonPatchOperationsServiceStub.jsonPatchByResourceType.and.returnValue(observableOf(mockSubmissionRestResponse)); const expected = cold('--b-', { b: new SaveSubmissionFormSuccessAction( submissionId, - mockSubmissionRestResponse as any - ) + mockSubmissionRestResponse as any, + ), }); expect(submissionObjectEffects.saveSubmission$).toBeObservable(expected); @@ -227,9 +250,9 @@ describe('SubmissionObjectEffects test suite', () => { type: SubmissionObjectActionTypes.SAVE_SUBMISSION_FORM, payload: { submissionId: submissionId, - isManual: true - } - } + isManual: true, + }, + }, }); submissionJsonPatchOperationsServiceStub.jsonPatchByResourceType.and.returnValue(observableOf(mockSubmissionRestResponse)); @@ -238,8 +261,8 @@ describe('SubmissionObjectEffects test suite', () => { submissionId, mockSubmissionRestResponse as any, true, - true - ) + true, + ), }); expect(submissionObjectEffects.saveSubmission$).toBeObservable(expected); @@ -251,9 +274,9 @@ describe('SubmissionObjectEffects test suite', () => { type: SubmissionObjectActionTypes.SAVE_SUBMISSION_FORM, payload: { submissionId: submissionId, - isManual: false - } - } + isManual: false, + }, + }, }); submissionJsonPatchOperationsServiceStub.jsonPatchByResourceType.and.returnValue(observableOf(mockSubmissionRestResponse)); @@ -262,8 +285,8 @@ describe('SubmissionObjectEffects test suite', () => { submissionId, mockSubmissionRestResponse as any, false, - false - ) + false, + ), }); expect(submissionObjectEffects.saveSubmission$).toBeObservable(expected); @@ -274,18 +297,18 @@ describe('SubmissionObjectEffects test suite', () => { a: { type: SubmissionObjectActionTypes.SAVE_SUBMISSION_FORM, payload: { - submissionId: submissionId - } - } + submissionId: submissionId, + }, + }, }); submissionJsonPatchOperationsServiceStub.jsonPatchByResourceType.and.callFake( - () => observableThrowError('Error') + () => observableThrowError('Error'), ); const expected = cold('--b-', { b: new SaveSubmissionFormErrorAction( - submissionId - ) + submissionId, + ), }); expect(submissionObjectEffects.saveSubmission$).toBeObservable(expected); @@ -298,17 +321,17 @@ describe('SubmissionObjectEffects test suite', () => { a: { type: SubmissionObjectActionTypes.SAVE_FOR_LATER_SUBMISSION_FORM, payload: { - submissionId: submissionId - } - } + submissionId: submissionId, + }, + }, }); submissionJsonPatchOperationsServiceStub.jsonPatchByResourceType.and.returnValue(observableOf(mockSubmissionRestResponse)); const expected = cold('--b-', { b: new SaveForLaterSubmissionFormSuccessAction( submissionId, - mockSubmissionRestResponse as any - ) + mockSubmissionRestResponse as any, + ), }); expect(submissionObjectEffects.saveForLaterSubmission$).toBeObservable(expected); @@ -319,18 +342,18 @@ describe('SubmissionObjectEffects test suite', () => { a: { type: SubmissionObjectActionTypes.SAVE_FOR_LATER_SUBMISSION_FORM, payload: { - submissionId: submissionId - } - } + submissionId: submissionId, + }, + }, }); submissionJsonPatchOperationsServiceStub.jsonPatchByResourceType.and.callFake( - () => observableThrowError('Error') + () => observableThrowError('Error'), ); const expected = cold('--b-', { b: new SaveSubmissionFormErrorAction( - submissionId - ) + submissionId, + ), }); expect(submissionObjectEffects.saveForLaterSubmission$).toBeObservable(expected); @@ -342,13 +365,13 @@ describe('SubmissionObjectEffects test suite', () => { it('should return a UPDATE_SECTION_DATA action for each updated section', () => { store.nextState({ submission: { - objects: submissionState - } + objects: submissionState, + }, } as any); const response = [Object.assign({}, mockSubmissionRestResponse[0], { sections: mockSectionsData, - errors: mockSectionsErrors + errors: mockSectionsErrors, })]; actions = hot('--a-', { a: { @@ -356,9 +379,9 @@ describe('SubmissionObjectEffects test suite', () => { payload: { submissionId: submissionId, submissionObject: response, - notify: true - } - } + notify: true, + }, + }, }); const errorsList = parseSectionErrors(mockSectionsErrors); @@ -368,21 +391,21 @@ describe('SubmissionObjectEffects test suite', () => { 'traditionalpageone', mockSectionsData.traditionalpageone as any, errorsList.traditionalpageone || [], - errorsList.traditionalpageone || [] + errorsList.traditionalpageone || [], ), c: new UpdateSectionDataAction( submissionId, 'license', mockSectionsData.license as any, errorsList.license || [], - errorsList.license || [] + errorsList.license || [], ), d: new UpdateSectionDataAction( submissionId, 'upload', mockSectionsData.upload as any, errorsList.upload || [], - errorsList.upload || [] + errorsList.upload || [], ), }); @@ -394,20 +417,20 @@ describe('SubmissionObjectEffects test suite', () => { it('should not display errors when notification are disabled and field are not touched', () => { store.nextState({ submission: { - objects: submissionState + objects: submissionState, }, forms: { '2_traditionalpageone': { touched: { - 'dc.title': true - } - } - } + 'dc.title': true, + }, + }, + }, } as any); const response = [Object.assign({}, mockSubmissionRestResponse[0], { sections: mockSectionsData, - errors: mockSectionsErrors + errors: mockSectionsErrors, })]; actions = hot('--a-', { a: { @@ -415,9 +438,9 @@ describe('SubmissionObjectEffects test suite', () => { payload: { submissionId: submissionId, submissionObject: response, - notify: false - } - } + notify: false, + }, + }, }); const errorsToShowList = parseSectionErrors(mockSectionsErrorsTouchedField); @@ -428,21 +451,21 @@ describe('SubmissionObjectEffects test suite', () => { 'traditionalpageone', mockSectionsData.traditionalpageone as any, errorsToShowList.traditionalpageone, - serverValidationErrorsList.traditionalpageone + serverValidationErrorsList.traditionalpageone, ), c: new UpdateSectionDataAction( submissionId, 'license', mockSectionsData.license as any, errorsToShowList.license || [], - serverValidationErrorsList.license || [] + serverValidationErrorsList.license || [], ), d: new UpdateSectionDataAction( submissionId, 'upload', mockSectionsData.upload as any, errorsToShowList.upload || [], - serverValidationErrorsList.upload || [] + serverValidationErrorsList.upload || [], ), }); @@ -453,21 +476,21 @@ describe('SubmissionObjectEffects test suite', () => { it('should display a success notification', () => { store.nextState({ submission: { - objects: submissionState - } + objects: submissionState, + }, } as any); const response = [Object.assign({}, mockSubmissionRestResponse[0], { - sections: mockSectionsData + sections: mockSectionsData, })]; actions = hot('--a-', { a: { type: SubmissionObjectActionTypes.SAVE_SUBMISSION_FORM_SUCCESS, payload: { submissionId: submissionId, - submissionObject: response - } - } + submissionObject: response, + }, + }, }); const expected = cold('--(bcd)-', { @@ -476,21 +499,21 @@ describe('SubmissionObjectEffects test suite', () => { 'traditionalpageone', mockSectionsData.traditionalpageone as any, [], - [] + [], ), c: new UpdateSectionDataAction( submissionId, 'license', mockSectionsData.license as any, [], - [] + [], ), d: new UpdateSectionDataAction( submissionId, 'upload', mockSectionsData.upload as any, [], - [] + [], ), }); @@ -501,22 +524,22 @@ describe('SubmissionObjectEffects test suite', () => { it('should display a warning notification when there are errors', () => { store.nextState({ submission: { - objects: submissionState - } + objects: submissionState, + }, } as any); const response = [Object.assign({}, mockSubmissionRestResponse[0], { sections: mockSectionsData, - errors: mockSectionsErrors + errors: mockSectionsErrors, })]; actions = hot('--a-', { a: { type: SubmissionObjectActionTypes.SAVE_SUBMISSION_FORM_SUCCESS, payload: { submissionId: submissionId, - submissionObject: response - } - } + submissionObject: response, + }, + }, }); const errorsList = parseSectionErrors(mockSectionsErrors); @@ -526,21 +549,21 @@ describe('SubmissionObjectEffects test suite', () => { 'traditionalpageone', mockSectionsData.traditionalpageone as any, errorsList.traditionalpageone || [], - errorsList.traditionalpageone || [] + errorsList.traditionalpageone || [], ), c: new UpdateSectionDataAction( submissionId, 'license', mockSectionsData.license as any, errorsList.license || [], - errorsList.license || [] + errorsList.license || [], ), d: new UpdateSectionDataAction( submissionId, 'upload', mockSectionsData.upload as any, errorsList.upload || [], - errorsList.upload || [] + errorsList.upload || [], ), }); @@ -551,22 +574,22 @@ describe('SubmissionObjectEffects test suite', () => { it('should detect and notify a new section', () => { store.nextState({ submission: { - objects: submissionState - } + objects: submissionState, + }, } as any); const response = [Object.assign({}, mockSubmissionRestResponse[0], { sections: mockSectionsDataTwo, - errors: mockSectionsErrors + errors: mockSectionsErrors, })]; actions = hot('--a-', { a: { type: SubmissionObjectActionTypes.SAVE_SUBMISSION_FORM_SUCCESS, payload: { submissionId: submissionId, - submissionObject: response - } - } + submissionObject: response, + }, + }, }); const errorsList = parseSectionErrors(mockSectionsErrors); @@ -576,28 +599,28 @@ describe('SubmissionObjectEffects test suite', () => { 'traditionalpageone', mockSectionsDataTwo.traditionalpageone as any, errorsList.traditionalpageone || [], - errorsList.traditionalpageone || [] + errorsList.traditionalpageone || [], ), c: new UpdateSectionDataAction( submissionId, 'traditionalpagetwo', mockSectionsDataTwo.traditionalpagetwo as any, errorsList.traditionalpagetwo || [], - errorsList.traditionalpagetwo || [] + errorsList.traditionalpagetwo || [], ), d: new UpdateSectionDataAction( submissionId, 'license', mockSectionsDataTwo.license as any, errorsList.license || [], - errorsList.license || [] + errorsList.license || [], ), e: new UpdateSectionDataAction( submissionId, 'upload', mockSectionsDataTwo.upload as any, errorsList.upload || [], - errorsList.upload || [] + errorsList.upload || [], ), }); @@ -612,22 +635,22 @@ describe('SubmissionObjectEffects test suite', () => { it('should return a UPDATE_SECTION_DATA action for each updated section', () => { store.nextState({ submission: { - objects: submissionState - } + objects: submissionState, + }, } as any); const response = [Object.assign({}, mockSubmissionRestResponse[0], { sections: mockSectionsData, - errors: mockSectionsErrors + errors: mockSectionsErrors, })]; actions = hot('--a-', { a: { type: SubmissionObjectActionTypes.SAVE_SUBMISSION_SECTION_FORM_SUCCESS, payload: { submissionId: submissionId, - submissionObject: response - } - } + submissionObject: response, + }, + }, }); const errorsList = parseSectionErrors(mockSectionsErrors); @@ -637,21 +660,21 @@ describe('SubmissionObjectEffects test suite', () => { 'traditionalpageone', mockSectionsData.traditionalpageone as any, [], - errorsList.traditionalpageone + errorsList.traditionalpageone, ), c: new UpdateSectionDataAction( submissionId, 'license', mockSectionsData.license as any, errorsList.license || [], - errorsList.license || [] + errorsList.license || [], ), d: new UpdateSectionDataAction( submissionId, 'upload', mockSectionsData.upload as any, errorsList.upload || [], - errorsList.upload || [] + errorsList.upload || [], ), }); @@ -662,21 +685,21 @@ describe('SubmissionObjectEffects test suite', () => { it('should not display a success notification', () => { store.nextState({ submission: { - objects: submissionState - } + objects: submissionState, + }, } as any); const response = [Object.assign({}, mockSubmissionRestResponse[0], { - sections: mockSectionsData + sections: mockSectionsData, })]; actions = hot('--a-', { a: { type: SubmissionObjectActionTypes.SAVE_SUBMISSION_SECTION_FORM_SUCCESS, payload: { submissionId: submissionId, - submissionObject: response - } - } + submissionObject: response, + }, + }, }); const expected = cold('--(bcd)-', { @@ -685,21 +708,21 @@ describe('SubmissionObjectEffects test suite', () => { 'traditionalpageone', mockSectionsData.traditionalpageone as any, [], - [] + [], ), c: new UpdateSectionDataAction( submissionId, 'license', mockSectionsData.license as any, [], - [] + [], ), d: new UpdateSectionDataAction( submissionId, 'upload', mockSectionsData.upload as any, [], - [] + [], ), }); @@ -710,22 +733,22 @@ describe('SubmissionObjectEffects test suite', () => { it('should not display a warning notification when there are errors', () => { store.nextState({ submission: { - objects: submissionState - } + objects: submissionState, + }, } as any); const response = [Object.assign({}, mockSubmissionRestResponse[0], { sections: mockSectionsData, - errors: mockSectionsErrors + errors: mockSectionsErrors, })]; actions = hot('--a-', { a: { type: SubmissionObjectActionTypes.SAVE_SUBMISSION_SECTION_FORM_SUCCESS, payload: { submissionId: submissionId, - submissionObject: response - } - } + submissionObject: response, + }, + }, }); const serverValidationErrorsList = parseSectionErrors(mockSectionsErrors); @@ -735,21 +758,21 @@ describe('SubmissionObjectEffects test suite', () => { 'traditionalpageone', mockSectionsData.traditionalpageone as any, [], - serverValidationErrorsList.traditionalpageone + serverValidationErrorsList.traditionalpageone, ), c: new UpdateSectionDataAction( submissionId, 'license', mockSectionsData.license as any, serverValidationErrorsList.license || [], - serverValidationErrorsList.license || [] + serverValidationErrorsList.license || [], ), d: new UpdateSectionDataAction( submissionId, 'upload', mockSectionsData.upload as any, serverValidationErrorsList.upload || [], - serverValidationErrorsList.upload || [] + serverValidationErrorsList.upload || [], ), }); @@ -760,13 +783,13 @@ describe('SubmissionObjectEffects test suite', () => { it('should detect new sections but not notify for it', () => { store.nextState({ submission: { - objects: submissionState - } + objects: submissionState, + }, } as any); const response = [Object.assign({}, mockSubmissionRestResponse[0], { sections: mockSectionsDataTwo, - errors: mockSectionsErrors + errors: mockSectionsErrors, })]; actions = hot('--a-', { a: { @@ -774,8 +797,8 @@ describe('SubmissionObjectEffects test suite', () => { payload: { submissionId: submissionId, submissionObject: response, - } - } + }, + }, }); const errorsList = parseSectionErrors(mockSectionsErrors); @@ -785,28 +808,28 @@ describe('SubmissionObjectEffects test suite', () => { 'traditionalpageone', mockSectionsDataTwo.traditionalpageone as any, [], - errorsList.traditionalpageone + errorsList.traditionalpageone, ), c: new UpdateSectionDataAction( submissionId, 'traditionalpagetwo', mockSectionsDataTwo.traditionalpagetwo as any, errorsList.traditionalpagetwo || [], - errorsList.traditionalpagetwo || [] + errorsList.traditionalpagetwo || [], ), d: new UpdateSectionDataAction( submissionId, 'license', mockSectionsDataTwo.license as any, errorsList.license || [], - errorsList.license || [] + errorsList.license || [], ), e: new UpdateSectionDataAction( submissionId, 'upload', mockSectionsDataTwo.upload as any, errorsList.upload || [], - errorsList.upload || [] + errorsList.upload || [], ), }); @@ -823,17 +846,17 @@ describe('SubmissionObjectEffects test suite', () => { type: SubmissionObjectActionTypes.SAVE_SUBMISSION_SECTION_FORM, payload: { submissionId: submissionId, - sectionId: 'traditionalpageone' - } - } + sectionId: 'traditionalpageone', + }, + }, }); submissionJsonPatchOperationsServiceStub.jsonPatchByResourceID.and.returnValue(observableOf(mockSubmissionRestResponse)); const expected = cold('--b-', { b: new SaveSubmissionSectionFormSuccessAction( submissionId, - mockSubmissionRestResponse as any - ) + mockSubmissionRestResponse as any, + ), }); expect(submissionObjectEffects.saveSection$).toBeObservable(expected); @@ -845,18 +868,18 @@ describe('SubmissionObjectEffects test suite', () => { type: SubmissionObjectActionTypes.SAVE_SUBMISSION_SECTION_FORM, payload: { submissionId: submissionId, - sectionId: 'traditionalpageone' - } - } + sectionId: 'traditionalpageone', + }, + }, }); submissionJsonPatchOperationsServiceStub.jsonPatchByResourceID.and.callFake( - () => observableThrowError('Error') + () => observableThrowError('Error'), ); const expected = cold('--b-', { b: new SaveSubmissionSectionFormErrorAction( - submissionId - ) + submissionId, + ), }); expect(submissionObjectEffects.saveSection$).toBeObservable(expected); @@ -869,20 +892,20 @@ describe('SubmissionObjectEffects test suite', () => { a: { type: SubmissionObjectActionTypes.SAVE_AND_DEPOSIT_SUBMISSION, payload: { - submissionId: submissionId - } - } + submissionId: submissionId, + }, + }, }); const response = [Object.assign({}, mockSubmissionRestResponse[0], { - sections: mockSectionsDataTwo + sections: mockSectionsDataTwo, })]; submissionJsonPatchOperationsServiceStub.jsonPatchByResourceType.and.returnValue(observableOf(response)); const expected = cold('--b-', { b: new DepositSubmissionAction( - submissionId - ) + submissionId, + ), }); expect(submissionObjectEffects.saveAndDeposit$).toBeObservable(expected); @@ -892,28 +915,28 @@ describe('SubmissionObjectEffects test suite', () => { it('should return a SAVE_SUBMISSION_FORM_SUCCESS action when there are errors', () => { store.nextState({ submission: { - objects: submissionState - } + objects: submissionState, + }, } as any); actions = hot('--a-', { a: { type: SubmissionObjectActionTypes.SAVE_AND_DEPOSIT_SUBMISSION, payload: { - submissionId: submissionId - } - } + submissionId: submissionId, + }, + }, }); const response = [Object.assign({}, mockSubmissionRestResponse[0], { sections: mockSectionsData, - errors: mockSectionsErrors + errors: mockSectionsErrors, })]; submissionJsonPatchOperationsServiceStub.jsonPatchByResourceType.and.returnValue(observableOf(response)); const expected = cold('--b-', { - b: new SaveSubmissionFormSuccessAction(submissionId, response as any[], false, true) + b: new SaveSubmissionFormSuccessAction(submissionId, response as any[], false, true), }); expect(submissionObjectEffects.saveAndDeposit$).toBeObservable(expected); @@ -925,18 +948,18 @@ describe('SubmissionObjectEffects test suite', () => { a: { type: SubmissionObjectActionTypes.SAVE_AND_DEPOSIT_SUBMISSION, payload: { - submissionId: submissionId - } - } + submissionId: submissionId, + }, + }, }); submissionJsonPatchOperationsServiceStub.jsonPatchByResourceType.and.callFake( - () => observableThrowError('Error') + () => observableThrowError('Error'), ); const expected = cold('--b-', { b: new SaveSubmissionFormErrorAction( - submissionId - ) + submissionId, + ), }); expect(submissionObjectEffects.saveAndDeposit$).toBeObservable(expected); @@ -947,24 +970,24 @@ describe('SubmissionObjectEffects test suite', () => { it('should return a DEPOSIT_SUBMISSION_SUCCESS action on success', () => { store.nextState({ submission: { - objects: submissionState - } + objects: submissionState, + }, } as any); actions = hot('--a-', { a: { type: SubmissionObjectActionTypes.DEPOSIT_SUBMISSION, payload: { - submissionId: submissionId - } - } + submissionId: submissionId, + }, + }, }); submissionServiceStub.depositSubmission.and.returnValue(observableOf(mockSubmissionRestResponse)); const expected = cold('--b-', { b: new DepositSubmissionSuccessAction( - submissionId - ) + submissionId, + ), }); expect(submissionObjectEffects.depositSubmission$).toBeObservable(expected); @@ -973,26 +996,26 @@ describe('SubmissionObjectEffects test suite', () => { it('should return a DEPOSIT_SUBMISSION_ERROR action on error', () => { store.nextState({ submission: { - objects: submissionState - } + objects: submissionState, + }, } as any); actions = hot('--a-', { a: { type: SubmissionObjectActionTypes.DEPOSIT_SUBMISSION, payload: { - submissionId: submissionId - } - } + submissionId: submissionId, + }, + }, }); submissionServiceStub.depositSubmission.and.callFake( - () => observableThrowError('Error') + () => observableThrowError('Error'), ); const expected = cold('--b-', { b: new DepositSubmissionErrorAction( - submissionId - ) + submissionId, + ), }); expect(submissionObjectEffects.depositSubmission$).toBeObservable(expected); @@ -1006,9 +1029,9 @@ describe('SubmissionObjectEffects test suite', () => { type: SubmissionObjectActionTypes.SAVE_FOR_LATER_SUBMISSION_FORM_SUCCESS, payload: { submissionId: submissionId, - submissionObject: mockSubmissionRestResponse - } - } + submissionObject: mockSubmissionRestResponse, + }, + }, }); submissionObjectEffects.saveForLaterSubmissionSuccess$.subscribe(() => { @@ -1024,9 +1047,9 @@ describe('SubmissionObjectEffects test suite', () => { a: { type: SubmissionObjectActionTypes.DEPOSIT_SUBMISSION_SUCCESS, payload: { - submissionId: submissionId - } - } + submissionId: submissionId, + }, + }, }); submissionObjectEffects.depositSubmissionSuccess$.subscribe(() => { @@ -1042,9 +1065,9 @@ describe('SubmissionObjectEffects test suite', () => { a: { type: SubmissionObjectActionTypes.DEPOSIT_SUBMISSION_ERROR, payload: { - submissionId: submissionId - } - } + submissionId: submissionId, + }, + }, }); submissionObjectEffects.depositSubmissionError$.subscribe(() => { @@ -1059,9 +1082,9 @@ describe('SubmissionObjectEffects test suite', () => { a: { type: SubmissionObjectActionTypes.SAVE_SUBMISSION_FORM_ERROR, payload: { - submissionId: submissionId - } - } + submissionId: submissionId, + }, + }, }); submissionObjectEffects.saveError$.subscribe(() => { @@ -1074,9 +1097,9 @@ describe('SubmissionObjectEffects test suite', () => { a: { type: SubmissionObjectActionTypes.SAVE_SUBMISSION_SECTION_FORM_ERROR, payload: { - submissionId: submissionId - } - } + submissionId: submissionId, + }, + }, }); submissionObjectEffects.saveError$.subscribe(() => { @@ -1089,24 +1112,24 @@ describe('SubmissionObjectEffects test suite', () => { it('should return a DISCARD_SUBMISSION_SUCCESS action on success', () => { store.nextState({ submission: { - objects: submissionState - } + objects: submissionState, + }, } as any); actions = hot('--a-', { a: { type: SubmissionObjectActionTypes.DISCARD_SUBMISSION, payload: { - submissionId: submissionId - } - } + submissionId: submissionId, + }, + }, }); submissionServiceStub.discardSubmission.and.returnValue(observableOf(mockSubmissionRestResponse)); const expected = cold('--b-', { b: new DiscardSubmissionSuccessAction( - submissionId - ) + submissionId, + ), }); expect(submissionObjectEffects.discardSubmission$).toBeObservable(expected); @@ -1115,26 +1138,26 @@ describe('SubmissionObjectEffects test suite', () => { it('should return a DISCARD_SUBMISSION_ERROR action on error', () => { store.nextState({ submission: { - objects: submissionState - } + objects: submissionState, + }, } as any); actions = hot('--a-', { a: { type: SubmissionObjectActionTypes.DISCARD_SUBMISSION, payload: { - submissionId: submissionId - } - } + submissionId: submissionId, + }, + }, }); submissionServiceStub.discardSubmission.and.callFake( - () => observableThrowError('Error') + () => observableThrowError('Error'), ); const expected = cold('--b-', { b: new DiscardSubmissionErrorAction( - submissionId - ) + submissionId, + ), }); expect(submissionObjectEffects.discardSubmission$).toBeObservable(expected); @@ -1147,9 +1170,9 @@ describe('SubmissionObjectEffects test suite', () => { a: { type: SubmissionObjectActionTypes.DISCARD_SUBMISSION_SUCCESS, payload: { - submissionId: submissionId - } - } + submissionId: submissionId, + }, + }, }); submissionObjectEffects.discardSubmissionSuccess$.subscribe(() => { @@ -1165,9 +1188,9 @@ describe('SubmissionObjectEffects test suite', () => { a: { type: SubmissionObjectActionTypes.DISCARD_SUBMISSION_ERROR, payload: { - submissionId: submissionId - } - } + submissionId: submissionId, + }, + }, }); submissionObjectEffects.discardSubmissionError$.subscribe(() => { diff --git a/src/app/submission/objects/submission-objects.effects.ts b/src/app/submission/objects/submission-objects.effects.ts index 98646009d5b..9afb6cec40f 100644 --- a/src/app/submission/objects/submission-objects.effects.ts +++ b/src/app/submission/objects/submission-objects.effects.ts @@ -1,27 +1,59 @@ import { Injectable } from '@angular/core'; -import { Actions, createEffect, ofType } from '@ngrx/effects'; +import { + Actions, + createEffect, + ofType, +} from '@ngrx/effects'; import { Store } from '@ngrx/store'; import { TranslateService } from '@ngx-translate/core'; import findKey from 'lodash/findKey'; import isEqual from 'lodash/isEqual'; import union from 'lodash/union'; - -import { from as observableFrom, Observable, of as observableOf } from 'rxjs'; -import { catchError, filter, map, mergeMap, switchMap, take, tap, withLatestFrom } from 'rxjs/operators'; +import { + from as observableFrom, + Observable, + of as observableOf, +} from 'rxjs'; +import { + catchError, + filter, + map, + mergeMap, + switchMap, + take, + tap, + withLatestFrom, +} from 'rxjs/operators'; + +import { environment } from '../../../environments/environment'; +import { RemoteData } from '../../core/data/remote-data'; +import { Item } from '../../core/shared/item.model'; +import { getFirstSucceededRemoteDataPayload } from '../../core/shared/operators'; import { SubmissionObject } from '../../core/submission/models/submission-object.model'; import { WorkflowItem } from '../../core/submission/models/workflowitem.model'; +import { WorkspaceItem } from '../../core/submission/models/workspaceitem.model'; +import { WorkspaceitemSectionDuplicatesObject } from '../../core/submission/models/workspaceitem-section-duplicates.model'; import { WorkspaceitemSectionUploadObject } from '../../core/submission/models/workspaceitem-section-upload.model'; import { WorkspaceitemSectionsObject } from '../../core/submission/models/workspaceitem-sections.model'; -import { WorkspaceItem } from '../../core/submission/models/workspaceitem.model'; import { SubmissionJsonPatchOperationsService } from '../../core/submission/submission-json-patch-operations.service'; -import { isEmpty, isNotEmpty, isNotUndefined } from '../../shared/empty.util'; +import { SubmissionObjectDataService } from '../../core/submission/submission-object-data.service'; +import { WorkspaceitemDataService } from '../../core/submission/workspaceitem-data.service'; +import { + isEmpty, + isNotEmpty, + isNotUndefined, +} from '../../shared/empty.util'; +import { FormState } from '../../shared/form/form.reducer'; import { NotificationsService } from '../../shared/notifications/notifications.service'; -import { SectionsType } from '../sections/sections-type'; +import { followLink } from '../../shared/utils/follow-link-config.model'; import { SectionsService } from '../sections/sections.service'; +import { SectionsType } from '../sections/sections-type'; import { SubmissionState } from '../submission.reducers'; import { SubmissionService } from '../submission.service'; +import parseSectionErrorPaths, { SectionErrorPath } from '../utils/parseSectionErrorPaths'; import parseSectionErrors from '../utils/parseSectionErrors'; import { + CleanDuplicateDetectionAction, CompleteInitSubmissionFormAction, DepositSubmissionAction, DepositSubmissionErrorAction, @@ -43,18 +75,11 @@ import { SubmissionObjectAction, SubmissionObjectActionTypes, UpdateSectionDataAction, - UpdateSectionDataSuccessAction + UpdateSectionDataSuccessAction, } from './submission-objects.actions'; import { SubmissionObjectEntry } from './submission-objects.reducer'; -import { Item } from '../../core/shared/item.model'; -import { RemoteData } from '../../core/data/remote-data'; -import { getFirstSucceededRemoteDataPayload } from '../../core/shared/operators'; -import { SubmissionObjectDataService } from '../../core/submission/submission-object-data.service'; -import { followLink } from '../../shared/utils/follow-link-config.model'; -import parseSectionErrorPaths, { SectionErrorPath } from '../utils/parseSectionErrorPaths'; -import { FormState } from '../../shared/form/form.reducer'; -import { SubmissionSectionObject } from './submission-section-object.model'; import { SubmissionSectionError } from './submission-section-error.model'; +import { SubmissionSectionObject } from './submission-section-object.model'; @Injectable() export class SubmissionObjectEffects { @@ -71,7 +96,12 @@ export class SubmissionObjectEffects { const selfLink = sectionDefinition._links.self.href || sectionDefinition._links.self; const sectionId = selfLink.substr(selfLink.lastIndexOf('/') + 1); const config = sectionDefinition._links.config ? (sectionDefinition._links.config.href || sectionDefinition._links.config) : ''; - const enabled = (sectionDefinition.mandatory) || (isNotEmpty(action.payload.sections) && action.payload.sections.hasOwnProperty(sectionId)); + // A section is enabled if it is mandatory or contains data in its section payload + // except for detect duplicate steps which will be hidden with no data unless overridden in config, even if mandatory + const enabled = (sectionDefinition.mandatory && (sectionDefinition.sectionType !== SectionsType.Duplicates)) + || (isNotEmpty(action.payload.sections) && action.payload.sections.hasOwnProperty(sectionId) + && (sectionDefinition.sectionType === SectionsType.Duplicates && (alwaysDisplayDuplicates() || isNotEmpty((action.payload.sections[sectionId] as WorkspaceitemSectionDuplicatesObject).potentialDuplicates))) + ); let sectionData; if (sectionDefinition.sectionType !== SectionsType.SubmissionForm) { sectionData = (isNotUndefined(action.payload.sections) && isNotUndefined(action.payload.sections[sectionId])) ? action.payload.sections[sectionId] : Object.create(null); @@ -90,8 +120,8 @@ export class SubmissionObjectEffects { sectionDefinition.visibility, enabled, sectionData, - sectionErrors - ) + sectionErrors, + ), ); }); return { action: action, definition: definition, mappedActions: mappedActions }; @@ -99,7 +129,7 @@ export class SubmissionObjectEffects { mergeMap((result) => { return observableFrom( result.mappedActions.concat( - new CompleteInitSubmissionFormAction(result.action.payload.submissionId) + new CompleteInitSubmissionFormAction(result.action.payload.submissionId), )); }))); @@ -116,7 +146,7 @@ export class SubmissionObjectEffects { action.payload.submissionDefinition, action.payload.sections, action.payload.item, - null + null, )))); /** @@ -129,8 +159,8 @@ export class SubmissionObjectEffects { this.submissionService.getSubmissionObjectLinkName(), action.payload.submissionId, 'sections').pipe( - map((response: SubmissionObject[]) => new SaveSubmissionFormSuccessAction(action.payload.submissionId, response, action.payload.isManual, action.payload.isManual)), - catchError(() => observableOf(new SaveSubmissionFormErrorAction(action.payload.submissionId)))); + map((response: SubmissionObject[]) => new SaveSubmissionFormSuccessAction(action.payload.submissionId, response, action.payload.isManual, action.payload.isManual)), + catchError(() => observableOf(new SaveSubmissionFormErrorAction(action.payload.submissionId)))); }))); /** @@ -143,8 +173,8 @@ export class SubmissionObjectEffects { this.submissionService.getSubmissionObjectLinkName(), action.payload.submissionId, 'sections').pipe( - map((response: SubmissionObject[]) => new SaveForLaterSubmissionFormSuccessAction(action.payload.submissionId, response)), - catchError(() => observableOf(new SaveSubmissionFormErrorAction(action.payload.submissionId)))); + map((response: SubmissionObject[]) => new SaveForLaterSubmissionFormSuccessAction(action.payload.submissionId, response)), + catchError(() => observableOf(new SaveSubmissionFormErrorAction(action.payload.submissionId)))); }))); /** @@ -184,8 +214,8 @@ export class SubmissionObjectEffects { action.payload.submissionId, 'sections', action.payload.sectionId).pipe( - map((response: SubmissionObject[]) => new SaveSubmissionSectionFormSuccessAction(action.payload.submissionId, response)), - catchError(() => observableOf(new SaveSubmissionSectionFormErrorAction(action.payload.submissionId)))); + map((response: SubmissionObject[]) => new SaveSubmissionSectionFormSuccessAction(action.payload.submissionId, response)), + catchError(() => observableOf(new SaveSubmissionSectionFormErrorAction(action.payload.submissionId)))); }))); /** @@ -210,9 +240,9 @@ export class SubmissionObjectEffects { action.payload.submissionId, 'sections') as Observable; } else { - response$ = this.submissionObjectService.findById(action.payload.submissionId, false, true).pipe( + response$ = this.submissionObjectService.findById(action.payload.submissionId, false, true, followLink('item'), followLink('collection')).pipe( getFirstSucceededRemoteDataPayload(), - map((submissionObject: SubmissionObject) => [submissionObject]) + map((submissionObject: SubmissionObject) => [submissionObject]), ); } return response$.pipe( @@ -224,7 +254,7 @@ export class SubmissionObjectEffects { null, this.translate.instant('submission.sections.general.cannot_deposit'), null, - true + true, ); return new SaveSubmissionFormSuccessAction(action.payload.submissionId, response, false, true); } @@ -241,7 +271,7 @@ export class SubmissionObjectEffects { switchMap(([action, state]: [DepositSubmissionAction, any]) => { return this.submissionService.depositSubmission(state.submission.objects[action.payload.submissionId].selfUrl).pipe( map(() => new DepositSubmissionSuccessAction(action.payload.submissionId)), - catchError((error) => observableOf(new DepositSubmissionErrorAction(action.payload.submissionId)))); + catchError((error: unknown) => observableOf(new DepositSubmissionErrorAction(action.payload.submissionId)))); }))); /** @@ -258,6 +288,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 }); /** @@ -292,7 +323,7 @@ export class SubmissionObjectEffects { if (section.sectionType === SectionsType.SubmissionForm) { const submissionObject$ = this.submissionObjectService .findById(action.payload.submissionId, true, false, followLink('item')).pipe( - getFirstSucceededRemoteDataPayload() + getFirstSucceededRemoteDataPayload(), ); const item$ = submissionObject$.pipe( @@ -303,7 +334,7 @@ export class SubmissionObjectEffects { return item$.pipe( map((item: Item) => item.metadata), filter((metadata) => !isEqual(action.payload.data, metadata)), - map((metadata: any) => new UpdateSectionDataAction(action.payload.submissionId, action.payload.sectionId, metadata, action.payload.errorsToShow, action.payload.serverValidationErrors, action.payload.metadata)) + map((metadata: any) => new UpdateSectionDataAction(action.payload.submissionId, action.payload.sectionId, metadata, action.payload.errorsToShow, action.payload.serverValidationErrors, action.payload.metadata)), ); } else { return observableOf(new UpdateSectionDataSuccessAction()); @@ -326,14 +357,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, + ) { } /** @@ -434,8 +468,16 @@ export class SubmissionObjectEffects { && isEmpty(sections[sherpaPoliciesSectionId])) { mappedActions.push(new UpdateSectionDataAction(submissionId, sherpaPoliciesSectionId, null, [], [])); } - }); + // When Duplicate Detection step is enabled, add it only if there are duplicates in the response section data + // or if configuration overrides this behaviour + if (!alwaysDisplayDuplicates()) { + const duplicatesSectionId = findKey(currentState.sections, (section) => section.sectionType === SectionsType.Duplicates); + if (isNotUndefined(duplicatesSectionId) && sections.hasOwnProperty(duplicatesSectionId) && isEmpty((sections[duplicatesSectionId] as WorkspaceitemSectionDuplicatesObject).potentialDuplicates)) { + mappedActions.push(new CleanDuplicateDetectionAction(submissionId)); + } + } + }); } return mappedActions; } @@ -464,7 +506,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) { @@ -481,3 +523,7 @@ function filterErrors(sectionForm: FormState, sectionErrors: SubmissionSectionEr }); return filteredErrors; } + +function alwaysDisplayDuplicates(): boolean { + return (environment.submission.duplicateDetection.alwaysShowSection); +} diff --git a/src/app/submission/objects/submission-objects.reducer.spec.ts b/src/app/submission/objects/submission-objects.reducer.spec.ts index 2a24afae19c..2389a5600c8 100644 --- a/src/app/submission/objects/submission-objects.reducer.spec.ts +++ b/src/app/submission/objects/submission-objects.reducer.spec.ts @@ -1,7 +1,16 @@ -import { submissionObjectReducer, SubmissionObjectState } from './submission-objects.reducer'; +import { Item } from '../../core/shared/item.model'; +import { + mockSubmissionCollectionId, + mockSubmissionDefinitionResponse, + mockSubmissionId, + mockSubmissionSelfUrl, + mockSubmissionState, +} from '../../shared/mocks/submission.mock'; +import { SectionsType } from '../sections/sections-type'; import { CancelSubmissionFormAction, ChangeSubmissionCollectionAction, + CleanDuplicateDetectionAction, CompleteInitSubmissionFormAction, DeleteSectionErrorsAction, DeleteUploadedFileAction, @@ -30,17 +39,12 @@ import { SaveSubmissionSectionFormSuccessAction, SectionStatusChangeAction, SubmissionObjectAction, - UpdateSectionDataAction + UpdateSectionDataAction, } from './submission-objects.actions'; -import { SectionsType } from '../sections/sections-type'; import { - mockSubmissionCollectionId, - mockSubmissionDefinitionResponse, - mockSubmissionId, - mockSubmissionSelfUrl, - mockSubmissionState -} from '../../shared/mocks/submission.mock'; -import { Item } from '../../core/shared/item.model'; + submissionObjectReducer, + SubmissionObjectState, +} from './submission-objects.reducer'; describe('submissionReducer test suite', () => { @@ -66,7 +70,7 @@ describe('submissionReducer test suite', () => { isLoading: true, savePending: false, depositPending: false, - } + }, }; const action = new InitSubmissionFormAction(collectionId, submissionId, selfUrl, submissionDefinition, {}, new Item(), null); @@ -78,8 +82,8 @@ describe('submissionReducer test suite', () => { it('should complete submission initialization', () => { const state = Object.assign({}, initState, { [submissionId]: Object.assign({}, initState[submissionId], { - isLoading: true - }) + isLoading: true, + }), }); const action = new CompleteInitSubmissionFormAction(submissionId); @@ -99,7 +103,7 @@ describe('submissionReducer test suite', () => { isLoading: true, savePending: false, depositPending: false, - } + }, }; const action = new ResetSubmissionFormAction(collectionId, submissionId, selfUrl, {}, submissionDefinition, new Item()); @@ -143,7 +147,7 @@ describe('submissionReducer test suite', () => { const state = Object.assign({}, initState, { [submissionId]: Object.assign({}, initState[submissionId], { savePending: true, - }) + }), }); let action: any = new SaveSubmissionFormSuccessAction(submissionId, []); @@ -191,7 +195,7 @@ describe('submissionReducer test suite', () => { const state = Object.assign({}, initState, { [submissionId]: Object.assign({}, initState[submissionId], { depositPending: true, - }) + }), }); const action: any = new DepositSubmissionSuccessAction(submissionId); @@ -241,7 +245,7 @@ describe('submissionReducer test suite', () => { errorsToShow: [], serverValidationErrors: [], isLoading: false, - isValid: true + isValid: true, } as any; let action: any = new InitSubmissionFormAction(collectionId, submissionId, selfUrl, submissionDefinition, {}, new Item(), null); @@ -273,7 +277,7 @@ describe('submissionReducer test suite', () => { expect(newState[826].sections.traditionalpagetwo.enabled).toBeTruthy(); }); - it('should enable submission section properly', () => { + it('should disable submission section properly', () => { let action: SubmissionObjectAction = new EnableSectionAction(submissionId, 'traditionalpagetwo'); let newState = submissionObjectReducer(initState, action); @@ -306,8 +310,8 @@ describe('submissionReducer test suite', () => { authority: null, display: 'Author, Test', confidence: -1, - place: 0 - } + place: 0, + }, ], 'dc.title': [ { @@ -316,8 +320,8 @@ describe('submissionReducer test suite', () => { authority: null, display: 'Title Test', confidence: -1, - place: 0 - } + place: 0, + }, ], 'dc.date.issued': [ { @@ -326,9 +330,9 @@ describe('submissionReducer test suite', () => { authority: null, display: '2015', confidence: -1, - place: 0 - } - ] + place: 0, + }, + ], } as any; const action = new UpdateSectionDataAction(submissionId, 'traditionalpageone', data, [], []); @@ -352,8 +356,8 @@ describe('submissionReducer test suite', () => { const errors = [ { path: '/sections/license', - message: 'error.validation.license.notgranted' - } + message: 'error.validation.license.notgranted', + }, ]; const action = new UpdateSectionDataAction(submissionId, 'traditionalpageone', {}, errors, errors); @@ -374,7 +378,7 @@ describe('submissionReducer test suite', () => { it('should add submission section error properly', () => { const error = { path: '/sections/traditionalpageone/dc.title/0', - message: 'error.validation.traditionalpageone.required' + message: 'error.validation.traditionalpageone.required', }; const action = new InertSectionErrorsAction(submissionId, 'traditionalpageone', error); @@ -387,21 +391,21 @@ describe('submissionReducer test suite', () => { const errors = [ { path: '/sections/traditionalpageone/dc.contributor.author', - message: 'error.validation.required' + message: 'error.validation.required', }, { path: '/sections/traditionalpageone/dc.date.issued', - message: 'error.validation.required' - } + message: 'error.validation.required', + }, ]; const error = { path: '/sections/traditionalpageone/dc.contributor.author', - message: 'error.validation.required' + message: 'error.validation.required', }; const expectedErrors = [{ path: '/sections/traditionalpageone/dc.date.issued', - message: 'error.validation.required' + message: 'error.validation.required', }]; let action: any = new UpdateSectionDataAction(submissionId, 'traditionalpageone', {}, errors, errors); @@ -433,9 +437,9 @@ describe('submissionReducer test suite', () => { authority: null, display: '28297_389341539060_6452876_n.jpg', confidence: -1, - place: 0 - } - ] + place: 0, + }, + ], }, accessConditions: [], format: { @@ -446,17 +450,17 @@ describe('submissionReducer test suite', () => { supportLevel: 0, internal: false, extensions: null, - type: 'bitstreamformat' + type: 'bitstreamformat', }, sizeBytes: 22737, checkSum: { checkSumAlgorithm: 'MD5', - value: '8722864dd671912f94a999ac7c4949d2' + value: '8722864dd671912f94a999ac7c4949d2', }, - url: 'https://rest.api/dspace-spring-rest/api/core/bitstreams/8cd86fba-70c8-483d-838a-70d28e7ed570/content' + url: 'https://rest.api/dspace-spring-rest/api/core/bitstreams/8cd86fba-70c8-483d-838a-70d28e7ed570/content', }; const expectedState = { - files: [fileData] + files: [fileData], }; const action = new NewUploadedFileAction(submissionId, 'upload', uuid, fileData); @@ -478,9 +482,9 @@ describe('submissionReducer test suite', () => { authority: null, display: 'image_test.jpg', confidence: -1, - place: 0 - } - ] + place: 0, + }, + ], }, accessConditions: [], format: { @@ -491,14 +495,14 @@ describe('submissionReducer test suite', () => { supportLevel: 0, internal: false, extensions: null, - type: 'bitstreamformat' + type: 'bitstreamformat', }, sizeBytes: 22737, checkSum: { checkSumAlgorithm: 'MD5', - value: '8722864dd671912f94a999ac7c4949d2' + value: '8722864dd671912f94a999ac7c4949d2', }, - url: 'https://rest.api/dspace-spring-rest/api/core/bitstreams/8cd86fba-70c8-483d-838a-70d28e7ed570/content' + url: 'https://rest.api/dspace-spring-rest/api/core/bitstreams/8cd86fba-70c8-483d-838a-70d28e7ed570/content', }; const fileData2: any = { uuid: uuid2, @@ -510,9 +514,9 @@ describe('submissionReducer test suite', () => { authority: null, display: 'image_test.jpg', confidence: -1, - place: 0 - } - ] + place: 0, + }, + ], }, accessConditions: [], format: { @@ -523,14 +527,14 @@ describe('submissionReducer test suite', () => { supportLevel: 0, internal: false, extensions: null, - type: 'bitstreamformat' + type: 'bitstreamformat', }, sizeBytes: 22737, checkSum: { checkSumAlgorithm: 'MD5', - value: '8722864dd671912f94a999ac7c4949d2' + value: '8722864dd671912f94a999ac7c4949d2', }, - url: 'https://rest.api/dspace-spring-rest/api/core/bitstreams/7e2f4ba9-9316-41fd-844a-1ef435f41a42/content' + url: 'https://rest.api/dspace-spring-rest/api/core/bitstreams/7e2f4ba9-9316-41fd-844a-1ef435f41a42/content', }; const state: SubmissionObjectState = Object.assign({}, initState, { @@ -538,15 +542,15 @@ describe('submissionReducer test suite', () => { sections: Object.assign({}, initState[submissionId].sections, { upload: Object.assign({}, initState[submissionId].sections.upload, { data: { - files: [fileData, fileData2] - } - }) - }) - }) + files: [fileData, fileData2], + }, + }), + }), + }), }); const expectedState = { - files: [fileData] + files: [fileData], }; const action = new DeleteUploadedFileAction(submissionId, 'upload', uuid2); @@ -567,9 +571,9 @@ describe('submissionReducer test suite', () => { authority: null, display: 'image_test.jpg', confidence: -1, - place: 0 - } - ] + place: 0, + }, + ], }, accessConditions: [], format: { @@ -580,14 +584,14 @@ describe('submissionReducer test suite', () => { supportLevel: 0, internal: false, extensions: null, - type: 'bitstreamformat' + type: 'bitstreamformat', }, sizeBytes: 22737, checkSum: { checkSumAlgorithm: 'MD5', - value: '8722864dd671912f94a999ac7c4949d2' + value: '8722864dd671912f94a999ac7c4949d2', }, - url: 'https://rest.api/dspace-spring-rest/api/core/bitstreams/8cd86fba-70c8-483d-838a-70d28e7ed570/content' + url: 'https://rest.api/dspace-spring-rest/api/core/bitstreams/8cd86fba-70c8-483d-838a-70d28e7ed570/content', }; const fileData2: any = { uuid: uuid, @@ -599,9 +603,9 @@ describe('submissionReducer test suite', () => { authority: null, display: 'New title', confidence: -1, - place: 0 - } - ] + place: 0, + }, + ], }, accessConditions: [], format: { @@ -612,14 +616,14 @@ describe('submissionReducer test suite', () => { supportLevel: 0, internal: false, extensions: null, - type: 'bitstreamformat' + type: 'bitstreamformat', }, sizeBytes: 22737, checkSum: { checkSumAlgorithm: 'MD5', - value: '8722864dd671912f94a999ac7c4949d2' + value: '8722864dd671912f94a999ac7c4949d2', }, - url: 'https://rest.api/dspace-spring-rest/api/core/bitstreams/7e2f4ba9-9316-41fd-844a-1ef435f41a42/content' + url: 'https://rest.api/dspace-spring-rest/api/core/bitstreams/7e2f4ba9-9316-41fd-844a-1ef435f41a42/content', }; const state: SubmissionObjectState = Object.assign({}, initState, { @@ -627,15 +631,15 @@ describe('submissionReducer test suite', () => { sections: Object.assign({}, initState[submissionId].sections, { upload: Object.assign({}, initState[submissionId].sections.upload, { data: { - files: [fileData] - } - }) - }) - }) + files: [fileData], + }, + }), + }), + }), }); const expectedState = { - files: [fileData2] + files: [fileData2], }; const action = new EditFileDataAction(submissionId, 'upload', uuid, fileData2); @@ -644,4 +648,20 @@ describe('submissionReducer test suite', () => { expect(newState[826].sections.upload.data).toEqual(expectedState); }); + it('should enable duplicates section properly', () => { + + let action: SubmissionObjectAction = new EnableSectionAction(submissionId, 'duplicates'); + let newState = submissionObjectReducer(initState, action); + + expect(newState[826].sections.duplicates.enabled).toBeTruthy(); + }); + + it('should clean duplicates section properly', () => { + + let action = new CleanDuplicateDetectionAction(submissionId); + let newState = submissionObjectReducer(initState, action); + + expect(newState[826].sections.duplicates.enabled).toBeFalsy(); + }); + }); diff --git a/src/app/submission/objects/submission-objects.reducer.ts b/src/app/submission/objects/submission-objects.reducer.ts index a05bf05f52c..82e33062cda 100644 --- a/src/app/submission/objects/submission-objects.reducer.ts +++ b/src/app/submission/objects/submission-objects.reducer.ts @@ -1,11 +1,20 @@ -import { hasValue, isEmpty, isNotEmpty, isNotNull, isUndefined } from '../../shared/empty.util'; import differenceWith from 'lodash/differenceWith'; import findKey from 'lodash/findKey'; import isEqual from 'lodash/isEqual'; import uniqWith from 'lodash/uniqWith'; +import { WorkspaceitemSectionUploadObject } from '../../core/submission/models/workspaceitem-section-upload.model'; +import { + hasValue, + isEmpty, + isNotEmpty, + isNotNull, + isNull, + isUndefined, +} from '../../shared/empty.util'; import { ChangeSubmissionCollectionAction, + CleanDuplicateDetectionAction, CompleteInitSubmissionFormAction, DeleteSectionErrorsAction, DeleteUploadedFileAction, @@ -14,6 +23,7 @@ import { DepositSubmissionSuccessAction, DisableSectionAction, EditFileDataAction, + EditFilePrimaryBitstreamAction, EnableSectionAction, InertSectionErrorsAction, InitSectionAction, @@ -36,9 +46,8 @@ import { SetSectionFormId, SubmissionObjectAction, SubmissionObjectActionTypes, - UpdateSectionDataAction + UpdateSectionDataAction, } from './submission-objects.actions'; -import { WorkspaceitemSectionUploadObject } from '../../core/submission/models/workspaceitem-section-upload.model'; import { SubmissionSectionObject } from './submission-section-object.model'; /** @@ -203,6 +212,10 @@ export function submissionObjectReducer(state = initialState, action: Submission return newFile(state, action as NewUploadedFileAction); } + case SubmissionObjectActionTypes.EDIT_FILE_PRIMARY_BITSTREAM_DATA: { + return editPrimaryBitstream(state, action as EditFilePrimaryBitstreamAction); + } + case SubmissionObjectActionTypes.EDIT_FILE_DATA: { return editFileData(state, action as EditFileDataAction); } @@ -224,6 +237,10 @@ export function submissionObjectReducer(state = initialState, action: Submission return removeSectionErrors(state, action as RemoveSectionErrorsAction); } + case SubmissionObjectActionTypes.CLEAN_DUPLICATE_DETECTION: { + return cleanDuplicateDetectionSection(state, action as CleanDuplicateDetectionAction); + } + default: { return state; } @@ -249,10 +266,10 @@ const removeError = (state: SubmissionObjectState, action: DeleteSectionErrorsAc [ submissionId ]: Object.assign({}, state[ submissionId ], { sections: Object.assign({}, state[ submissionId ].sections, { [ sectionId ]: Object.assign({}, state[ submissionId ].sections [ sectionId ], { - errorsToShow: filteredErrors - }) - }) - }) + errorsToShow: filteredErrors, + }), + }), + }), }); } else { return state; @@ -269,10 +286,10 @@ const addError = (state: SubmissionObjectState, action: InertSectionErrorsAction [ submissionId ]: Object.assign({}, state[ submissionId ], { activeSection: state[ action.payload.submissionId ].activeSection, sections: Object.assign({}, state[ submissionId ].sections, { [ sectionId ]: Object.assign({}, state[ action.payload.submissionId ].sections [ action.payload.sectionId ], { - errorsToShow - }) + errorsToShow, + }), }), - }) + }), }); } else { return state; @@ -296,10 +313,10 @@ function removeSectionErrors(state: SubmissionObjectState, action: RemoveSection [ action.payload.submissionId ]: Object.assign({}, state[ action.payload.submissionId ], { sections: Object.assign({}, state[ action.payload.submissionId ].sections, { [ action.payload.sectionId ]: Object.assign({}, state[ action.payload.submissionId ].sections [ action.payload.sectionId ], { - errorsToShow: [] - }) - }) - }) + errorsToShow: [], + }), + }), + }), }); } else { return state; @@ -349,8 +366,8 @@ function resetSubmission(state: SubmissionObjectState, action: ResetSubmissionFo return Object.assign({}, state, { [ action.payload.submissionId ]: Object.assign({}, state[ action.payload.submissionId ], { sections: Object.create(null), - isLoading: true - }) + isLoading: true, + }), }); } else { return state; @@ -371,8 +388,8 @@ function completeInit(state: SubmissionObjectState, action: CompleteInitSubmissi if (hasValue(state[ action.payload.submissionId ])) { return Object.assign({}, state, { [ action.payload.submissionId ]: Object.assign({}, state[ action.payload.submissionId ], { - isLoading: false - }) + isLoading: false, + }), }); } else { return state; @@ -391,7 +408,7 @@ function completeInit(state: SubmissionObjectState, action: CompleteInitSubmissi * the new state, with the flag set to true. */ function saveSubmission(state: SubmissionObjectState, - action: SaveSubmissionFormAction + action: SaveSubmissionFormAction | SaveSubmissionSectionFormAction | SaveForLaterSubmissionFormAction | SaveAndDepositSubmissionAction): SubmissionObjectState { @@ -402,7 +419,7 @@ function saveSubmission(state: SubmissionObjectState, sections: state[ action.payload.submissionId ].sections, isLoading: state[ action.payload.submissionId ].isLoading, savePending: true, - }) + }), }); } else { return state; @@ -422,7 +439,7 @@ function saveSubmission(state: SubmissionObjectState, * the new state, with the flag set to false. */ function completeSave(state: SubmissionObjectState, - action: SaveSubmissionFormSuccessAction + action: SaveSubmissionFormSuccessAction | SaveForLaterSubmissionFormSuccessAction | SaveSubmissionSectionFormSuccessAction | SaveSubmissionFormErrorAction @@ -432,7 +449,7 @@ function completeSave(state: SubmissionObjectState, return Object.assign({}, state, { [ action.payload.submissionId ]: Object.assign({}, state[ action.payload.submissionId ], { savePending: false, - }) + }), }); } else { return state; @@ -455,7 +472,7 @@ function startDeposit(state: SubmissionObjectState, action: DepositSubmissionAct [ action.payload.submissionId ]: Object.assign({}, state[ action.payload.submissionId ], { savePending: false, depositPending: true, - }) + }), }); } else { return state; @@ -477,7 +494,7 @@ function endDeposit(state: SubmissionObjectState, action: DepositSubmissionSucce return Object.assign({}, state, { [ action.payload.submissionId ]: Object.assign({}, state[ action.payload.submissionId ], { depositPending: false, - }) + }), }); } else { return state; @@ -497,8 +514,8 @@ function endDeposit(state: SubmissionObjectState, action: DepositSubmissionSucce function changeCollection(state: SubmissionObjectState, action: ChangeSubmissionCollectionAction): SubmissionObjectState { return Object.assign({}, state, { [ action.payload.submissionId ]: Object.assign({}, state[ action.payload.submissionId ], { - collection: action.payload.collectionId - }) + collection: action.payload.collectionId, + }), }); } @@ -522,7 +539,7 @@ function setActiveSection(state: SubmissionObjectState, action: SetActiveSection sections: state[ action.payload.submissionId ].sections, isLoading: state[ action.payload.submissionId ].isLoading, savePending: state[ action.payload.submissionId ].savePending, - }) + }), }); } else { return state; @@ -556,10 +573,10 @@ function initSection(state: SubmissionObjectState, action: InitSectionAction): S errorsToShow: [], serverValidationErrors: action.payload.errors || [], isLoading: false, - isValid: isEmpty(action.payload.errors) - } - }) - }) + isValid: isEmpty(action.payload.errors), + }, + }), + }), }); } else { return state; @@ -583,10 +600,10 @@ function setSectionFormId(state: SubmissionObjectState, action: SetSectionFormId sections: Object.assign({}, state[ action.payload.submissionId ].sections, { [ action.payload.sectionId ]: { ...state[ action.payload.submissionId ].sections [action.payload.sectionId], - formId: action.payload.formId - } - }) - }) + formId: action.payload.formId, + }, + }), + }), }); } else { return state; @@ -614,10 +631,10 @@ function updateSectionData(state: SubmissionObjectState, action: UpdateSectionDa data: action.payload.data, errorsToShow: action.payload.errorsToShow, serverValidationErrors: action.payload.serverValidationErrors, - metadata: reduceSectionMetadata(action.payload.metadata, state[ action.payload.submissionId ].sections [ action.payload.sectionId ].metadata) - }) - }) - }) + metadata: reduceSectionMetadata(action.payload.metadata, state[ action.payload.submissionId ].sections [ action.payload.sectionId ].metadata), + }), + }), + }), }); } else { return state; @@ -661,10 +678,10 @@ function changeSectionState(state: SubmissionObjectState, action: EnableSectionA // sections: deleteProperty(state[ action.payload.submissionId ].sections, action.payload.sectionId), sections: Object.assign({}, state[ action.payload.submissionId ].sections, { [ action.payload.sectionId ]: Object.assign({}, state[ action.payload.submissionId ].sections [ action.payload.sectionId ], { - enabled - }) - }) - }) + enabled, + }), + }), + }), }); } else { return state; @@ -688,11 +705,11 @@ function setIsValid(state: SubmissionObjectState, action: SectionStatusChangeAct sections: Object.assign({}, state[ action.payload.submissionId ].sections, Object.assign({}, { [ action.payload.sectionId ]: Object.assign({}, state[ action.payload.submissionId ].sections [ action.payload.sectionId ], { - isValid: action.payload.status - }) - }) - ) - }) + isValid: action.payload.status, + }), + }), + ), + }), }); } else { return state; @@ -716,7 +733,7 @@ function newFile(state: SubmissionObjectState, action: NewUploadedFileAction): S let newData; if (isUndefined(filesData.files)) { newData = { - files: [action.payload.data] + files: [action.payload.data], }; } else { newData = filesData; @@ -728,13 +745,53 @@ function newFile(state: SubmissionObjectState, action: NewUploadedFileAction): S sections: Object.assign({}, state[ action.payload.submissionId ].sections, { [ action.payload.sectionId ]: Object.assign({}, state[ action.payload.submissionId ].sections [ action.payload.sectionId ], { enabled: true, - data: newData - }) - }) - }) + data: newData, + }), + }), + }), }); } +/** + * Edit primary bitstream. + * + * @param state + * the current state + * @param action + * an EditFilePrimaryBitstreamAction action + * @return SubmissionObjectState + * the new state, with the edited file. + */ +function editPrimaryBitstream(state: SubmissionObjectState, action: EditFilePrimaryBitstreamAction): SubmissionObjectState { + const filesData = state[ action.payload.submissionId ].sections[ action.payload.sectionId ].data as WorkspaceitemSectionUploadObject; + const { submissionId, sectionId, fileId } = action.payload; + + const fileIndex = findKey(filesData.files, { uuid: fileId }); + if (isNull(fileIndex)) { + return state; + } + + const submission = state[submissionId]; + return { + ...state, + [submissionId]: { + ...submission, + sections: { + ...submission.sections, + [sectionId]: { + ...submission.sections[sectionId], + data: { + ...submission.sections[sectionId].data as WorkspaceitemSectionUploadObject, + primary: fileId, + }, + }, + }, + isLoading: submission.isLoading, + savePending: submission.savePending, + }, + }; +} + /** * Edit a file. * @@ -761,14 +818,14 @@ function editFileData(state: SubmissionObjectState, action: EditFileDataAction): Object.assign({}, { [ action.payload.sectionId ]: Object.assign({}, state[ action.payload.submissionId ].sections [ action.payload.sectionId ], { data: Object.assign({}, state[ action.payload.submissionId ].sections[ action.payload.sectionId ].data, { - files: newData - }) - }) - }) + files: newData, + }), + }), + }), ), isLoading: state[ action.payload.submissionId ].isLoading, savePending: state[ action.payload.submissionId ].savePending, - }) + }), }); } } @@ -790,7 +847,7 @@ function deleteFile(state: SubmissionObjectState, action: DeleteUploadedFileActi if (hasValue(filesData.files)) { const fileIndex: any = findKey( filesData.files, - {uuid: action.payload.fileId}); + { uuid: action.payload.fileId }); if (isNotNull(fileIndex)) { const newData = Array.from(filesData.files); newData.splice(fileIndex, 1); @@ -800,14 +857,31 @@ function deleteFile(state: SubmissionObjectState, action: DeleteUploadedFileActi Object.assign({}, { [ action.payload.sectionId ]: Object.assign({}, state[ action.payload.submissionId ].sections[ action.payload.sectionId ], { data: Object.assign({}, state[ action.payload.submissionId ].sections[ action.payload.sectionId ].data, { - files: newData - }) - }) - }) - ) - }) + files: newData, + }), + }), + }), + ), + }), }); } } return state; } + +function cleanDuplicateDetectionSection(state: SubmissionObjectState, action: CleanDuplicateDetectionAction): SubmissionObjectState { + if (isNotEmpty(state[ action.payload.submissionId ]) && state[action.payload.submissionId].sections.hasOwnProperty('duplicates')) { + return Object.assign({}, state, { + [ action.payload.submissionId ]: Object.assign({}, state[ action.payload.submissionId ], { + sections: Object.assign({}, state[ action.payload.submissionId ].sections, { + [ 'duplicates' ]: Object.assign({}, state[ action.payload.submissionId ].sections.duplicates, { + enabled: false, + data: { potentialDuplicates: [] }, + }), + }), + }), + }); + } else { + return state; + } +} diff --git a/src/app/submission/objects/submission-section-object.model.ts b/src/app/submission/objects/submission-section-object.model.ts index 16e437da80f..7338539607b 100644 --- a/src/app/submission/objects/submission-section-object.model.ts +++ b/src/app/submission/objects/submission-section-object.model.ts @@ -1,6 +1,6 @@ +import { WorkspaceitemSectionDataType } from '../../core/submission/models/workspaceitem-sections.model'; import { SectionsType } from '../sections/sections-type'; import { SectionVisibility } from './section-visibility.model'; -import { WorkspaceitemSectionDataType } from '../../core/submission/models/workspaceitem-sections.model'; import { SubmissionSectionError } from './submission-section-error.model'; /** diff --git a/src/app/submission/provide-submission-state.ts b/src/app/submission/provide-submission-state.ts new file mode 100644 index 00000000000..3656df6da3d --- /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 c70758a4df4..c49bd74e0d6 100644 --- a/src/app/submission/sections/accesses/section-accesses.component.spec.ts +++ b/src/app/submission/sections/accesses/section-accesses.component.spec.ts @@ -1,45 +1,64 @@ -import { FormService } from '../../../shared/form/form.service'; -import { ComponentFixture, inject, TestBed } from '@angular/core/testing'; - -import { SubmissionSectionAccessesComponent } from './section-accesses.component'; -import { SectionsService } from '../sections.service'; -import { SectionsServiceStub } from '../../../shared/testing/sections-service.stub'; - -import { FormBuilderService } from '../../../shared/form/builder/form-builder.service'; -import { getMockFormBuilderService } from '../../../shared/mocks/form-builder-service.mock'; -import { SubmissionAccessesConfigDataService } from '../../../core/config/submission-accesses-config-data.service'; -import { - getSubmissionAccessesConfigNotChangeDiscoverableService, - getSubmissionAccessesConfigService -} from '../../../shared/mocks/section-accesses-config.service.mock'; -import { SectionAccessesService } from './section-accesses.service'; -import { SectionFormOperationsService } from '../form/section-form-operations.service'; -import { JsonPatchOperationsBuilder } from '../../../core/json-patch/builder/json-patch-operations-builder'; -import { TranslateModule, TranslateService } from '@ngx-translate/core'; -import { - SubmissionJsonPatchOperationsService -} from '../../../core/submission/submission-json-patch-operations.service'; -import { getSectionAccessesService } from '../../../shared/mocks/section-accesses.service.mock'; -import { getMockFormOperationsService } from '../../../shared/mocks/form-operations-service.mock'; -import { getMockTranslateService } from '../../../shared/mocks/translate.service.mock'; +import { CommonModule } from '@angular/common'; import { - SubmissionJsonPatchOperationsServiceStub -} from '../../../shared/testing/submission-json-patch-operations-service.stub'; -import { BrowserModule } from '@angular/platform-browser'; - -import { of as observableOf } from 'rxjs'; -import { Store } from '@ngrx/store'; -import { FormComponent } from '../../../shared/form/form.component'; + ComponentFixture, + TestBed, +} from '@angular/core/testing'; import { + DYNAMIC_FORM_CONTROL_MAP_FN, DynamicCheckboxModel, DynamicDatePickerModel, DynamicFormArrayModel, - DynamicSelectModel + DynamicSelectModel, } from '@ng-dynamic-forms/core'; -import { AppState } from '../../../app.reducer'; +import { Store } from '@ngrx/store'; +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 { 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'; +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 { getSectionAccessesService } from '../../../shared/mocks/section-accesses.service.mock'; +import { + getSubmissionAccessesConfigNotChangeDiscoverableService, + getSubmissionAccessesConfigService, +} from '../../../shared/mocks/section-accesses-config.service.mock'; import { mockAccessesFormData } from '../../../shared/mocks/submission.mock'; -import { accessConditionChangeEvent, checkboxChangeEvent } from '../../../shared/testing/form-event.stub'; +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; @@ -70,12 +89,12 @@ describe('SubmissionSectionAccessesComponent', () => { enabled: true, data: { discoverable: true, - accessConditions: [] + accessConditions: [], }, errorsToShow: [], serverValidationErrors: [], isLoading: false, - isValid: true + isValid: true, }; describe('First with canChangeDiscoverable true', () => { @@ -83,29 +102,37 @@ describe('SubmissionSectionAccessesComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ imports: [ - BrowserModule, - TranslateModule.forRoot() + 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' }, - FormBuilderService - ] + { 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); @@ -114,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(); @@ -144,8 +170,8 @@ describe('SubmissionSectionAccessesComponent', () => { }); it('should have set maxStartDate and maxEndDate properly', () => { - const maxStartDate = {year: 2024, month: 12, day: 20}; - const maxEndDate = {year: 2022, month: 6, day: 20}; + const maxStartDate = { year: 2024, month: 12, day: 20 }; + const maxEndDate = { year: 2022, month: 6, day: 20 }; const startDateModel = formbuilderService.findById('startDate', component.formModel); expect(startDateModel.max).toEqual(maxStartDate); @@ -169,42 +195,49 @@ describe('SubmissionSectionAccessesComponent', () => { describe('when canDescoverable is false', () => { - - beforeEach(async () => { + formService = getMockFormService(); await TestBed.configureTestingModule({ imports: [ - BrowserModule, - TranslateModule.forRoot() + 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 451fc736704..b47f1cf8ff0 100644 --- a/src/app/submission/sections/accesses/section-accesses.component.ts +++ b/src/app/submission/sections/accesses/section-accesses.component.ts @@ -1,16 +1,10 @@ -import { SectionAccessesService } from './section-accesses.service'; -import { Component, Inject, ViewChild } from '@angular/core'; +import { NgIf } from '@angular/common'; +import { + Component, + Inject, + ViewChild, +} from '@angular/core'; import { UntypedFormControl } from '@angular/forms'; - -import { filter, map, mergeMap, take } from 'rxjs/operators'; -import { combineLatest, Observable, of, Subscription } from 'rxjs'; -import { TranslateService } from '@ngx-translate/core'; - -import { renderSectionFor } from '../sections-decorator'; -import { SectionsType } from '../sections-type'; -import { SectionDataObject } from '../models/section-data.model'; -import { SectionsService } from '../sections.service'; -import { SectionModelComponent } from '../models/section.model'; import { DYNAMIC_FORM_CONTROL_TYPE_CHECKBOX, DYNAMIC_FORM_CONTROL_TYPE_DATEPICKER, @@ -22,10 +16,44 @@ import { DynamicFormGroupModel, DynamicSelectModel, MATCH_ENABLED, - OR_OPERATOR + OR_OPERATOR, } 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 { TranslateService } from '@ngx-translate/core'; +import { + combineLatest, + Observable, + of, + Subscription, +} from 'rxjs'; +import { + filter, + map, + mergeMap, + take, +} from 'rxjs/operators'; +import { AccessesConditionOption } from '../../../core/config/models/config-accesses-conditions-options.model'; +import { SubmissionAccessesConfigDataService } from '../../../core/config/submission-accesses-config-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 { getFirstSucceededRemoteData } from '../../../core/shared/operators'; +import { WorkspaceitemSectionAccessesObject } from '../../../core/submission/models/workspaceitem-section-accesses.model'; +import { SubmissionJsonPatchOperationsService } from '../../../core/submission/submission-json-patch-operations.service'; +import { dateToISOFormat } from '../../../shared/date.util'; +import { + hasValue, + isNotEmpty, + isNotNull, +} from '../../../shared/empty.util'; import { FormBuilderService } from '../../../shared/form/builder/form-builder.service'; +import { FormComponent } from '../../../shared/form/form.component'; +import { FormService } from '../../../shared/form/form.service'; +import { SectionFormOperationsService } from '../form/section-form-operations.service'; +import { SectionModelComponent } from '../models/section.model'; +import { SectionDataObject } from '../models/section-data.model'; +import { SectionsService } from '../sections.service'; import { ACCESS_CONDITION_GROUP_CONFIG, ACCESS_CONDITION_GROUP_LAYOUT, @@ -38,26 +66,9 @@ import { FORM_ACCESS_CONDITION_START_DATE_CONFIG, FORM_ACCESS_CONDITION_START_DATE_LAYOUT, FORM_ACCESS_CONDITION_TYPE_CONFIG, - FORM_ACCESS_CONDITION_TYPE_LAYOUT + FORM_ACCESS_CONDITION_TYPE_LAYOUT, } from './section-accesses.model'; -import { hasValue, isNotEmpty, isNotNull } from '../../../shared/empty.util'; -import { - WorkspaceitemSectionAccessesObject -} from '../../../core/submission/models/workspaceitem-section-accesses.model'; -import { SubmissionAccessesConfigDataService } from '../../../core/config/submission-accesses-config-data.service'; -import { getFirstSucceededRemoteData } from '../../../core/shared/operators'; -import { FormComponent } from '../../../shared/form/form.component'; -import { FormService } from '../../../shared/form/form.service'; -import { JsonPatchOperationPathCombiner } from '../../../core/json-patch/builder/json-patch-operation-path-combiner'; -import { SectionFormOperationsService } from '../form/section-form-operations.service'; -import { JsonPatchOperationsBuilder } from '../../../core/json-patch/builder/json-patch-operations-builder'; -import { AccessesConditionOption } from '../../../core/config/models/config-accesses-conditions-options.model'; -import { - SubmissionJsonPatchOperationsService -} from '../../../core/submission/submission-json-patch-operations.service'; -import { dateToISOFormat } from '../../../shared/date.util'; -import { DynamicFormControlCondition } from '@ng-dynamic-forms/core/lib/model/misc/dynamic-form-control-relation.model'; -import { DynamicDateControlValue } from '@ng-dynamic-forms/core/lib/model/dynamic-date-control.model'; +import { SectionAccessesService } from './section-accesses.service'; /** * This component represents a section for managing item's access conditions. @@ -65,9 +76,13 @@ import { DynamicDateControlValue } from '@ng-dynamic-forms/core/lib/model/dynami @Component({ selector: 'ds-section-accesses', templateUrl: './section-accesses.component.html', - styleUrls: ['./section-accesses.component.scss'] + styleUrls: ['./section-accesses.component.scss'], + imports: [ + FormComponent, + NgIf, + ], + standalone: true, }) -@renderSectionFor(SectionsType.AccessesCondition) export class SubmissionSectionAccessesComponent extends SectionModelComponent { /** @@ -163,7 +178,7 @@ export class SubmissionSectionAccessesComponent extends SectionModelComponent { metadataModel.value = { year: date.getUTCFullYear(), month: date.getUTCMonth() + 1, - day: date.getUTCDate() + day: date.getUTCDate(), }; } else { metadataModel.value = accessCondition[key]; @@ -203,7 +218,7 @@ export class SubmissionSectionAccessesComponent extends SectionModelComponent { take(1), filter((isValid) => isValid), mergeMap(() => this.formService.getFormData(this.formId)), - take(1) + take(1), ).subscribe((formData: any) => { const accessConditionsToSave = []; formData.accessCondition @@ -307,10 +322,10 @@ export class SubmissionSectionAccessesComponent extends SectionModelComponent { const discoverableCheckboxConfig = Object.assign({}, ACCESS_FORM_CHECKBOX_CONFIG, { label: this.translate.instant('submission.sections.accesses.form.discoverable-label'), hint: this.translate.instant('submission.sections.accesses.form.discoverable-description'), - value: this.accessesData.discoverable + value: this.accessesData.discoverable, }); formModel.push( - new DynamicCheckboxModel(discoverableCheckboxConfig, ACCESS_FORM_CHECKBOX_LAYOUT) + new DynamicCheckboxModel(discoverableCheckboxConfig, ACCESS_FORM_CHECKBOX_LAYOUT), ); } @@ -322,8 +337,8 @@ export class SubmissionSectionAccessesComponent extends SectionModelComponent { accessConditionTypeOptions.push( { label: accessCondition.name, - value: accessCondition.name - } + value: accessCondition.name, + }, ); } accessConditionTypeModelConfig.options = accessConditionTypeOptions; @@ -342,7 +357,7 @@ export class SubmissionSectionAccessesComponent extends SectionModelComponent { maxStartDate = { year: min.getUTCFullYear(), month: min.getUTCMonth() + 1, - day: min.getUTCDate() + day: min.getUTCDate(), }; } } @@ -353,7 +368,7 @@ export class SubmissionSectionAccessesComponent extends SectionModelComponent { maxEndDate = { year: max.getUTCFullYear(), month: max.getUTCMonth() + 1, - day: max.getUTCDate() + day: max.getUTCDate(), }; } } @@ -391,7 +406,7 @@ export class SubmissionSectionAccessesComponent extends SectionModelComponent { // Number of access conditions blocks in form accessConditionsArrayConfig.initialCount = isNotEmpty(this.accessesData.accessConditions) ? this.accessesData.accessConditions.length : 1; formModel.push( - new DynamicFormArrayModel(accessConditionsArrayConfig, ACCESS_CONDITIONS_FORM_ARRAY_LAYOUT) + new DynamicFormArrayModel(accessConditionsArrayConfig, ACCESS_CONDITIONS_FORM_ARRAY_LAYOUT), ); this.initModelData(formModel); diff --git a/src/app/submission/sections/accesses/section-accesses.model.ts b/src/app/submission/sections/accesses/section-accesses.model.ts index 435c521175a..6b4732b881d 100644 --- a/src/app/submission/sections/accesses/section-accesses.model.ts +++ b/src/app/submission/sections/accesses/section-accesses.model.ts @@ -12,7 +12,7 @@ import { DynamicCheckboxModelConfig } from '@ng-dynamic-forms/core/lib/model/che export const ACCESS_FORM_CHECKBOX_CONFIG: DynamicCheckboxModelConfig = { id: 'discoverable', - name: 'discoverable' + name: 'discoverable', }; export const ACCESS_FORM_CHECKBOX_LAYOUT = { @@ -20,21 +20,21 @@ export const ACCESS_FORM_CHECKBOX_LAYOUT = { element: { container: 'custom-control custom-checkbox pl-1', control: 'custom-control-input', - label: 'custom-control-label pt-1' - } + label: 'custom-control-label pt-1', + }, }; export const ACCESS_CONDITION_GROUP_CONFIG: DynamicFormGroupModelConfig = { id: 'accessConditionGroup', - group: [] + group: [], }; export const ACCESS_CONDITION_GROUP_LAYOUT: DynamicFormControlLayout = { element: { host: 'form-group access-condition-group col', container: 'pl-1 pr-1', - control: 'form-row ' - } + control: 'form-row ', + }, }; export const ACCESS_CONDITIONS_FORM_ARRAY_CONFIG: DynamicFormArrayModelConfig = { @@ -44,20 +44,20 @@ export const ACCESS_CONDITIONS_FORM_ARRAY_CONFIG: DynamicFormArrayModelConfig = export const ACCESS_CONDITIONS_FORM_ARRAY_LAYOUT: DynamicFormControlLayout = { grid: { group: 'form-row pt-4', - } + }, }; export const FORM_ACCESS_CONDITION_TYPE_CONFIG: DynamicSelectModelConfig = { id: 'name', label: 'submission.sections.accesses.form.access-condition-label', hint: 'submission.sections.accesses.form.access-condition-hint', - options: [] + options: [], }; export const FORM_ACCESS_CONDITION_TYPE_LAYOUT: DynamicFormControlLayout = { element: { host: 'col-12', - label: 'col-form-label name-label' - } + label: 'col-form-label name-label', + }, }; export const FORM_ACCESS_CONDITION_START_DATE_CONFIG: DynamicDatePickerModelConfig = { @@ -71,24 +71,24 @@ export const FORM_ACCESS_CONDITION_START_DATE_CONFIG: DynamicDatePickerModelConf { match: MATCH_ENABLED, operator: OR_OPERATOR, - when: [] - } + when: [], + }, ], required: true, validators: { - required: null + required: null, }, errorMessages: { - required: 'submission.sections.accesses.form.date-required-from' - } + required: 'submission.sections.accesses.form.date-required-from', + }, }; export const FORM_ACCESS_CONDITION_START_DATE_LAYOUT: DynamicFormControlLayout = { element: { - label: 'col-form-label' + label: 'col-form-label', }, grid: { - host: 'col-6' - } + host: 'col-6', + }, }; export const FORM_ACCESS_CONDITION_END_DATE_CONFIG: DynamicDatePickerModelConfig = { @@ -102,22 +102,22 @@ export const FORM_ACCESS_CONDITION_END_DATE_CONFIG: DynamicDatePickerModelConfig { match: MATCH_ENABLED, operator: OR_OPERATOR, - when: [] - } + when: [], + }, ], required: true, validators: { - required: null + required: null, }, errorMessages: { - required: 'submission.sections.accesses.form.date-required-until' - } + required: 'submission.sections.accesses.form.date-required-until', + }, }; export const FORM_ACCESS_CONDITION_END_DATE_LAYOUT: DynamicFormControlLayout = { element: { - label: 'col-form-label' + label: 'col-form-label', }, grid: { - host: 'col-6' - } + host: 'col-6', + }, }; diff --git a/src/app/submission/sections/accesses/section-accesses.service.ts b/src/app/submission/sections/accesses/section-accesses.service.ts index f3585821362..4fa3a632d67 100644 --- a/src/app/submission/sections/accesses/section-accesses.service.ts +++ b/src/app/submission/sections/accesses/section-accesses.service.ts @@ -1,18 +1,20 @@ import { Injectable } from '@angular/core'; - -import { Observable } from 'rxjs'; -import { distinctUntilChanged, filter } from 'rxjs/operators'; import { Store } from '@ngrx/store'; +import { Observable } from 'rxjs'; +import { + distinctUntilChanged, + filter, +} from 'rxjs/operators'; -import { SubmissionState } from '../../submission.reducers'; +import { WorkspaceitemSectionAccessesObject } from '../../../core/submission/models/workspaceitem-section-accesses.model'; import { isNotUndefined } from '../../../shared/empty.util'; import { submissionSectionDataFromIdSelector } from '../../selectors'; -import { WorkspaceitemSectionAccessesObject } from '../../../core/submission/models/workspaceitem-section-accesses.model'; +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 0796da5a64a..0c8c1abeeb2 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 @@ -4,7 +4,7 @@ - + {{ getSelectedCcLicense().name }} @@ -20,7 +20,7 @@ - @@ -121,7 +121,7 @@
- +
@@ -134,6 +134,7 @@
{{ 'submission.sections.ccLicense.confirmation' | translate }} 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 bb83e48d212..7b540ecd910 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 @@ -1,23 +1,29 @@ -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { SubmissionSectionCcLicensesComponent } from './submission-section-cc-licenses.component'; -import { SUBMISSION_CC_LICENSE } from '../../../core/submission/models/submission-cc-licence.resource-type'; -import { of as observableOf } from 'rxjs'; -import { SubmissionCcLicenseDataService } from '../../../core/submission/submission-cc-license-data.service'; import { DebugElement } from '@angular/core'; +import { + ComponentFixture, + TestBed, + waitForAsync, +} from '@angular/core/testing'; import { By } from '@angular/platform-browser'; -import { SharedModule } from '../../../shared/shared.module'; -import { SectionsService } from '../sections.service'; -import { SectionDataObject } from '../models/section-data.model'; -import { SectionsType } from '../sections-type'; import { TranslateModule } from '@ngx-translate/core'; -import { SubmissionCcLicence } from '../../../core/submission/models/submission-cc-license.model'; 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'; +import { ConfigurationProperty } from '../../../core/shared/configuration-property.model'; +import { SUBMISSION_CC_LICENSE } from '../../../core/submission/models/submission-cc-licence.resource-type'; +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 { createPaginatedList } from '../../../shared/testing/utils.test'; -import {ConfigurationDataService} from '../../../core/data/configuration-data.service'; -import {ConfigurationProperty} from '../../../core/shared/configuration-property.model'; +import { SectionDataObject } from '../models/section-data.model'; +import { SectionsService } from '../sections.service'; +import { SectionsType } from '../sections-type'; +import { SubmissionSectionCcLicensesComponent } from './submission-section-cc-licenses.component'; describe('SubmissionSectionCcLicensesComponent', () => { @@ -33,7 +39,7 @@ describe('SubmissionSectionCcLicensesComponent', () => { serverValidationErrors: [], header: 'test header', id: 'test section id', - sectionType: SectionsType.SubmissionForm + sectionType: SectionsType.SubmissionForm, }; const submissionCcLicenses: SubmissionCcLicence[] = [ @@ -96,12 +102,12 @@ describe('SubmissionSectionCcLicensesComponent', () => { { id: 'test enum id 2a I', label: 'test enum label 2a I', - description: 'test enum description 2a I' + description: 'test enum description 2a I', }, { id: 'test enum id 2a II', label: 'test enum label 2a II', - description: 'test enum description 2a II' + description: 'test enum description 2a II', }, ], }, @@ -113,12 +119,12 @@ describe('SubmissionSectionCcLicensesComponent', () => { { id: 'test enum id 2b I', label: 'test enum label 2b I', - description: 'test enum description 2b I' + description: 'test enum description 2b I', }, { id: 'test enum id 2b II', label: 'test enum label 2b II', - description: 'test enum description 2b II' + description: 'test enum description 2b II', }, ], }, @@ -139,7 +145,7 @@ describe('SubmissionSectionCcLicensesComponent', () => { getCcLicenseLink: createSuccessfulRemoteDataObject$( { url: 'test cc license link', - } + }, ), }); @@ -150,7 +156,7 @@ describe('SubmissionSectionCcLicensesComponent', () => { setSectionStatus: () => undefined, updateSectionData: (submissionId, sectionId, updatedData) => { component.sectionData.data = updatedData; - } + }, }; const operationsBuilder = jasmine.createSpyObj('operationsBuilder', { @@ -169,10 +175,7 @@ describe('SubmissionSectionCcLicensesComponent', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [ - SharedModule, TranslateModule.forRoot(), - ], - declarations: [ SubmissionSectionCcLicensesComponent, ], providers: [ @@ -184,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(); })); @@ -198,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 ds-select .dropdown-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 ds-select .dropdown-menu button:nth-child(2)')).nativeElement.innerText, ).toContain('test license name 2'); }); @@ -216,7 +227,7 @@ describe('SubmissionSectionCcLicensesComponent', () => { it('should display the selected cc license', () => { expect( - de.query(By.css('.ccLicense-select ds-select button.selection')).nativeElement.innerText + de.query(By.css('.ccLicense-select ds-select button.selection')).nativeElement.innerText, ).toContain('test license name 2'); }); @@ -249,7 +260,7 @@ describe('SubmissionSectionCcLicensesComponent', () => { new Map([ [ccLicence.fields[0], ccLicence.fields[0].enums[1]], [ccLicence.fields[1], ccLicence.fields[1].enums[0]], - ]) + ]), ); }); 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 c8cd898f966..d286a830e2f 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,25 +1,53 @@ -import { Component, Inject } from '@angular/core'; -import { Observable, of as observableOf, Subscription } from 'rxjs'; -import { Field, Option, SubmissionCcLicence } from '../../../core/submission/models/submission-cc-license.model'; +import { + AsyncPipe, + NgForOf, + NgIf, +} from '@angular/common'; +import { + Component, + Inject, +} from '@angular/core'; +import { + NgbModal, + NgbModalRef, +} from '@ng-bootstrap/ng-bootstrap'; +import { TranslateModule } from '@ngx-translate/core'; +import { + Observable, + of as observableOf, + Subscription, +} from 'rxjs'; +import { + distinctUntilChanged, + filter, + map, + take, +} from 'rxjs/operators'; + +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 { getFirstCompletedRemoteData, getFirstSucceededRemoteData, - getRemoteDataPayload + getRemoteDataPayload, } from '../../../core/shared/operators'; -import { distinctUntilChanged, filter, map, take } from 'rxjs/operators'; +import { + Field, + Option, + SubmissionCcLicence, +} from '../../../core/submission/models/submission-cc-license.model'; +import { WorkspaceitemSectionCcLicenseObject } from '../../../core/submission/models/workspaceitem-section-cc-license.model'; import { SubmissionCcLicenseDataService } from '../../../core/submission/submission-cc-license-data.service'; -import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; -import { renderSectionFor } from '../sections-decorator'; -import { SectionsType } from '../sections-type'; +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 { WorkspaceitemSectionCcLicenseObject } from '../../../core/submission/models/workspaceitem-section-cc-license.model'; -import { JsonPatchOperationPathCombiner } from '../../../core/json-patch/builder/json-patch-operation-path-combiner'; -import { isNotEmpty } from '../../../shared/empty.util'; -import { JsonPatchOperationsBuilder } from '../../../core/json-patch/builder/json-patch-operations-builder'; -import { SubmissionCcLicenseUrlDataService } from '../../../core/submission/submission-cc-license-url-data.service'; -import {ConfigurationDataService} from '../../../core/data/configuration-data.service'; +import { SectionsType } from '../sections-type'; /** * This component represents the submission section to select the Creative Commons license. @@ -27,9 +55,18 @@ import {ConfigurationDataService} from '../../../core/data/configuration-data.se @Component({ selector: 'ds-submission-section-cc-licenses', templateUrl: './submission-section-cc-licenses.component.html', - styleUrls: ['./submission-section-cc-licenses.component.scss'] + styleUrls: ['./submission-section-cc-licenses.component.scss'], + imports: [ + TranslateModule, + NgIf, + ThemedLoadingComponent, + AsyncPipe, + VarDirective, + NgForOf, + DsSelectComponent, + ], + standalone: true, }) -@renderSectionFor(SectionsType.CcLicense) export class SubmissionSectionCcLicensesComponent extends SectionModelComponent { /** @@ -96,7 +133,7 @@ export class SubmissionSectionCcLicensesComponent extends SectionModelComponent protected configService: ConfigurationDataService, @Inject('collectionIdProvider') public injectedCollectionId: string, @Inject('sectionDataProvider') public injectedSectionData: SectionDataObject, - @Inject('submissionIdProvider') public injectedSubmissionId: string + @Inject('submissionIdProvider') public injectedSubmissionId: string, ) { super( injectedCollectionId, @@ -154,7 +191,7 @@ export class SubmissionSectionCcLicensesComponent extends SectionModelComponent ccLicense: { id: ccLicense.id, fields: Object.assign({}, this.data.ccLicense.fields, { - [field.id]: option + [field.id]: option, }), }, accepted: false, @@ -188,7 +225,7 @@ export class SubmissionSectionCcLicensesComponent extends SectionModelComponent */ getCcLicenseLink$(): Observable { - if (!!this.storedCcLicenseLink) { + if (this.storedCcLicenseLink) { return observableOf(this.storedCcLicenseLink); } if (!this.getSelectedCcLicense() || this.getSelectedCcLicense().fields.some( @@ -199,7 +236,7 @@ export class SubmissionSectionCcLicensesComponent extends SectionModelComponent return this.submissionCcLicenseUrlDataService.getCcLicenseLink( selectedCcLicense, new Map(selectedCcLicense.fields.map( - (field) => [field, this.getSelectedOption(selectedCcLicense, field)] + (field) => [field, this.getSelectedOption(selectedCcLicense, field)], )), ); } @@ -257,7 +294,7 @@ export class SubmissionSectionCcLicensesComponent extends SectionModelComponent ).subscribe((link) => { this.operationsBuilder.add(path, link.toString(), false, true); }); - } else if (!!this.data.uri) { + } else if (this.data.uri) { this.operationsBuilder.remove(path); } } @@ -268,19 +305,19 @@ export class SubmissionSectionCcLicensesComponent extends SectionModelComponent getRemoteDataPayload(), map((list) => list.page), ).subscribe( - (licenses) => this.submissionCcLicenses = licenses + (licenses) => this.submissionCcLicenses = licenses, ), this.configService.findByPropertyName('cc.license.jurisdiction').pipe( getFirstCompletedRemoteData(), - getRemoteDataPayload() + getRemoteDataPayload(), ).subscribe((remoteData) => { - if (remoteData === undefined || remoteData.values.length === 0) { - // No value configured, use blank value (International jurisdiction) - this.defaultJurisdiction = ''; - } else { - this.defaultJurisdiction = remoteData.values[0]; - } - }) + if (remoteData === undefined || remoteData.values.length === 0) { + // No value configured, use blank value (International jurisdiction) + this.defaultJurisdiction = ''; + } else { + this.defaultJurisdiction = remoteData.values[0]; + } + }), ); } @@ -290,7 +327,7 @@ export class SubmissionSectionCcLicensesComponent extends SectionModelComponent */ setAccepted(accepted: boolean) { this.updateSectionData({ - accepted + accepted, }); this.updateSectionStatus(); } diff --git a/src/app/submission/sections/container/section-container.component.html b/src/app/submission/sections/container/section-container.component.html index e6ae9d1b9c1..3f0050ea51a 100644 --- a/src/app/submission/sections/container/section-container.component.html +++ b/src/app/submission/sections/container/section-container.component.html @@ -9,7 +9,7 @@ 'submission.sections.'+sectionData.header | translate }}
- @@ -48,4 +48,4 @@ -
\ No newline at end of file +
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 d3f4a93762a..4dfa65a13bf 100644 --- a/src/app/submission/sections/container/section-container.component.spec.ts +++ b/src/app/submission/sections/container/section-container.component.spec.ts @@ -1,35 +1,45 @@ // Load the implementations that should be tested -import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; -import { ComponentFixture, inject, TestBed, waitForAsync, } from '@angular/core/testing'; +import { + Component, + CUSTOM_ELEMENTS_SCHEMA, +} from '@angular/core'; +import { + ComponentFixture, + inject, + TestBed, + waitForAsync, +} from '@angular/core/testing'; import { By } from '@angular/platform-browser'; - -import { of as observableOf } from 'rxjs'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { TranslateModule } from '@ngx-translate/core'; +import { of as observableOf } from 'rxjs'; -import { SubmissionSectionContainerComponent } from './section-container.component'; +import { + mockSubmissionCollectionId, + mockSubmissionId, +} from '../../../shared/mocks/submission.mock'; +import { SectionsServiceStub } from '../../../shared/testing/sections-service.stub'; +import { SubmissionServiceStub } from '../../../shared/testing/submission-service.stub'; import { createTestComponent } from '../../../shared/testing/utils.test'; -import { SectionsType } from '../sections-type'; -import { SectionsDirective } from '../sections.directive'; import { SubmissionService } from '../../submission.service'; -import { SectionsService } from '../sections.service'; -import { SubmissionServiceStub } from '../../../shared/testing/submission-service.stub'; -import { SectionsServiceStub } from '../../../shared/testing/sections-service.stub'; import { SectionDataObject } from '../models/section-data.model'; -import { mockSubmissionCollectionId, mockSubmissionId } from '../../../shared/mocks/submission.mock'; +import { SectionsDirective } from '../sections.directive'; +import { SectionsService } from '../sections.service'; +import { SectionsType } from '../sections-type'; +import { SubmissionSectionContainerComponent } from './section-container.component'; const sectionState = { header: 'submit.progressbar.describe.stepone', - config: 'https://rest.api/dspace-spring-rest/api/config/submissionforms/traditionalpageone', - mandatory: true, - sectionType: SectionsType.SubmissionForm, - collapsed: false, - enabled: true, - data: {}, - errorsToShow: [], - serverValidationErrors: [], - isLoading: false, - isValid: false + config: 'https://rest.api/dspace-spring-rest/api/config/submissionforms/traditionalpageone', + mandatory: true, + sectionType: SectionsType.SubmissionForm, + collapsed: false, + enabled: true, + data: {}, + errorsToShow: [], + serverValidationErrors: [], + isLoading: false, + isValid: false, } as any; const sectionObject: SectionDataObject = { @@ -40,7 +50,7 @@ const sectionObject: SectionDataObject = { serverValidationErrors: [], header: 'submit.progressbar.describe.stepone', id: 'traditionalpageone', - sectionType: SectionsType.SubmissionForm + sectionType: SectionsType.SubmissionForm, }; describe('SubmissionSectionContainerComponent test suite', () => { @@ -68,19 +78,17 @@ describe('SubmissionSectionContainerComponent test suite', () => { TestBed.configureTestingModule({ imports: [ NgbModule, - TranslateModule.forRoot() - ], - declarations: [ + TranslateModule.forRoot(), SubmissionSectionContainerComponent, SectionsDirective, TestComponent, - ], // declare the test component + ], providers: [ { provide: SectionsService, useValue: sectionsServiceStub }, { provide: SubmissionService, useValue: submissionServiceStub }, - SubmissionSectionContainerComponent + SubmissionSectionContainerComponent, ], - schemas: [CUSTOM_ELEMENTS_SCHEMA] + schemas: [CUSTOM_ELEMENTS_SCHEMA], }).compileComponents(); })); @@ -226,7 +234,9 @@ describe('SubmissionSectionContainerComponent test suite', () => { @Component({ // eslint-disable-next-line @angular-eslint/component-selector selector: '', - template: `` + 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 8f9ebfbfda7..6f4126a1739 100644 --- a/src/app/submission/sections/container/section-container.component.ts +++ b/src/app/submission/sections/container/section-container.component.ts @@ -1,9 +1,25 @@ -import { Component, Injector, Input, OnInit, ViewChild } from '@angular/core'; +import { + AsyncPipe, + NgClass, + NgComponentOutlet, + NgForOf, + NgIf, +} from '@angular/common'; +import { + Component, + Injector, + Input, + OnInit, + ViewChild, +} from '@angular/core'; +import { NgbAccordionModule } from '@ng-bootstrap/ng-bootstrap'; +import { TranslateModule } from '@ngx-translate/core'; -import { SectionsDirective } from '../sections.directive'; +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'; import { rendersSectionType } from '../sections-decorator'; -import { AlertType } from '../../../shared/alert/aletr-type'; /** * This component represents a section that contains the submission license form. @@ -11,7 +27,19 @@ import { AlertType } from '../../../shared/alert/aletr-type'; @Component({ selector: 'ds-submission-section-container', templateUrl: './section-container.component.html', - styleUrls: ['./section-container.component.scss'] + styleUrls: ['./section-container.component.scss'], + imports: [ + AlertComponent, + NgForOf, + NgbAccordionModule, + NgComponentOutlet, + TranslateModule, + NgClass, + NgIf, + AsyncPipe, + SectionsDirective, + ], + standalone: true, }) export class SubmissionSectionContainerComponent implements OnInit { @@ -68,7 +96,7 @@ export class SubmissionSectionContainerComponent implements OnInit { { provide: 'sectionDataProvider', useFactory: () => (this.sectionData), deps: [] }, { provide: 'submissionIdProvider', useFactory: () => (this.submissionId), deps: [] }, ], - parent: this.injector + parent: this.injector, }); } @@ -87,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.html b/src/app/submission/sections/duplicates/section-duplicates.component.html new file mode 100644 index 00000000000..d9e33a70f7e --- /dev/null +++ b/src/app/submission/sections/duplicates/section-duplicates.component.html @@ -0,0 +1,20 @@ + +
+ +
{{ 'submission.sections.duplicates.none' | translate }}
+
+ +
{{ 'submission.sections.duplicates.detected' | translate }}
+
+ {{dupe.title}} +
+ {{('item.preview.' + metadatum.key) | translate}} {{metadatum.value}} +
+

{{ 'submission.sections.duplicates.in-workspace' | translate }}

+

{{ 'submission.sections.duplicates.in-workflow' | translate }}

+
+
+
diff --git a/src/app/submission/sections/duplicates/section-duplicates.component.spec.ts b/src/app/submission/sections/duplicates/section-duplicates.component.spec.ts new file mode 100644 index 00000000000..501a60e3b82 --- /dev/null +++ b/src/app/submission/sections/duplicates/section-duplicates.component.spec.ts @@ -0,0 +1,266 @@ +import { CommonModule } from '@angular/common'; +import { + ChangeDetectorRef, + Component, + NO_ERRORS_SCHEMA, +} from '@angular/core'; +import { + ComponentFixture, + TestBed, + waitForAsync, +} from '@angular/core/testing'; +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'; +import { NgxPaginationModule } from 'ngx-pagination'; +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 { 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 { License } from '../../../core/shared/license.model'; +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 { + mockSubmissionCollectionId, + mockSubmissionId, +} from '../../../shared/mocks/submission.mock'; +import { defaultUUID } from '../../../shared/mocks/uuid.service.mock'; +import { NotificationsService } from '../../../shared/notifications/notifications.service'; +import { Duplicate } from '../../../shared/object-list/duplicate-data/duplicate.model'; +import { DUPLICATE } from '../../../shared/object-list/duplicate-data/duplicate.resource-type'; +import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; +import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub'; +import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub'; +import { SectionsServiceStub } from '../../../shared/testing/sections-service.stub'; +import { SubmissionServiceStub } from '../../../shared/testing/submission-service.stub'; +import { ObjNgFor } from '../../../shared/utils/object-ngfor.pipe'; +import { VarDirective } from '../../../shared/utils/var.directive'; +import { SubmissionService } from '../../submission.service'; +import { SectionFormOperationsService } from '../form/section-form-operations.service'; +import { SectionsService } from '../sections.service'; +import { SectionsType } from '../sections-type'; +import { SubmissionSectionDuplicatesComponent } from './section-duplicates.component'; + +function getMockSubmissionFormsConfigService(): SubmissionFormsConfigDataService { + return jasmine.createSpyObj('FormOperationsService', { + getConfigAll: jasmine.createSpy('getConfigAll'), + getConfigByHref: jasmine.createSpy('getConfigByHref'), + getConfigByName: jasmine.createSpy('getConfigByName'), + getConfigBySearch: jasmine.createSpy('getConfigBySearch'), + }); +} + +function getMockCollectionDataService(): CollectionDataService { + return jasmine.createSpyObj('CollectionDataService', { + findById: jasmine.createSpy('findById'), + findByHref: jasmine.createSpy('findByHref'), + }); +} + +const duplicates: Duplicate[] = [{ + title: 'Unique title', + uuid: defaultUUID, + workflowItemId: 1, + workspaceItemId: 2, + owningCollection: 'Test Collection', + metadata: { + 'dc.title': [ + Object.assign(new MetadataValue(), { + 'value': 'Unique title', + 'language': null, + 'authority': null, + 'confidence': -1, + 'place': 0, + })], + }, + type: DUPLICATE, + _links: { + self: { + href: 'http://localhost:8080/server/api/core/submission/duplicates/search?uuid=testid', + }, + }, +}]; + +const sectionObject = { + header: 'submission.sections.submit.progressbar.duplicates', + mandatory: true, + opened: true, + data: { potentialDuplicates: duplicates }, + errorsToShow: [], + serverValidationErrors: [], + id: 'duplicates', + sectionType: SectionsType.Duplicates, + sectionVisibility: null, +}; + +describe('SubmissionSectionDuplicatesComponent test suite', () => { + let comp: SubmissionSectionDuplicatesComponent; + let compAsAny: any; + let fixture: ComponentFixture; + let submissionServiceStub: any = new SubmissionServiceStub(); + const sectionsServiceStub: any = new SectionsServiceStub(); + let formService: any; + let formOperationsService: any; + let formBuilderService: any; + let collectionDataService: any; + + const submissionId = mockSubmissionId; + const collectionId = mockSubmissionCollectionId; + const jsonPatchOpBuilder: any = jasmine.createSpyObj('jsonPatchOpBuilder', { + add: jasmine.createSpy('add'), + replace: jasmine.createSpy('replace'), + remove: jasmine.createSpy('remove'), + }); + + const licenseText = 'License text'; + const mockCollection = Object.assign(new Collection(), { + name: 'Community 1-Collection 1', + id: collectionId, + metadata: [ + { + key: 'dc.title', + language: 'en_US', + value: 'Community 1-Collection 1', + }], + license: createSuccessfulRemoteDataObject$(Object.assign(new License(), { text: licenseText })), + }); + const paginationService = new PaginationServiceStub(); + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [ + BrowserModule, + CommonModule, + FormsModule, + ReactiveFormsModule, + NgxPaginationModule, + NoopAnimationsModule, + TranslateModule.forRoot(), + SubmissionSectionDuplicatesComponent, + TestComponent, + ObjNgFor, + VarDirective, + ], + providers: [ + { provide: CollectionDataService, useValue: getMockCollectionDataService() }, + { provide: SectionFormOperationsService, useValue: getMockFormOperationsService() }, + { provide: FormService, useValue: getMockFormService() }, + { provide: JsonPatchOperationsBuilder, useValue: jsonPatchOpBuilder }, + { provide: SubmissionFormsConfigDataService, useValue: getMockSubmissionFormsConfigService() }, + { provide: NotificationsService, useClass: NotificationsServiceStub }, + { provide: SectionsService, useClass: SectionsServiceStub }, + { provide: SubmissionService, useClass: SubmissionServiceStub }, + { provide: 'collectionIdProvider', useValue: collectionId }, + { provide: 'sectionDataProvider', useValue: sectionObject }, + { provide: 'submissionIdProvider', useValue: submissionId }, + { provide: PaginationService, useValue: paginationService }, + ChangeDetectorRef, + { provide: FormBuilderService, useValue: getMockFormBuilderService() }, + ], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents().then(); + })); + + // First test to check the correct component creation + describe('', () => { + let testComp: TestComponent; + let testFixture: ComponentFixture; + + // synchronous beforeEach + beforeEach(() => { + sectionsServiceStub.isSectionReadOnly.and.returnValue(observableOf(false)); + sectionsServiceStub.getSectionErrors.and.returnValue(observableOf([])); + sectionsServiceStub.getSectionData.and.returnValue(observableOf(sectionObject)); + testFixture = TestBed.createComponent(SubmissionSectionDuplicatesComponent); + testComp = testFixture.componentInstance; + + }); + + afterEach(() => { + testFixture.destroy(); + }); + + it('should create SubmissionSectionDuplicatesComponent', () => { + expect(testComp).toBeTruthy(); + }); + }); + + describe('', () => { + beforeEach(() => { + fixture = TestBed.createComponent(SubmissionSectionDuplicatesComponent); + comp = fixture.componentInstance; + compAsAny = comp; + submissionServiceStub = TestBed.inject(SubmissionService); + formService = TestBed.inject(FormService); + formBuilderService = TestBed.inject(FormBuilderService); + formOperationsService = TestBed.inject(SectionFormOperationsService); + collectionDataService = TestBed.inject(CollectionDataService); + compAsAny.pathCombiner = new JsonPatchOperationPathCombiner('sections', sectionObject.id); + }); + + afterEach(() => { + fixture.destroy(); + comp = null; + compAsAny = null; + }); + + // Test initialisation of the submission section + it('Should init section properly', () => { + collectionDataService.findById.and.returnValue(createSuccessfulRemoteDataObject$(mockCollection)); + sectionsServiceStub.getSectionErrors.and.returnValue(observableOf([])); + sectionsServiceStub.isSectionReadOnly.and.returnValue(observableOf(false)); + compAsAny.submissionService.getSubmissionScope.and.returnValue(SubmissionScopeType.WorkspaceItem); + spyOn(comp, 'getSectionStatus').and.returnValue(observableOf(true)); + spyOn(comp, 'getDuplicateData').and.returnValue(observableOf({ potentialDuplicates: duplicates })); + expect(comp.isLoading).toBeTruthy(); + comp.onSectionInit(); + fixture.detectChanges(); + expect(comp.isLoading).toBeFalsy(); + }); + + // The following tests look for proper logic in the getSectionStatus() implementation + // These are very simple as we don't really have a 'false' state unless we're still loading + it('Should return TRUE if the isLoading is FALSE', () => { + compAsAny.isLoading = false; + expect(compAsAny.getSectionStatus()).toBeObservable(cold('(a|)', { + a: true, + })); + }); + it('Should return FALSE', () => { + compAsAny.isLoadin = true; + expect(compAsAny.getSectionStatus()).toBeObservable(cold('(a|)', { + a: false, + })); + }); + }); + +}); + +// declare a test component +@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 new file mode 100644 index 00000000000..885511ca524 --- /dev/null +++ b/src/app/submission/sections/duplicates/section-duplicates.component.ts @@ -0,0 +1,145 @@ +import { + AsyncPipe, + NgForOf, + NgIf, +} from '@angular/common'; +import { + ChangeDetectionStrategy, + Component, + Inject, + OnInit, +} from '@angular/core'; +import { + TranslateModule, + TranslateService, +} from '@ngx-translate/core'; +import { + Observable, + of as observableOf, + Subscription, +} from 'rxjs'; + +import { Metadata } from '../../../core/shared/metadata.utils'; +import { WorkspaceitemSectionDuplicatesObject } from '../../../core/submission/models/workspaceitem-section-duplicates.model'; +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'; + +/** + * Detect duplicates step + * + * @author Kim Shepherd + */ +@Component({ + selector: 'ds-submission-section-duplicates', + templateUrl: './section-duplicates.component.html', + changeDetection: ChangeDetectionStrategy.Default, + imports: [ + VarDirective, + NgIf, + AsyncPipe, + TranslateModule, + NgForOf, + ], + standalone: true, +}) + +export class SubmissionSectionDuplicatesComponent extends SectionModelComponent implements OnInit { + protected readonly Metadata = Metadata; + /** + * The Alert categories. + * @type {AlertType} + */ + public AlertTypeEnum = AlertType; + + /** + * Variable to track if the section is loading. + * @type {boolean} + */ + public isLoading = true; + + /** + * Array to track all subscriptions and unsubscribe them onDestroy + * @type {Array} + */ + protected subs: Subscription[] = []; + + /** + * Initialize instance variables. + * + * @param {TranslateService} translate + * @param {SectionsService} sectionService + * @param {SubmissionService} submissionService + * @param {string} injectedCollectionId + * @param {SectionDataObject} injectedSectionData + * @param {string} injectedSubmissionId + */ + constructor(protected translate: TranslateService, + protected sectionService: SectionsService, + protected submissionService: SubmissionService, + @Inject('collectionIdProvider') public injectedCollectionId: string, + @Inject('sectionDataProvider') public injectedSectionData: SectionDataObject, + @Inject('submissionIdProvider') public injectedSubmissionId: string) { + super(injectedCollectionId, injectedSectionData, injectedSubmissionId); + } + + ngOnInit() { + super.ngOnInit(); + } + + /** + * Initialize all instance variables and retrieve configuration. + */ + onSectionInit() { + this.isLoading = false; + } + + /** + * Check if identifier section has read-only visibility + */ + isReadOnly(): boolean { + return true; + } + + /** + * Unsubscribe from all subscriptions, if needed. + */ + onSectionDestroy(): void { + return; + } + + /** + * Get section status. Because this simple component never requires human interaction, this is basically + * always going to be the opposite of "is this section still loading". This is not the place for API response + * error checking but determining whether the step can 'proceed'. + * + * @return Observable + * the section status + */ + public getSectionStatus(): Observable { + return observableOf(!this.isLoading); + } + + /** + * Get duplicate data as observable from the section data + */ + public getDuplicateData(): Observable { + return this.sectionService.getSectionData(this.submissionId, this.sectionData.id, this.sectionData.sectionType) as + Observable; + } + + /** + * Construct and return an item link for use with a preview item stub + * @param uuid + */ + public getItemLink(uuid: any) { + return new URLCombiner(getItemModuleRoute(), uuid).toString(); + } + + +} 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 65ddbe0cb09..cd169a76ace 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 @@ -1,20 +1,27 @@ -import { TestBed, waitForAsync } from '@angular/core/testing'; - -import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; +import { + TestBed, + waitForAsync, +} from '@angular/core/testing'; import { DYNAMIC_FORM_CONTROL_TYPE_ARRAY, DYNAMIC_FORM_CONTROL_TYPE_GROUP, DynamicFormControlEvent, - DynamicInputModel + DynamicInputModel, } from '@ng-dynamic-forms/core'; +import { + TranslateLoader, + TranslateModule, +} from '@ngx-translate/core'; -import { FormBuilderService } from '../../../shared/form/builder/form-builder.service'; -import { getMockFormBuilderService } from '../../../shared/mocks/form-builder-service.mock'; -import { JsonPatchOperationsBuilder } from '../../../core/json-patch/builder/json-patch-operations-builder'; -import { TranslateLoaderMock } from '../../../shared/mocks/translate-loader.mock'; -import { SectionFormOperationsService } from './section-form-operations.service'; +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'; +import { DynamicRowArrayModel } from '../../../shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-row-array-model'; +import { FormBuilderService } from '../../../shared/form/builder/form-builder.service'; +import { FormFieldMetadataValueObject } from '../../../shared/form/builder/models/form-field-metadata-value.model'; import { FormFieldPreviousValueObject } from '../../../shared/form/builder/models/form-field-previous-value-object'; +import { getMockFormBuilderService } from '../../../shared/mocks/form-builder-service.mock'; import { mockInputWithAuthorityValueModel, mockInputWithFormFieldValueModel, @@ -25,11 +32,10 @@ import { mockQualdropInputModel, MockQualdropModel, MockRelationModel, - mockRowGroupModel + mockRowGroupModel, } from '../../../shared/mocks/form-models.mock'; -import { FormFieldMetadataValueObject } from '../../../shared/form/builder/models/form-field-metadata-value.model'; -import { VocabularyEntry } from '../../../core/submission/vocabularies/models/vocabulary-entry.model'; -import { DynamicRowArrayModel } from '../../../shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-row-array-model'; +import { TranslateLoaderMock } from '../../../shared/mocks/translate-loader.mock'; +import { SectionFormOperationsService } from './section-form-operations.service'; describe('SectionFormOperationsService test suite', () => { let formBuilderService: any; @@ -37,10 +43,10 @@ describe('SectionFormOperationsService test suite', () => { let serviceAsAny: any; const jsonPatchOpBuilder: any = jasmine.createSpyObj('jsonPatchOpBuilder', { - add: jasmine.createSpy('add'), - replace: jasmine.createSpy('replace'), - remove: jasmine.createSpy('remove'), - }); + add: jasmine.createSpy('add'), + replace: jasmine.createSpy('replace'), + remove: jasmine.createSpy('remove'), + }); const pathCombiner = new JsonPatchOperationPathCombiner('sections', 'test'); const dynamicFormControlChangeEvent: DynamicFormControlEvent = { @@ -49,7 +55,7 @@ describe('SectionFormOperationsService test suite', () => { control: null, group: null, model: null, - type: 'change' + type: 'change', }; const dynamicFormControlRemoveEvent: DynamicFormControlEvent = { @@ -58,7 +64,7 @@ describe('SectionFormOperationsService test suite', () => { control: null, group: null, model: null, - type: 'remove' + type: 'remove', }; beforeEach(waitForAsync(() => { @@ -67,15 +73,16 @@ describe('SectionFormOperationsService test suite', () => { TranslateModule.forRoot({ loader: { provide: TranslateLoader, - useClass: TranslateLoaderMock - } - }) + useClass: TranslateLoaderMock, + }, + }), ], providers: [ { provide: FormBuilderService, useValue: getMockFormBuilderService() }, { provide: JsonPatchOperationsBuilder, useValue: jsonPatchOpBuilder }, - SectionFormOperationsService - ] + { provide: APP_DATA_SERVICES_MAP, useValue: {} }, + SectionFormOperationsService, + ], }).compileComponents().then(); })); @@ -114,8 +121,8 @@ describe('SectionFormOperationsService test suite', () => { it('should return the index of the array to which the element belongs', () => { const event = Object.assign({}, dynamicFormControlChangeEvent, { context: { - index: 1 - } + index: 1, + }, }); expect(service.getArrayIndexFromEvent(event)).toBe(1); @@ -126,10 +133,10 @@ describe('SectionFormOperationsService test suite', () => { model: { parent: { parent: { - index: 2 - } - } - } + index: 2, + }, + }, + }, }); spyOn(serviceAsAny, 'isPartOfArrayOfGroup').and.returnValue(true); @@ -157,10 +164,10 @@ describe('SectionFormOperationsService test suite', () => { type: DYNAMIC_FORM_CONTROL_TYPE_GROUP, parent: { context: { - type: DYNAMIC_FORM_CONTROL_TYPE_ARRAY - } - } - } + type: DYNAMIC_FORM_CONTROL_TYPE_ARRAY, + }, + }, + }, }; expect(service.isPartOfArrayOfGroup(model as any)).toBeTruthy(); @@ -168,7 +175,7 @@ describe('SectionFormOperationsService test suite', () => { it('should return false when parent element doesn\'t belong to an array group element', () => { const model = { - parent: null + parent: null, }; expect(service.isPartOfArrayOfGroup(model as any)).toBeFalsy(); @@ -181,19 +188,19 @@ describe('SectionFormOperationsService test suite', () => { const context = { groups: [ { - group: [MockQualdropModel] - } - ] + group: [MockQualdropModel], + }, + ], }; const model = { parent: { parent: { - context: context - } - } + context: context, + }, + }, }; const event = Object.assign({}, dynamicFormControlChangeEvent, { - model: model + model: model, }); const expectMap = new Map(); expectMap.set(MockQualdropModel.qualdropId, [MockQualdropModel.value]); @@ -207,17 +214,17 @@ describe('SectionFormOperationsService test suite', () => { const context = { groups: [ { - group: [MockQualdropModel] - } - ] + group: [MockQualdropModel], + }, + ], }; const model = { parent: { - context: context - } + context: context, + }, }; const event = Object.assign({}, dynamicFormControlChangeEvent, { - model: model + model: model, }); const expectMap = new Map(); expectMap.set(MockQualdropModel.qualdropId, [MockQualdropModel.value]); @@ -247,19 +254,19 @@ describe('SectionFormOperationsService test suite', () => { const context = { groups: [ { - group: [MockQualdropModel] - } - ] + group: [MockQualdropModel], + }, + ], }; const model = { parent: { parent: { - context: context - } - } + context: context, + }, + }, }; const event = Object.assign({}, dynamicFormControlChangeEvent, { - model: model + model: model, }); const expectPath = 'dc.identifier.issn/0'; spyOn(serviceAsAny, 'getArrayIndexFromEvent').and.returnValue(0); @@ -273,17 +280,17 @@ describe('SectionFormOperationsService test suite', () => { const context = { groups: [ { - group: [MockQualdropModel] - } - ] + group: [MockQualdropModel], + }, + ], }; const model = { parent: { - context: context - } + context: context, + }, }; const event = Object.assign({}, dynamicFormControlChangeEvent, { - model: model + model: model, }); const expectPath = 'dc.identifier.issn/0'; spyOn(serviceAsAny, 'getArrayIndexFromEvent').and.returnValue(0); @@ -297,7 +304,7 @@ describe('SectionFormOperationsService test suite', () => { describe('getFieldPathSegmentedFromChangeEvent', () => { it('should return field segmented path properly', () => { const event = Object.assign({}, dynamicFormControlChangeEvent, { - model: mockQualdropInputModel + model: mockQualdropInputModel, }); formBuilderService.isQualdropGroup.and.returnValues(false, false); @@ -306,7 +313,7 @@ describe('SectionFormOperationsService test suite', () => { it('should return field segmented path properly when model is DynamicQualdropModel', () => { const event = Object.assign({}, dynamicFormControlChangeEvent, { - model: MockQualdropModel + model: MockQualdropModel, }); formBuilderService.isQualdropGroup.and.returnValue(true); @@ -316,8 +323,8 @@ describe('SectionFormOperationsService test suite', () => { it('should return field segmented path properly when model belongs to a DynamicQualdropModel', () => { const event = Object.assign({}, dynamicFormControlChangeEvent, { model: { - parent: MockQualdropModel - } + parent: MockQualdropModel, + }, }); formBuilderService.isQualdropGroup.and.returnValues(false, true); @@ -330,8 +337,8 @@ describe('SectionFormOperationsService test suite', () => { it('should return field value properly when model belongs to a DynamicQualdropModel', () => { const event = Object.assign({}, dynamicFormControlChangeEvent, { model: { - parent: MockQualdropModel - } + parent: MockQualdropModel, + }, }); formBuilderService.isModelInCustomGroup.and.returnValue(true); const expectedValue = 'test'; @@ -341,18 +348,18 @@ describe('SectionFormOperationsService test suite', () => { it('should return field value properly when model is DynamicRelationGroupModel', () => { const event = Object.assign({}, dynamicFormControlChangeEvent, { - model: MockRelationModel + model: MockRelationModel, }); formBuilderService.isModelInCustomGroup.and.returnValue(false); formBuilderService.isRelationGroup.and.returnValue(true); const expectedValue = { journal: [ 'journal test 1', - 'journal test 2' + 'journal test 2', ], issue: [ 'issue test 1', - 'issue test 2' + 'issue test 2', ], }; @@ -361,7 +368,7 @@ describe('SectionFormOperationsService test suite', () => { it('should return field value properly when model has language', () => { let event = Object.assign({}, dynamicFormControlChangeEvent, { - model: mockInputWithLanguageModel + model: mockInputWithLanguageModel, }); formBuilderService.isModelInCustomGroup.and.returnValue(false); formBuilderService.isRelationGroup.and.returnValue(false); @@ -370,19 +377,19 @@ describe('SectionFormOperationsService test suite', () => { expect(service.getFieldValueFromChangeEvent(event)).toEqual(expectedValue); event = Object.assign({}, dynamicFormControlChangeEvent, { - model: mockInputWithLanguageAndAuthorityModel + model: mockInputWithLanguageAndAuthorityModel, }); - expectedValue = Object.assign(new VocabularyEntry(), mockInputWithLanguageAndAuthorityModel.value, {language: mockInputWithLanguageAndAuthorityModel.language}); + expectedValue = Object.assign(new VocabularyEntry(), mockInputWithLanguageAndAuthorityModel.value, { language: mockInputWithLanguageAndAuthorityModel.language }); expect(service.getFieldValueFromChangeEvent(event)).toEqual(expectedValue); event = Object.assign({}, dynamicFormControlChangeEvent, { - model: mockInputWithLanguageAndAuthorityArrayModel + model: mockInputWithLanguageAndAuthorityArrayModel, }); expectedValue = [ Object.assign(new VocabularyEntry(), mockInputWithLanguageAndAuthorityArrayModel.value[0], - { language: mockInputWithLanguageAndAuthorityArrayModel.language } - ) + { language: mockInputWithLanguageAndAuthorityArrayModel.language }, + ), ]; expect(service.getFieldValueFromChangeEvent(event)).toEqual(expectedValue); @@ -390,7 +397,7 @@ describe('SectionFormOperationsService test suite', () => { it('should return field value properly when model has an object as value', () => { let event = Object.assign({}, dynamicFormControlChangeEvent, { - model: mockInputWithFormFieldValueModel + model: mockInputWithFormFieldValueModel, }); formBuilderService.isModelInCustomGroup.and.returnValue(false); formBuilderService.isRelationGroup.and.returnValue(false); @@ -399,14 +406,14 @@ describe('SectionFormOperationsService test suite', () => { expect(service.getFieldValueFromChangeEvent(event)).toEqual(expectedValue); event = Object.assign({}, dynamicFormControlChangeEvent, { - model: mockInputWithAuthorityValueModel + model: mockInputWithAuthorityValueModel, }); expectedValue = mockInputWithAuthorityValueModel.value; expect(service.getFieldValueFromChangeEvent(event)).toEqual(expectedValue); event = Object.assign({}, dynamicFormControlChangeEvent, { - model: mockInputWithObjectValueModel + model: mockInputWithObjectValueModel, }); expectedValue = mockInputWithObjectValueModel.value; @@ -461,8 +468,8 @@ describe('SectionFormOperationsService test suite', () => { const previousValue = new FormFieldPreviousValueObject(['path', 'test'], 'value'); const event = Object.assign({}, dynamicFormControlChangeEvent, { model: { - parent: MockQualdropModel - } + parent: MockQualdropModel, + }, }); spyOn(service, 'getFieldPathFromEvent').and.returnValue('path/0'); spyOn(service, 'getFieldPathSegmentedFromChangeEvent').and.returnValue('path'); @@ -481,8 +488,8 @@ describe('SectionFormOperationsService test suite', () => { const previousValue = new FormFieldPreviousValueObject(['path', 'test'], 'value'); const event = Object.assign({}, dynamicFormControlChangeEvent, { model: { - parent: MockRelationModel - } + parent: MockRelationModel, + }, }); spyOn(service, 'getFieldPathFromEvent').and.returnValue('path/0'); spyOn(service, 'getFieldPathSegmentedFromChangeEvent').and.returnValue('path'); @@ -502,8 +509,8 @@ describe('SectionFormOperationsService test suite', () => { const previousValue = new FormFieldPreviousValueObject(['path', 'test'], 'value'); const event = Object.assign({}, dynamicFormControlChangeEvent, { model: { - parent: mockRowGroupModel - } + parent: mockRowGroupModel, + }, }); spyOn(service, 'getFieldPathFromEvent').and.returnValue('path/0'); spyOn(service, 'getFieldPathSegmentedFromChangeEvent').and.returnValue('path'); @@ -523,8 +530,8 @@ describe('SectionFormOperationsService test suite', () => { const previousValue = new FormFieldPreviousValueObject(['path', 'test'], 'value'); const event = Object.assign({}, dynamicFormControlChangeEvent, { model: { - parent: mockRowGroupModel - } + parent: mockRowGroupModel, + }, }); const spyPath = spyOn(service, 'getFieldPathFromEvent').and.returnValue('path/0'); spyOn(service, 'getFieldPathSegmentedFromChangeEvent').and.returnValue('path'); @@ -552,8 +559,8 @@ describe('SectionFormOperationsService test suite', () => { const previousValue = new FormFieldPreviousValueObject(['path', 'test'], 'value'); const event = Object.assign({}, dynamicFormControlChangeEvent, { model: { - parent: mockRowGroupModel - } + parent: mockRowGroupModel, + }, }); spyOn(service, 'getFieldPathFromEvent').and.returnValue('path/0'); spyOn(service, 'getFieldPathSegmentedFromChangeEvent').and.returnValue('path'); @@ -577,8 +584,8 @@ describe('SectionFormOperationsService test suite', () => { let previousValue = new FormFieldPreviousValueObject(['path', 'test'], 'value'); const event = Object.assign({}, dynamicFormControlChangeEvent, { model: { - parent: mockRowGroupModel - } + parent: mockRowGroupModel, + }, }); const spyPath = spyOn(service, 'getFieldPathFromEvent').and.returnValue('path/0'); spyOn(service, 'getFieldPathSegmentedFromChangeEvent').and.returnValue('path'); @@ -607,8 +614,8 @@ describe('SectionFormOperationsService test suite', () => { const previousValue = new FormFieldPreviousValueObject(['path', 'test'], 'value'); const event = Object.assign({}, dynamicFormControlChangeEvent, { model: { - parent: mockRowGroupModel - } + parent: mockRowGroupModel, + }, }); spyOn(service, 'getFieldPathFromEvent').and.returnValue('path/0'); spyOn(service, 'getFieldPathSegmentedFromChangeEvent').and.returnValue('path'); @@ -632,8 +639,8 @@ describe('SectionFormOperationsService test suite', () => { const previousValue = new FormFieldPreviousValueObject(['path', 'test'], null); const event = Object.assign({}, dynamicFormControlChangeEvent, { model: { - parent: mockRowGroupModel - } + parent: mockRowGroupModel, + }, }); spyOn(service, 'getFieldPathFromEvent').and.returnValue('path/0'); spyOn(service, 'getFieldPathSegmentedFromChangeEvent').and.returnValue('path'); @@ -658,8 +665,8 @@ describe('SectionFormOperationsService test suite', () => { const previousValue = new FormFieldPreviousValueObject(['path', 'test'], 'value'); const event = Object.assign({}, dynamicFormControlChangeEvent, { model: { - parent: mockRowGroupModel - } + parent: mockRowGroupModel, + }, }); const spyPath = spyOn(service, 'getFieldPathFromEvent').and.returnValue('path/0'); spyOn(service, 'getFieldPathSegmentedFromChangeEvent').and.returnValue('path'); @@ -693,8 +700,8 @@ describe('SectionFormOperationsService test suite', () => { const previousValue = new FormFieldPreviousValueObject(['path', 'test'], 'value'); const event = Object.assign({}, dynamicFormControlChangeEvent, { model: { - parent: mockRowGroupModel - } + parent: mockRowGroupModel, + }, }); spyOn(service, 'getFieldPathFromEvent').and.returnValue('path/1'); spyOn(service, 'getFieldPathSegmentedFromChangeEvent').and.returnValue('path'); @@ -712,7 +719,7 @@ describe('SectionFormOperationsService test suite', () => { expect(jsonPatchOpBuilder.add).toHaveBeenCalledWith( pathCombiner.getPath('path'), new FormFieldMetadataValueObject('test'), - true + true, ); }); }); @@ -808,7 +815,7 @@ describe('SectionFormOperationsService test suite', () => { isDraggable: true, groupFactory: () => { return [ - new DynamicInputModel({ id: 'testFormRowArrayGroupInput' }) + new DynamicInputModel({ id: 'testFormRowArrayGroupInput' }), ]; }, required: false, @@ -816,8 +823,8 @@ describe('SectionFormOperationsService test suite', () => { metadataFields: ['dc.contributor.author'], hasSelectableMetadata: true, showButtons: true, - typeBindRelations: [] - } + typeBindRelations: [], + }, ); spyOn(serviceAsAny, 'getFieldPathSegmentedFromChangeEvent').and.returnValue('path'); previousValue = new FormFieldPreviousValueObject(['path'], null); @@ -831,7 +838,7 @@ describe('SectionFormOperationsService test suite', () => { pathCombiner, dynamicFormControlChangeEvent, arrayModel, - previousValue + previousValue, ); expect(jsonPatchOpBuilder.add).not.toHaveBeenCalled(); @@ -841,10 +848,10 @@ describe('SectionFormOperationsService test suite', () => { it('should dispatch a json-path add operation when a array value is not empty', () => { const pathValue = [ new FormFieldMetadataValueObject('test'), - new FormFieldMetadataValueObject('test two') + new FormFieldMetadataValueObject('test two'), ]; formBuilderService.getValueFromModel.and.returnValue({ - path:pathValue + path:pathValue, }); spyOn(previousValue, 'isPathEqual').and.returnValue(false); @@ -852,13 +859,13 @@ describe('SectionFormOperationsService test suite', () => { pathCombiner, dynamicFormControlChangeEvent, arrayModel, - previousValue + previousValue, ); expect(jsonPatchOpBuilder.add).toHaveBeenCalledWith( pathCombiner.getPath('path'), pathValue, - false + false, ); expect(jsonPatchOpBuilder.remove).not.toHaveBeenCalled(); }); @@ -871,7 +878,7 @@ describe('SectionFormOperationsService test suite', () => { pathCombiner, dynamicFormControlChangeEvent, arrayModel, - previousValue + previousValue, ); expect(jsonPatchOpBuilder.add).not.toHaveBeenCalled(); 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 778063dd316..25c2ae303da 100644 --- a/src/app/submission/sections/form/section-form-operations.service.ts +++ b/src/app/submission/sections/form/section-form-operations.service.ts @@ -1,36 +1,45 @@ import { Injectable } from '@angular/core'; - -import isEqual from 'lodash/isEqual'; -import isObject from 'lodash/isObject'; import { DYNAMIC_FORM_CONTROL_TYPE_ARRAY, DYNAMIC_FORM_CONTROL_TYPE_GROUP, DynamicFormArrayGroupModel, DynamicFormControlEvent, DynamicFormControlModel, - isDynamicFormControlEvent + isDynamicFormControlEvent, } from '@ng-dynamic-forms/core'; +import { deepClone } from 'fast-json-patch'; +import isEqual from 'lodash/isEqual'; +import isObject from 'lodash/isObject'; -import { hasValue, isNotEmpty, isNotNull, isNotUndefined, isNull, isUndefined } from '../../../shared/empty.util'; import { JsonPatchOperationPathCombiner } from '../../../core/json-patch/builder/json-patch-operation-path-combiner'; -import { FormFieldPreviousValueObject } from '../../../shared/form/builder/models/form-field-previous-value-object'; import { JsonPatchOperationsBuilder } from '../../../core/json-patch/builder/json-patch-operations-builder'; -import { FormFieldLanguageValueObject } from '../../../shared/form/builder/models/form-field-language-value.model'; -import { DsDynamicInputModel } from '../../../shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-input.model'; import { VocabularyEntry } from '../../../core/submission/vocabularies/models/vocabulary-entry.model'; -import { FormBuilderService } from '../../../shared/form/builder/form-builder.service'; -import { FormFieldMetadataValueObject } from '../../../shared/form/builder/models/form-field-metadata-value.model'; -import { DynamicQualdropModel } from '../../../shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-qualdrop.model'; -import { DynamicRelationGroupModel } from '../../../shared/form/builder/ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.model'; import { VocabularyEntryDetail } from '../../../core/submission/vocabularies/models/vocabulary-entry-detail.model'; -import { deepClone } from 'fast-json-patch'; -import { dateToString, isNgbDateStruct } from '../../../shared/date.util'; +import { + dateToString, + isNgbDateStruct, +} from '../../../shared/date.util'; +import { + hasValue, + isNotEmpty, + isNotNull, + isNotUndefined, + isNull, + isUndefined, +} from '../../../shared/empty.util'; +import { DsDynamicInputModel } from '../../../shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-input.model'; +import { DynamicQualdropModel } from '../../../shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-qualdrop.model'; import { DynamicRowArrayModel } from '../../../shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-row-array-model'; +import { DynamicRelationGroupModel } from '../../../shared/form/builder/ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.model'; +import { FormBuilderService } from '../../../shared/form/builder/form-builder.service'; +import { FormFieldLanguageValueObject } from '../../../shared/form/builder/models/form-field-language-value.model'; +import { FormFieldMetadataValueObject } from '../../../shared/form/builder/models/form-field-metadata-value.model'; +import { FormFieldPreviousValueObject } from '../../../shared/form/builder/models/form-field-previous-value-object'; /** * The service handling all form section operations */ -@Injectable() +@Injectable({ providedIn: 'root' }) export class SectionFormOperationsService { /** @@ -57,9 +66,9 @@ export class SectionFormOperationsService { * representing if field value related to the specified operation has stored value */ public dispatchOperationsFromEvent(pathCombiner: JsonPatchOperationPathCombiner, - event: DynamicFormControlEvent, - previousValue: FormFieldPreviousValueObject, - hasStoredValue: boolean): void { + event: DynamicFormControlEvent, + previousValue: FormFieldPreviousValueObject, + hasStoredValue: boolean): void { switch (event.type) { case 'remove': this.dispatchOperationsFromRemoveEvent(pathCombiner, event, previousValue); @@ -83,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)) { @@ -101,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; } } @@ -296,8 +305,8 @@ export class SectionFormOperationsService { * the [[FormFieldPreviousValueObject]] for the specified operation */ protected dispatchOperationsFromRemoveEvent(pathCombiner: JsonPatchOperationPathCombiner, - event: DynamicFormControlEvent, - previousValue: FormFieldPreviousValueObject): void { + event: DynamicFormControlEvent, + previousValue: FormFieldPreviousValueObject): void { const path = this.getFieldPathFromEvent(event); const value = this.getFieldValueFromChangeEvent(event); @@ -321,7 +330,7 @@ export class SectionFormOperationsService { */ protected dispatchOperationsFromAddEvent( pathCombiner: JsonPatchOperationPathCombiner, - event: DynamicFormControlEvent + event: DynamicFormControlEvent, ): void { const path = this.getFieldPathSegmentedFromChangeEvent(event); const value = deepClone(this.getFieldValueFromChangeEvent(event)); @@ -360,11 +369,11 @@ export class SectionFormOperationsService { * representing if field value related to the specified operation has stored value */ protected dispatchOperationsFromChangeEvent(pathCombiner: JsonPatchOperationPathCombiner, - event: DynamicFormControlEvent, - previousValue: FormFieldPreviousValueObject, - hasStoredValue: boolean): void { + event: DynamicFormControlEvent, + previousValue: FormFieldPreviousValueObject, + hasStoredValue: boolean): void { - if (event.context && event.context instanceof DynamicFormArrayGroupModel) { + if (event.context && event.context instanceof DynamicFormArrayGroupModel) { // Model is a DynamicRowArrayModel this.handleArrayGroupPatch(pathCombiner, event, (event as any).context.context, previousValue); return; @@ -399,7 +408,7 @@ export class SectionFormOperationsService { if (isNotEmpty(moveFrom.path) && isNotEmpty(moveTo.path) && moveFrom.path !== moveTo.path) { this.operationsBuilder.move( moveTo, - moveFrom.path + moveFrom.path, ); } } @@ -448,9 +457,9 @@ export class SectionFormOperationsService { * the [[FormFieldPreviousValueObject]] for the specified operation */ protected dispatchOperationsFromMap(valueMap: Map, - pathCombiner: JsonPatchOperationPathCombiner, - event: DynamicFormControlEvent, - previousValue: FormFieldPreviousValueObject): void { + pathCombiner: JsonPatchOperationPathCombiner, + event: DynamicFormControlEvent, + previousValue: FormFieldPreviousValueObject): void { const currentValueMap = valueMap; if (event.type === 'remove') { const path = this.getQualdropItemPathFromEvent(event); @@ -493,8 +502,8 @@ export class SectionFormOperationsService { * the [[FormFieldPreviousValueObject]] for the specified operation */ private dispatchOperationsFromMoveEvent(pathCombiner: JsonPatchOperationPathCombiner, - event: DynamicFormControlEvent, - previousValue: FormFieldPreviousValueObject) { + event: DynamicFormControlEvent, + previousValue: FormFieldPreviousValueObject) { return this.handleArrayGroupPatch(pathCombiner, event.$event, (event as any).$event.arrayModel, previousValue); } @@ -512,9 +521,9 @@ export class SectionFormOperationsService { * the [[FormFieldPreviousValueObject]] for the specified operation */ private handleArrayGroupPatch(pathCombiner: JsonPatchOperationPathCombiner, - event, - model: DynamicRowArrayModel, - previousValue: FormFieldPreviousValueObject) { + event, + model: DynamicRowArrayModel, + previousValue: FormFieldPreviousValueObject) { const arrayValue = this.formBuilder.getValueFromModel([model]); const segmentedPath = this.getFieldPathSegmentedFromChangeEvent(event); @@ -522,7 +531,7 @@ export class SectionFormOperationsService { this.operationsBuilder.add( pathCombiner.getPath(segmentedPath), arrayValue[segmentedPath], - false + false, ); } else if (previousValue.isPathEqual(this.formBuilder.getPath(event.model))) { this.operationsBuilder.remove(pathCombiner.getPath(segmentedPath)); diff --git a/src/app/submission/sections/form/section-form.component.html b/src/app/submission/sections/form/section-form.component.html index 675b550b570..cd7b45bb00c 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 @@ - + { @@ -144,6 +164,7 @@ describe('SubmissionSectionFormComponent test suite', () => { let submissionServiceStub: SubmissionServiceStub; let notificationsServiceStub: NotificationsServiceStub; let formService: any = getMockFormService(); + let themeService = getMockThemeService(); let formOperationsService: any; let formBuilderService: any; @@ -158,16 +179,13 @@ describe('SubmissionSectionFormComponent test suite', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [ - BrowserModule, CommonModule, FormsModule, ReactiveFormsModule, - TranslateModule.forRoot() - ], - declarations: [ + TranslateModule.forRoot(), FormComponent, SubmissionSectionFormComponent, - TestComponent + TestComponent, ], providers: [ { provide: FormBuilderService, useValue: getMockFormBuilderService() }, @@ -176,18 +194,21 @@ 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 }, { provide: SubmissionObjectDataService, useValue: { getHrefByID: () => observableOf('testUrl'), findById: () => createSuccessfulRemoteDataObject$(new WorkspaceItem()) } }, ChangeDetectorRef, - SubmissionSectionFormComponent + SubmissionSectionFormComponent, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents().then(); })); @@ -261,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(); }); @@ -269,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); @@ -281,10 +302,10 @@ describe('SubmissionSectionFormComponent test suite', () => { const sectionData = {}; const sectionError: SubmissionSectionError = { message: 'test' + 'Error: test', - path: '/sections/' + sectionObject.id + path: '/sections/' + sectionObject.id, }; - comp.initForm(sectionData); + comp.initForm(sectionData, [], []); expect(comp.formModel).toBeUndefined(); expect(sectionsServiceStub.setSectionError).toHaveBeenCalledWith(submissionId, sectionObject.id, sectionError); @@ -293,11 +314,11 @@ describe('SubmissionSectionFormComponent test suite', () => { it('should return true when has Metadata Enrichment', () => { const newSectionData = { - 'dc.title': [new FormFieldMetadataValueObject('test')] + 'dc.title': [new FormFieldMetadataValueObject('test')], }; compAsAny.formData = {}; compAsAny.sectionData.data = { - 'dc.title': [new FormFieldMetadataValueObject('test')] + 'dc.title': [new FormFieldMetadataValueObject('test')], }; spyOn(compAsAny, 'inCurrentSubmissionScope').and.callThrough(); @@ -307,11 +328,11 @@ describe('SubmissionSectionFormComponent test suite', () => { it('should return false when has not Metadata Enrichment', () => { const newSectionData = { - 'dc.title': [new FormFieldMetadataValueObject('test')] + 'dc.title': [new FormFieldMetadataValueObject('test')], }; compAsAny.formData = newSectionData; compAsAny.sectionData.data = { - 'dc.title': [new FormFieldMetadataValueObject('test')] + 'dc.title': [new FormFieldMetadataValueObject('test')], }; spyOn(compAsAny, 'inCurrentSubmissionScope').and.callThrough(); @@ -321,7 +342,7 @@ describe('SubmissionSectionFormComponent test suite', () => { it('should return false when metadata has Metadata Enrichment but not belonging to sectionMetadata', () => { const newSectionData = { - 'dc.title': [new FormFieldMetadataValueObject('test')] + 'dc.title': [new FormFieldMetadataValueObject('test')], }; compAsAny.formData = newSectionData; compAsAny.sectionMetadata = []; @@ -338,16 +359,16 @@ describe('SubmissionSectionFormComponent test suite', () => { { selectableMetadata: [{ metadata: 'scoped.workflow' }], scope: 'WORKFLOW', - } as FormFieldModel - ] + } as FormFieldModel, + ], }, { fields: [ { selectableMetadata: [{ metadata: 'scoped.workspace' }], scope: 'WORKSPACE', - } as FormFieldModel - ] + } as FormFieldModel, + ], }, { fields: [ @@ -369,10 +390,10 @@ describe('SubmissionSectionFormComponent test suite', () => { fields: [ { selectableMetadata: [{ metadata: 'dc.title' }], - } as FormFieldModel - ] - } - ] + } as FormFieldModel, + ], + }, + ], }; }); @@ -435,7 +456,7 @@ describe('SubmissionSectionFormComponent test suite', () => { spyOn(comp, 'initForm'); spyOn(comp, 'checksForErrors'); const sectionData: any = { - 'dc.title': [new FormFieldMetadataValueObject('test')] + 'dc.title': [new FormFieldMetadataValueObject('test')], }; const sectionError = []; comp.sectionData.data = {}; @@ -443,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(); @@ -455,15 +476,19 @@ describe('SubmissionSectionFormComponent test suite', () => { it('should update form error properly', () => { spyOn(comp, 'initForm'); spyOn(comp, 'checksForErrors'); - const sectionData: any = { - 'dc.title': [new FormFieldMetadataValueObject('test')] + 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(); @@ -474,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(); @@ -495,7 +521,7 @@ describe('SubmissionSectionFormComponent test suite', () => { sectionObject.id, 'test', parsedSectionErrors, - [] + [], ); expect(comp.sectionData.errorsToShow).toEqual(parsedSectionErrors); }); @@ -504,7 +530,7 @@ describe('SubmissionSectionFormComponent test suite', () => { formService.isValid.and.returnValue(observableOf(true)); sectionsServiceStub.getSectionServerErrors.and.returnValue(observableOf([])); const expected = cold('(b|)', { - b: true + b: true, }); expect(compAsAny.getSectionStatus()).toBeObservable(expected); @@ -514,7 +540,7 @@ describe('SubmissionSectionFormComponent test suite', () => { formService.isValid.and.returnValue(observableOf(true)); sectionsServiceStub.getSectionServerErrors.and.returnValue(observableOf(parsedSectionErrors)); const expected = cold('(b|)', { - b: false + b: false, }); expect(compAsAny.getSectionStatus()).toBeObservable(expected); @@ -524,7 +550,7 @@ describe('SubmissionSectionFormComponent test suite', () => { formService.isValid.and.returnValue(observableOf(false)); sectionsServiceStub.getSectionServerErrors.and.returnValue(observableOf([])); const expected = cold('(b|)', { - b: false + b: false, }); expect(compAsAny.getSectionStatus()).toBeObservable(expected); @@ -533,15 +559,15 @@ describe('SubmissionSectionFormComponent test suite', () => { it('should subscribe to state properly', () => { spyOn(comp, 'updateForm'); const formData = { - 'dc.title': [new FormFieldMetadataValueObject('test')] + 'dc.title': [new FormFieldMetadataValueObject('test')], }; const sectionData: any = { - 'dc.title': [new FormFieldMetadataValueObject('test')] + 'dc.title': [new FormFieldMetadataValueObject('test')], }; const sectionState = { data: sectionData, - errorsToShow: parsedSectionErrors - }; + errorsToShow: parsedSectionErrors, + } as any; formService.getFormData.and.returnValue(observableOf(formData)); sectionsServiceStub.getSectionState.and.returnValue(observableOf(sectionState)); @@ -550,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); }); @@ -617,7 +643,7 @@ describe('SubmissionSectionFormComponent test suite', () => { it('should check if has stored value in the section state', () => { comp.sectionData.data = { - 'dc.title': [new FormFieldMetadataValueObject('test')] + 'dc.title': [new FormFieldMetadataValueObject('test')], } as any; expect(comp.hasStoredValue('dc.title', 0)).toBeTruthy(); @@ -631,8 +657,8 @@ describe('SubmissionSectionFormComponent test suite', () => { // declare a test component @Component({ selector: 'ds-test-cmp', - template: `` + 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 2a07f7e3f17..26285320b05 100644 --- a/src/app/submission/sections/form/section-form.component.ts +++ b/src/app/submission/sections/form/section-form.component.ts @@ -1,45 +1,72 @@ -import { ChangeDetectorRef, Component, Inject, ViewChild } from '@angular/core'; -import { DynamicFormControlEvent, DynamicFormControlModel } from '@ng-dynamic-forms/core'; - -import { combineLatest as observableCombineLatest, Observable, Subscription } from 'rxjs'; -import { distinctUntilChanged, filter, find, map, mergeMap, take, tap } from 'rxjs/operators'; +import { NgIf } from '@angular/common'; +import { + ChangeDetectorRef, + Component, + Inject, + ViewChild, +} from '@angular/core'; +import { + DynamicFormControlEvent, + DynamicFormControlModel, +} from '@ng-dynamic-forms/core'; import { TranslateService } from '@ngx-translate/core'; import findIndex from 'lodash/findIndex'; import isEqual from 'lodash/isEqual'; +import { + combineLatest as observableCombineLatest, + Observable, + Subscription, +} from 'rxjs'; +import { + distinctUntilChanged, + filter, + find, + map, + mergeMap, + take, + tap, +} from 'rxjs/operators'; -import { FormBuilderService } from '../../../shared/form/builder/form-builder.service'; -import { FormComponent } from '../../../shared/form/form.component'; -import { FormService } from '../../../shared/form/form.service'; -import { SectionModelComponent } from '../models/section.model'; +import { environment } from '../../../../environments/environment'; +import { ObjectCacheService } from '../../../core/cache/object-cache.service'; +import { ConfigObject } from '../../../core/config/models/config.model'; +import { FormRowModel } from '../../../core/config/models/config-submission-form.model'; +import { SubmissionFormsModel } from '../../../core/config/models/config-submission-forms.model'; import { SubmissionFormsConfigDataService } from '../../../core/config/submission-forms-config-data.service'; -import { hasValue, isEmpty, isNotEmpty, isUndefined } from '../../../shared/empty.util'; +import { RemoteData } from '../../../core/data/remote-data'; +import { RequestService } from '../../../core/data/request.service'; import { JsonPatchOperationPathCombiner } from '../../../core/json-patch/builder/json-patch-operation-path-combiner'; -import { SubmissionFormsModel } from '../../../core/config/models/config-submission-forms.model'; +import { + getFirstSucceededRemoteData, + getRemoteDataPayload, +} from '../../../core/shared/operators'; +import { SubmissionObject } from '../../../core/submission/models/submission-object.model'; +import { WorkflowItem } from '../../../core/submission/models/workflowitem.model'; +import { WorkspaceItem } from '../../../core/submission/models/workspaceitem.model'; +import { WorkspaceitemSectionFormObject } from '../../../core/submission/models/workspaceitem-section-form.model'; +import { SubmissionObjectDataService } from '../../../core/submission/submission-object-data.service'; +import { SubmissionScopeType } from '../../../core/submission/submission-scope-type'; +import { + hasValue, + isEmpty, + isNotEmpty, + isUndefined, +} from '../../../shared/empty.util'; +import { FormBuilderService } from '../../../shared/form/builder/form-builder.service'; import { FormFieldPreviousValueObject } from '../../../shared/form/builder/models/form-field-previous-value-object'; -import { SectionDataObject } from '../models/section-data.model'; -import { renderSectionFor } from '../sections-decorator'; -import { SectionsType } from '../sections-type'; -import { SubmissionService } from '../../submission.service'; -import { SectionFormOperationsService } from './section-form-operations.service'; +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 { SectionsService } from '../sections.service'; import { difference } from '../../../shared/object.util'; -import { WorkspaceitemSectionFormObject } from '../../../core/submission/models/workspaceitem-section-form.model'; -import { WorkspaceItem } from '../../../core/submission/models/workspaceitem.model'; -import { getFirstSucceededRemoteData, getRemoteDataPayload } from '../../../core/shared/operators'; -import { SubmissionObjectDataService } from '../../../core/submission/submission-object-data.service'; -import { ObjectCacheService } from '../../../core/cache/object-cache.service'; -import { RequestService } from '../../../core/data/request.service'; import { followLink } from '../../../shared/utils/follow-link-config.model'; -import { environment } from '../../../../environments/environment'; -import { ConfigObject } from '../../../core/config/models/config.model'; -import { RemoteData } from '../../../core/data/remote-data'; -import { SubmissionScopeType } from '../../../core/submission/submission-scope-type'; -import { WorkflowItem } from '../../../core/submission/models/workflowitem.model'; -import { SubmissionObject } from '../../../core/submission/models/submission-object.model'; -import { SubmissionSectionObject } from '../../objects/submission-section-object.model'; import { SubmissionSectionError } from '../../objects/submission-section-error.model'; -import { FormRowModel } from '../../../core/config/models/config-submission-form.model'; +import { SubmissionSectionObject } from '../../objects/submission-section-object.model'; +import { SubmissionService } from '../../submission.service'; +import { SectionModelComponent } from '../models/section.model'; +import { SectionDataObject } from '../models/section-data.model'; +import { SectionsService } from '../sections.service'; +import { SectionFormOperationsService } from './section-form-operations.service'; /** * This component represents a section that contains a Form. @@ -48,8 +75,13 @@ import { FormRowModel } from '../../../core/config/models/config-submission-form 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 { /** @@ -183,7 +215,7 @@ export class SubmissionSectionFormComponent extends SectionModelComponent { this.submissionObjectService.findById(this.submissionId, true, false, followLink('item')).pipe( getFirstSucceededRemoteData(), getRemoteDataPayload()), - this.sectionService.isSectionReadOnly(this.submissionId, this.sectionData.id, this.submissionService.getSubmissionScope()) + this.sectionService.isSectionReadOnly(this.submissionId, this.sectionData.id, this.submissionService.getSubmissionScope()), ])), take(1)) .subscribe(([sectionData, submissionObject, isSectionReadOnly]: [WorkspaceitemSectionFormObject, SubmissionObject, boolean]) => { @@ -192,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; @@ -219,11 +251,11 @@ export class SubmissionSectionFormComponent extends SectionModelComponent { protected getSectionStatus(): Observable { const formStatus$ = this.formService.isValid(this.formId); const serverValidationStatus$ = this.sectionService.getSectionServerErrors(this.submissionId, this.sectionData.id).pipe( - map((validationErrors) => isEmpty(validationErrors)) + map((validationErrors) => isEmpty(validationErrors)), ); return observableCombineLatest([formStatus$, serverValidationStatus$]).pipe( - map(([formValidation, serverSideValidation]: [boolean, boolean]) => formValidation && serverSideValidation) + map(([formValidation, serverSideValidation]: [boolean, boolean]) => formValidation && serverSideValidation), ); } @@ -278,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: { @@ -296,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, @@ -304,17 +336,19 @@ export class SubmissionSectionFormComponent extends SectionModelComponent { this.collectionId, sectionData, this.submissionService.getSubmissionScope(), - this.isSectionReadonly + 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 + path: '/sections/' + this.sectionData.id, }; - console.error(e.stack); + if (e instanceof Error) { + console.error(e.stack); + } this.sectionService.setSectionError(this.submissionId, this.sectionData.id, sectionError); } } @@ -322,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; @@ -335,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(); @@ -389,8 +424,8 @@ 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); + }), ); } @@ -416,7 +451,7 @@ export class SubmissionSectionFormComponent extends SectionModelComponent { } private hasRelatedCustomError(medatata): boolean { - const index = findIndex(this.sectionData.errorsToShow, {path: this.pathCombiner.getPath(medatata).path}); + const index = findIndex(this.sectionData.errorsToShow, { path: this.pathCombiner.getPath(medatata).path }); if (index !== -1) { const error = this.sectionData.errorsToShow[index]; const validator = error.message.replace('error.validation.', ''); 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 041e2af23a4..8aa760bb3e3 100644 --- a/src/app/submission/sections/identifiers/section-identifiers.component.spec.ts +++ b/src/app/submission/sections/identifiers/section-identifiers.component.spec.ts @@ -1,59 +1,74 @@ -import { ChangeDetectorRef, Component, NO_ERRORS_SCHEMA } from '@angular/core'; -import { BrowserModule } from '@angular/platform-browser'; import { CommonModule } from '@angular/common'; -import { FormsModule, ReactiveFormsModule } from '@angular/forms'; -import { ComponentFixture, inject, TestBed, waitForAsync } from '@angular/core/testing'; +import { + ChangeDetectorRef, + Component, + NO_ERRORS_SCHEMA, +} from '@angular/core'; +import { + ComponentFixture, + inject, + TestBed, + waitForAsync, +} from '@angular/core/testing'; +import { + FormsModule, + ReactiveFormsModule, +} from '@angular/forms'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; - -import { NgxPaginationModule } from 'ngx-pagination'; +import { TranslateModule } from '@ngx-translate/core'; import { cold } from 'jasmine-marbles'; +import { NgxPaginationModule } from 'ngx-pagination'; import { of as observableOf } from 'rxjs'; -import { TranslateModule } from '@ngx-translate/core'; -import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; -import { createTestComponent } from '../../../shared/testing/utils.test'; -import { NotificationsService } from '../../../shared/notifications/notifications.service'; -import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub'; -import { SubmissionService } from '../../submission.service'; -import { SubmissionServiceStub } from '../../../shared/testing/submission-service.stub'; -import { SectionsService } from '../sections.service'; -import { SectionsServiceStub } from '../../../shared/testing/sections-service.stub'; -import { FormBuilderService } from '../../../shared/form/builder/form-builder.service'; -import { getMockFormOperationsService } from '../../../shared/mocks/form-operations-service.mock'; -import { getMockFormService } from '../../../shared/mocks/form-service.mock'; -import { FormService } from '../../../shared/form/form.service'; import { SubmissionFormsConfigDataService } from '../../../core/config/submission-forms-config-data.service'; -import { SectionDataObject } from '../models/section-data.model'; -import { SectionsType } from '../sections-type'; -import { mockSubmissionCollectionId, mockSubmissionId } from '../../../shared/mocks/submission.mock'; -import { JsonPatchOperationPathCombiner } from '../../../core/json-patch/builder/json-patch-operation-path-combiner'; -import { SubmissionSectionIdentifiersComponent } from './section-identifiers.component'; 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 { SectionFormOperationsService } from '../form/section-form-operations.service'; -import { SubmissionScopeType } from '../../../core/submission/submission-scope-type'; -import { License } from '../../../core/shared/license.model'; +import { PaginationService } from '../../../core/pagination/pagination.service'; import { Collection } from '../../../core/shared/collection.model'; -import { ObjNgFor } from '../../../shared/utils/object-ngfor.pipe'; -import { VarDirective } from '../../../shared/utils/var.directive'; -import { WorkspaceitemSectionIdentifiersObject } from '../../../core/submission/models/workspaceitem-section-identifiers.model'; +import { ConfigurationProperty } from '../../../core/shared/configuration-property.model'; import { Item } from '../../../core/shared/item.model'; -import { PaginationService } from '../../../core/pagination/pagination.service'; +import { License } from '../../../core/shared/license.model'; +import { WorkspaceitemSectionIdentifiersObject } from '../../../core/submission/models/workspaceitem-section-identifiers.model'; +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 { getMockFormOperationsService } from '../../../shared/mocks/form-operations-service.mock'; +import { getMockFormService } from '../../../shared/mocks/form-service.mock'; +import { + mockSubmissionCollectionId, + mockSubmissionId, +} from '../../../shared/mocks/submission.mock'; +import { NotificationsService } from '../../../shared/notifications/notifications.service'; +import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; +import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub'; import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub'; +import { SectionsServiceStub } from '../../../shared/testing/sections-service.stub'; +import { SubmissionServiceStub } from '../../../shared/testing/submission-service.stub'; +import { createTestComponent } from '../../../shared/testing/utils.test'; +import { ObjNgFor } from '../../../shared/utils/object-ngfor.pipe'; +import { VarDirective } from '../../../shared/utils/var.directive'; +import { SubmissionService } from '../../submission.service'; +import { SectionFormOperationsService } from '../form/section-form-operations.service'; +import { SectionDataObject } from '../models/section-data.model'; +import { SectionsService } from '../sections.service'; +import { SectionsType } from '../sections-type'; +import { SubmissionSectionIdentifiersComponent } from './section-identifiers.component'; function getMockSubmissionFormsConfigService(): SubmissionFormsConfigDataService { return jasmine.createSpyObj('FormOperationsService', { getConfigAll: jasmine.createSpy('getConfigAll'), getConfigByHref: jasmine.createSpy('getConfigByHref'), getConfigByName: jasmine.createSpy('getConfigByName'), - getConfigBySearch: jasmine.createSpy('getConfigBySearch') + getConfigBySearch: jasmine.createSpy('getConfigBySearch'), }); } function getMockCollectionDataService(): CollectionDataService { return jasmine.createSpyObj('CollectionDataService', { findById: jasmine.createSpy('findById'), - findByHref: jasmine.createSpy('findByHref') + findByHref: jasmine.createSpy('findByHref'), }); } @@ -64,9 +79,9 @@ const mockItem = Object.assign(new Item(), { 'dc.title': [ { language: null, - value: 'mockmatch' - } - ] + value: 'mockmatch', + }, + ], }, }); @@ -76,16 +91,16 @@ const identifierData: WorkspaceitemSectionIdentifiersObject = { value: 'https://doi.org/10.33515/dspace-61', identifierType: 'doi', identifierStatus: 'TO_BE_REGISTERED', - type: 'identifier' + type: 'identifier', }, { value: '123456789/418', identifierType: 'handle', identifierStatus: null, - type: 'identifier' - } + type: 'identifier', + }, ], - displayTypes: ['doi', 'handle'] + displayTypes: ['doi', 'handle'], }; // Mock section object to use with tests @@ -99,7 +114,7 @@ const sectionObject: SectionDataObject = { header: 'submission.sections.submit.progressbar.identifiers', id: 'identifiers', sectionType: SectionsType.Identifiers, - sectionVisibility: null + sectionVisibility: null, }; describe('SubmissionSectionIdentifiersComponent test suite', () => { @@ -121,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', @@ -129,24 +153,21 @@ describe('SubmissionSectionIdentifiersComponent test suite', () => { { key: 'dc.title', language: 'en_US', - value: 'Community 1-Collection 1' + value: 'Community 1-Collection 1', }], - license: createSuccessfulRemoteDataObject$(Object.assign(new License(), { text: licenseText })) + license: createSuccessfulRemoteDataObject$(Object.assign(new License(), { text: licenseText })), }); const paginationService = new PaginationServiceStub(); beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [ - BrowserModule, CommonModule, FormsModule, ReactiveFormsModule, NgxPaginationModule, NoopAnimationsModule, TranslateModule.forRoot(), - ], - declarations: [ SubmissionSectionIdentifiersComponent, TestComponent, ObjNgFor, @@ -165,11 +186,12 @@ describe('SubmissionSectionIdentifiersComponent test suite', () => { { provide: 'sectionDataProvider', useValue: sectionObject }, { provide: 'submissionIdProvider', useValue: submissionId }, { provide: PaginationService, useValue: paginationService }, + { provide: ConfigurationDataService, useValue: configurationDataService }, ChangeDetectorRef, FormBuilderService, - SubmissionSectionIdentifiersComponent + SubmissionSectionIdentifiersComponent, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents().then(); })); @@ -235,13 +257,13 @@ describe('SubmissionSectionIdentifiersComponent test suite', () => { it('Should return TRUE if the isLoading is FALSE', () => { compAsAny.isLoading = false; expect(compAsAny.getSectionStatus()).toBeObservable(cold('(a|)', { - a: true + a: true, })); }); it('Should return FALSE if the identifier data is missing handle', () => { compAsAny.isLoadin = true; expect(compAsAny.getSectionStatus()).toBeObservable(cold('(a|)', { - a: false + a: false, })); }); }); @@ -251,7 +273,13 @@ describe('SubmissionSectionIdentifiersComponent test suite', () => { // declare a test component @Component({ selector: 'ds-test-cmp', - template: `` + 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 2dc70f668e5..e297c795df4 100644 --- a/src/app/submission/sections/identifiers/section-identifiers.component.ts +++ b/src/app/submission/sections/identifiers/section-identifiers.component.ts @@ -1,15 +1,31 @@ -import {ChangeDetectionStrategy, Component, Inject } from '@angular/core'; +import { + AsyncPipe, + NgForOf, + NgIf, +} from '@angular/common'; +import { + ChangeDetectionStrategy, + Component, + Inject, + OnInit, +} from '@angular/core'; +import { + TranslateModule, + TranslateService, +} from '@ngx-translate/core'; +import { + Observable, + of as observableOf, + Subscription, +} from 'rxjs'; -import { Observable, of as observableOf, Subscription } from 'rxjs'; -import { TranslateService } from '@ngx-translate/core'; -import { SectionsType } from '../sections-type'; +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 { renderSectionFor } from '../sections-decorator'; import { SectionDataObject } from '../models/section-data.model'; -import { SubmissionService } from '../../submission.service'; -import { AlertType } from '../../../shared/alert/aletr-type'; import { SectionsService } from '../sections.service'; -import { WorkspaceitemSectionIdentifiersObject } from '../../../core/submission/models/workspaceitem-section-identifiers.model'; /** * This simple component displays DOI, handle and other identifiers that are already minted for the item in @@ -21,11 +37,18 @@ import { WorkspaceitemSectionIdentifiersObject } from '../../../core/submission/ @Component({ selector: 'ds-submission-section-identifiers', templateUrl: './section-identifiers.component.html', - changeDetection: ChangeDetectionStrategy.Default + 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} @@ -54,7 +77,6 @@ export class SubmissionSectionIdentifiersComponent extends SectionModelComponent /** * Initialize instance variables. * - * @param {PaginationService} paginationService * @param {TranslateService} translate * @param {SectionsService} sectionService * @param {SubmissionService} submissionService @@ -71,8 +93,8 @@ export class SubmissionSectionIdentifiersComponent extends SectionModelComponent super(injectedCollectionId, injectedSectionData, injectedSubmissionId); } - ngOnInit() { - super.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 35fea95b8df..95b2e7f50ab 100644 --- a/src/app/submission/sections/license/section-license.component.spec.ts +++ b/src/app/submission/sections/license/section-license.component.spec.ts @@ -1,43 +1,80 @@ -import { ChangeDetectorRef, Component, NO_ERRORS_SCHEMA } from '@angular/core'; -import { BrowserModule } from '@angular/platform-browser'; import { CommonModule } from '@angular/common'; -import { FormsModule, ReactiveFormsModule } from '@angular/forms'; -import { ComponentFixture, inject, TestBed, waitForAsync } from '@angular/core/testing'; - -import { of as observableOf } from 'rxjs'; +import { + ChangeDetectorRef, + Component, + NO_ERRORS_SCHEMA, +} from '@angular/core'; +import { + ComponentFixture, + inject, + TestBed, + waitForAsync, +} from '@angular/core/testing'; +import { + FormsModule, + ReactiveFormsModule, +} from '@angular/forms'; +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 { DynamicCheckboxModel, DynamicFormControlEvent, DynamicFormControlEventType } from '@ng-dynamic-forms/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 { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; -import { createTestComponent } from '../../../shared/testing/utils.test'; -import { NotificationsService } from '../../../shared/notifications/notifications.service'; -import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub'; -import { SubmissionService } from '../../submission.service'; -import { SubmissionServiceStub } from '../../../shared/testing/submission-service.stub'; -import { SectionsService } from '../sections.service'; -import { SectionsServiceStub } from '../../../shared/testing/sections-service.stub'; +import { SubmissionFormsConfigDataService } from '../../../core/config/submission-forms-config-data.service'; +import { CollectionDataService } from '../../../core/data/collection-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 { 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'; +import { FormService } from '../../../shared/form/form.service'; import { getMockFormOperationsService } from '../../../shared/mocks/form-operations-service.mock'; import { getMockFormService } from '../../../shared/mocks/form-service.mock'; -import { FormService } from '../../../shared/form/form.service'; -import { SubmissionFormsConfigDataService } from '../../../core/config/submission-forms-config-data.service'; -import { SectionDataObject } from '../models/section-data.model'; -import { SectionsType } from '../sections-type'; import { mockLicenseParsedErrors, mockSubmissionCollectionId, - mockSubmissionId + mockSubmissionId, + mockSubmissionObject, } from '../../../shared/mocks/submission.mock'; -import { FormComponent } from '../../../shared/form/form.component'; -import { JsonPatchOperationPathCombiner } from '../../../core/json-patch/builder/json-patch-operation-path-combiner'; -import { SubmissionSectionLicenseComponent } from './section-license.component'; -import { CollectionDataService } from '../../../core/data/collection-data.service'; -import { JsonPatchOperationsBuilder } from '../../../core/json-patch/builder/json-patch-operations-builder'; +import { NotificationsService } from '../../../shared/notifications/notifications.service'; +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'; +import { createTestComponent } from '../../../shared/testing/utils.test'; +import { SubmissionService } from '../../submission.service'; import { SectionFormOperationsService } from '../form/section-form-operations.service'; -import { Collection } from '../../../core/shared/collection.model'; -import { License } from '../../../core/shared/license.model'; -import { FormFieldMetadataValueObject } from '../../../shared/form/builder/models/form-field-metadata-value.model'; -import { cold } from 'jasmine-marbles'; +import { SectionDataObject } from '../models/section-data.model'; +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'; @@ -48,9 +85,9 @@ const mockCollection = Object.assign(new Collection(), { { key: 'dc.title', language: 'en_US', - value: 'Community 1-Collection 1' + value: 'Community 1-Collection 1', }], - license: createSuccessfulRemoteDataObject$(Object.assign(new License(), { text: licenseText })) + license: createSuccessfulRemoteDataObject$(Object.assign(new License(), { text: licenseText })), }); function getMockSubmissionFormsConfigService(): SubmissionFormsConfigDataService { @@ -58,7 +95,7 @@ function getMockSubmissionFormsConfigService(): SubmissionFormsConfigDataService getConfigAll: jasmine.createSpy('getConfigAll'), getConfigByHref: jasmine.createSpy('getConfigByHref'), getConfigByName: jasmine.createSpy('getConfigByName'), - getConfigBySearch: jasmine.createSpy('getConfigBySearch') + getConfigBySearch: jasmine.createSpy('getConfigBySearch'), }); } @@ -68,13 +105,13 @@ const sectionObject: SectionDataObject = { data: { url: null, acceptanceDate: null, - granted: false + granted: false, }, errorsToShow: [], serverValidationErrors: [], header: 'submit.progressbar.describe.license', id: 'license', - sectionType: SectionsType.License + sectionType: SectionsType.License, }; const dynamicFormControlEvent: DynamicFormControlEvent = { @@ -83,7 +120,7 @@ const dynamicFormControlEvent: DynamicFormControlEvent = { control: null, group: null, model: null, - type: DynamicFormControlEventType.Change + type: DynamicFormControlEventType.Change, }; describe('SubmissionSectionLicenseComponent test suite', () => { @@ -108,22 +145,27 @@ describe('SubmissionSectionLicenseComponent test suite', () => { const mockCollectionDataService = jasmine.createSpyObj('CollectionDataService', { findById: jasmine.createSpy('findById'), - findByHref: jasmine.createSpy('findByHref') + 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: [ + TranslateModule.forRoot(), FormComponent, SubmissionSectionLicenseComponent, - TestComponent + TestComponent, ], providers: [ { provide: CollectionDataService, useValue: mockCollectionDataService }, @@ -138,10 +180,22 @@ describe('SubmissionSectionLicenseComponent test suite', () => { { provide: 'sectionDataProvider', useValue: Object.assign({}, sectionObject) }, { provide: 'submissionIdProvider', useValue: submissionId }, ChangeDetectorRef, + provideMockStore({ initialState }), FormBuilderService, - SubmissionSectionLicenseComponent + { 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] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents().then(); })); @@ -198,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); @@ -213,7 +266,7 @@ describe('SubmissionSectionLicenseComponent test suite', () => { expect(model.value).toBeFalsy(); expect(comp.licenseText$).toBeObservable(cold('(ab|)', { a: '', - b: licenseText + b: licenseText, })); }); @@ -221,39 +274,34 @@ describe('SubmissionSectionLicenseComponent test suite', () => { comp.sectionData.data = { url: 'url', acceptanceDate: Date.now(), - granted: true + 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(); expect(comp.licenseText$).toBeObservable(cold('(ab|)', { a: '', - b: licenseText + b: licenseText, })); }); - 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(); }); @@ -271,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(); @@ -283,10 +331,10 @@ describe('SubmissionSectionLicenseComponent test suite', () => { comp.sectionData.data = { url: 'url', acceptanceDate: Date.now(), - granted: true + granted: true, } as any; - comp.onSectionInit(); + fixture.detectChanges(); expect(sectionsServiceStub.dispatchRemoveSectionErrors).toHaveBeenCalled(); @@ -325,7 +373,15 @@ describe('SubmissionSectionLicenseComponent test suite', () => { // declare a test component @Component({ selector: 'ds-test-cmp', - template: `` + 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 e9a0cf15668..86a0455c307 100644 --- a/src/app/submission/sections/license/section-license.component.ts +++ b/src/app/submission/sections/license/section-license.component.ts @@ -1,13 +1,35 @@ -import { ChangeDetectorRef, Component, Inject, ViewChild } from '@angular/core'; +import { + AsyncPipe, + NgIf, +} from '@angular/common'; +import { + AfterViewChecked, + ChangeDetectorRef, + Component, + Inject, + ViewChild, +} from '@angular/core'; import { DynamicCheckboxModel, DynamicFormControlEvent, DynamicFormControlModel, - DynamicFormLayout + DynamicFormLayout, } from '@ng-dynamic-forms/core'; +import { TranslateService } from '@ngx-translate/core'; +import { + Observable, + Subscription, +} from 'rxjs'; +import { + distinctUntilChanged, + filter, + find, + map, + mergeMap, + startWith, + take, +} from 'rxjs/operators'; -import { Observable, Subscription } from 'rxjs'; -import { distinctUntilChanged, filter, find, map, mergeMap, startWith, take } from 'rxjs/operators'; import { CollectionDataService } from '../../../core/data/collection-data.service'; import { RemoteData } from '../../../core/data/remote-data'; import { JsonPatchOperationPathCombiner } from '../../../core/json-patch/builder/json-patch-operation-path-combiner'; @@ -15,21 +37,25 @@ import { JsonPatchOperationsBuilder } from '../../../core/json-patch/builder/jso import { Collection } from '../../../core/shared/collection.model'; import { License } from '../../../core/shared/license.model'; import { WorkspaceitemSectionLicenseObject } from '../../../core/submission/models/workspaceitem-section-license.model'; -import { hasValue, isNotEmpty, isNotNull, isNotUndefined } from '../../../shared/empty.util'; +import { + hasValue, + isNotEmpty, + isNotNull, + isNotUndefined, +} from '../../../shared/empty.util'; import { FormBuilderService } from '../../../shared/form/builder/form-builder.service'; import { FormComponent } from '../../../shared/form/form.component'; import { FormService } from '../../../shared/form/form.service'; import { followLink } from '../../../shared/utils/follow-link-config.model'; import { SubmissionService } from '../../submission.service'; import { SectionFormOperationsService } from '../form/section-form-operations.service'; -import { SectionDataObject } from '../models/section-data.model'; - import { SectionModelComponent } from '../models/section.model'; -import { renderSectionFor } from '../sections-decorator'; -import { SectionsType } from '../sections-type'; +import { SectionDataObject } from '../models/section-data.model'; import { SectionsService } from '../sections.service'; -import { SECTION_LICENSE_FORM_LAYOUT, SECTION_LICENSE_FORM_MODEL } from './section-license.model'; -import { TranslateService } from '@ngx-translate/core'; +import { + SECTION_LICENSE_FORM_LAYOUT, + SECTION_LICENSE_FORM_MODEL, +} from './section-license.model'; /** * This component represents a section that contains the submission license form. @@ -38,9 +64,15 @@ import { TranslateService } from '@ngx-translate/core'; 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 @@ -130,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; @@ -181,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 * @@ -213,6 +250,7 @@ export class SubmissionSectionLicenseComponent extends SectionModelComponent { } else { this.operationsBuilder.remove(this.pathCombiner.getPath(path)); } + this.submissionService.dispatchSaveSection(this.submissionId, this.sectionData.id); } /** diff --git a/src/app/submission/sections/license/section-license.model.ts b/src/app/submission/sections/license/section-license.model.ts index 0f21ef98a43..33f7721ac3d 100644 --- a/src/app/submission/sections/license/section-license.model.ts +++ b/src/app/submission/sections/license/section-license.model.ts @@ -5,9 +5,9 @@ export const SECTION_LICENSE_FORM_LAYOUT = { element: { container: 'custom-control custom-checkbox pl-1', control: 'custom-control-input', - label: 'custom-control-label pt-1' - } - } + label: 'custom-control-label pt-1', + }, + }, }; export const SECTION_LICENSE_FORM_MODEL = [ @@ -17,12 +17,12 @@ export const SECTION_LICENSE_FORM_MODEL = [ required: true, value: false, validators: { - required: null + required: null, }, errorMessages: { required: 'submission.sections.license.required', - notgranted: 'submission.sections.license.notgranted' + notgranted: 'submission.sections.license.notgranted', }, type: 'CHECKBOX', - } + }, ]; diff --git a/src/app/submission/sections/models/section-data.model.ts b/src/app/submission/sections/models/section-data.model.ts index 6f8126dffc0..7ecb820db77 100644 --- a/src/app/submission/sections/models/section-data.model.ts +++ b/src/app/submission/sections/models/section-data.model.ts @@ -1,6 +1,6 @@ import { WorkspaceitemSectionDataType } from '../../../core/submission/models/workspaceitem-sections.model'; -import { SectionsType } from '../sections-type'; import { SubmissionSectionError } from '../../objects/submission-section-error.model'; +import { SectionsType } from '../sections-type'; /** * An interface to represent section model diff --git a/src/app/submission/sections/models/section.model.ts b/src/app/submission/sections/models/section.model.ts index c9be0792134..4868f0c6c13 100644 --- a/src/app/submission/sections/models/section.model.ts +++ b/src/app/submission/sections/models/section.model.ts @@ -1,11 +1,24 @@ -import { Component, Inject, OnDestroy, OnInit } from '@angular/core'; - -import { Observable, Subscription } from 'rxjs'; -import { filter, startWith } from 'rxjs/operators'; - -import { SectionDataObject } from './section-data.model'; +import { + Component, + Inject, + OnDestroy, + OnInit, +} from '@angular/core'; +import { + Observable, + Subscription, +} from 'rxjs'; +import { + filter, + startWith, +} from 'rxjs/operators'; + +import { + hasValue, + isNotUndefined, +} from '../../../shared/empty.util'; import { SectionsService } from '../sections.service'; -import { hasValue, isNotUndefined } from '../../../shared/empty.util'; +import { SectionDataObject } from './section-data.model'; export interface SectionDataModel { sectionData: SectionDataObject; @@ -16,7 +29,7 @@ export interface SectionDataModel { */ @Component({ selector: 'ds-section-model', - template: '' + template: '', }) export abstract class SectionModelComponent implements OnDestroy, OnInit, SectionDataModel { protected abstract sectionService: SectionsService; diff --git a/src/app/submission/sections/section-coar-notify/coar-notify-config-data.service.spec.ts b/src/app/submission/sections/section-coar-notify/coar-notify-config-data.service.spec.ts new file mode 100644 index 00000000000..cfad518b852 --- /dev/null +++ b/src/app/submission/sections/section-coar-notify/coar-notify-config-data.service.spec.ts @@ -0,0 +1,96 @@ +import { + cold, + getTestScheduler, +} from 'jasmine-marbles'; +import { of } from 'rxjs'; +import { TestScheduler } from 'rxjs/testing'; + +import { RemoteDataBuildService } from '../../../core/cache/builders/remote-data-build.service'; +import { ObjectCacheService } from '../../../core/cache/object-cache.service'; +import { RestResponse } from '../../../core/cache/response.models'; +import { CreateData } from '../../../core/data/base/create-data'; +import { testCreateDataImplementation } from '../../../core/data/base/create-data.spec'; +import { DeleteData } from '../../../core/data/base/delete-data'; +import { testDeleteDataImplementation } from '../../../core/data/base/delete-data.spec'; +import { FindAllData } from '../../../core/data/base/find-all-data'; +import { testFindAllDataImplementation } from '../../../core/data/base/find-all-data.spec'; +import { PatchData } from '../../../core/data/base/patch-data'; +import { testPatchDataImplementation } from '../../../core/data/base/patch-data.spec'; +import { RemoteData } from '../../../core/data/remote-data'; +import { RequestService } from '../../../core/data/request.service'; +import { RequestEntry } from '../../../core/data/request-entry.model'; +import { RequestEntryState } from '../../../core/data/request-entry-state.model'; +import { HALEndpointService } from '../../../core/shared/hal-endpoint.service'; +import { NotificationsService } from '../../../shared/notifications/notifications.service'; +import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; +import { CoarNotifyConfigDataService } from './coar-notify-config-data.service'; + +describe('CoarNotifyConfigDataService test', () => { + let scheduler: TestScheduler; + let service: CoarNotifyConfigDataService; + let requestService: RequestService; + let rdbService: RemoteDataBuildService; + let objectCache: ObjectCacheService; + let halService: HALEndpointService; + let notificationsService: NotificationsService; + let responseCacheEntry: RequestEntry; + + const endpointURL = `https://rest.api/rest/api/coar-notify`; + const requestUUID = '8b3c613a-5a4b-438b-9686-be1d5b4a1c5a'; + + const remoteDataMocks = { + Success: new RemoteData(null, null, null, RequestEntryState.Success, null, null, 200), + }; + + function initTestService() { + return new CoarNotifyConfigDataService( + requestService, + rdbService, + objectCache, + halService, + notificationsService, + ); + } + + beforeEach(() => { + scheduler = getTestScheduler(); + + objectCache = {} as ObjectCacheService; + notificationsService = {} as NotificationsService; + responseCacheEntry = new RequestEntry(); + responseCacheEntry.request = { href: 'https://rest.api/' } as any; + responseCacheEntry.response = new RestResponse(true, 200, 'Success'); + + requestService = jasmine.createSpyObj('requestService', { + generateRequestId: requestUUID, + send: true, + removeByHrefSubstring: {}, + getByHref: of(responseCacheEntry), + getByUUID: of(responseCacheEntry), + }); + + halService = jasmine.createSpyObj('halService', { + getEndpoint: of(endpointURL), + }); + + rdbService = jasmine.createSpyObj('rdbService', { + buildSingle: createSuccessfulRemoteDataObject$({}, 500), + buildList: cold('a', { a: remoteDataMocks.Success }), + }); + + + service = initTestService(); + }); + + describe('composition', () => { + const initCreateService = () => new CoarNotifyConfigDataService(null, null, null, null, null) as unknown as CreateData; + const initFindAllService = () => new CoarNotifyConfigDataService(null, null, null, null, null) as unknown as FindAllData; + const initDeleteService = () => new CoarNotifyConfigDataService(null, null, null, null, null) as unknown as DeleteData; + const initPatchService = () => new CoarNotifyConfigDataService(null, null, null, null, null) as unknown as PatchData; + testCreateDataImplementation(initCreateService); + testFindAllDataImplementation(initFindAllService); + testPatchDataImplementation(initPatchService); + testDeleteDataImplementation(initDeleteService); + }); + +}); 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 new file mode 100644 index 00000000000..74b2f0b97ea --- /dev/null +++ b/src/app/submission/sections/section-coar-notify/coar-notify-config-data.service.ts @@ -0,0 +1,126 @@ +import { Injectable } from '@angular/core'; +import { Operation } from 'fast-json-patch'; +import { Observable } from 'rxjs'; +import { + map, + take, +} from 'rxjs/operators'; + +import { RemoteDataBuildService } from '../../../core/cache/builders/remote-data-build.service'; +import { RequestParam } from '../../../core/cache/models/request-param.model'; +import { ObjectCacheService } from '../../../core/cache/object-cache.service'; +import { + CreateData, + CreateDataImpl, +} from '../../../core/data/base/create-data'; +import { + DeleteData, + DeleteDataImpl, +} from '../../../core/data/base/delete-data'; +import { + FindAllData, + FindAllDataImpl, +} from '../../../core/data/base/find-all-data'; +import { IdentifiableDataService } from '../../../core/data/base/identifiable-data.service'; +import { + PatchData, + PatchDataImpl, +} from '../../../core/data/base/patch-data'; +import { ChangeAnalyzer } from '../../../core/data/change-analyzer'; +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 { MultipartPostRequest } from '../../../core/data/request.models'; +import { RequestService } from '../../../core/data/request.service'; +import { RestRequest } from '../../../core/data/rest-request.model'; +import { RestRequestMethod } from '../../../core/data/rest-request-method'; +import { HALEndpointService } from '../../../core/shared/hal-endpoint.service'; +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 { SubmissionCoarNotifyConfig } from './submission-coar-notify.config'; + + +/** + * A service responsible for fetching/sending data from/to the REST API on the CoarNotifyConfig endpoint + */ +@Injectable({ providedIn: 'root' }) +export class CoarNotifyConfigDataService extends IdentifiableDataService implements FindAllData, DeleteData, PatchData, CreateData { + createData: CreateDataImpl; + private findAllData: FindAllDataImpl; + private deleteData: DeleteDataImpl; + private patchData: PatchDataImpl; + private comparator: ChangeAnalyzer; + + constructor( + protected requestService: RequestService, + protected rdbService: RemoteDataBuildService, + protected objectCache: ObjectCacheService, + protected halService: HALEndpointService, + protected notificationsService: NotificationsService, + ) { + super('submissioncoarnotifyconfigs', requestService, rdbService, objectCache, halService); + + this.findAllData = new FindAllDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.responseMsToLive); + this.deleteData = new DeleteDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, notificationsService, this.responseMsToLive, this.constructIdEndpoint); + this.patchData = new PatchDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, this.comparator, this.responseMsToLive, this.constructIdEndpoint); + this.createData = new CreateDataImpl(this.linkPath, requestService, rdbService, objectCache, halService, notificationsService, this.responseMsToLive); + } + + + create(object: SubmissionCoarNotifyConfig, ...params: RequestParam[]): Observable> { + return this.createData.create(object, ...params); + } + + patch(object: SubmissionCoarNotifyConfig, operations: Operation[]): Observable> { + return this.patchData.patch(object, operations); + } + + update(object: SubmissionCoarNotifyConfig): Observable> { + return this.patchData.update(object); + } + + commitUpdates(method?: RestRequestMethod): void { + return this.patchData.commitUpdates(method); + } + + createPatchFromCache(object: SubmissionCoarNotifyConfig): Observable { + return this.patchData.createPatchFromCache(object); + } + + findAll(options?: FindListOptions, useCachedVersionIfAvailable?: boolean, reRequestOnStale?: boolean, ...linksToFollow: FollowLinkConfig[]): Observable>> { + return this.findAllData.findAll(options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow); + } + + + public delete(objectId: string, copyVirtualMetadata?: string[]): Observable> { + return this.deleteData.delete(objectId, copyVirtualMetadata); + } + + public deleteByHref(href: string, copyVirtualMetadata?: string[]): Observable> { + return this.deleteData.deleteByHref(href, copyVirtualMetadata); + } + + public invoke(serviceName: string, serviceId: string, files: File[]): Observable> { + const requestId = this.requestService.generateRequestId(); + this.getBrowseEndpoint().pipe( + take(1), + map((endpoint: string) => new URLCombiner(endpoint, serviceName, 'submissioncoarnotifyconfigmodel', serviceId).toString()), + map((endpoint: string) => { + const body = this.getInvocationFormData(files); + return new MultipartPostRequest(requestId, endpoint, body); + }), + ).subscribe((request: RestRequest) => this.requestService.send(request)); + + return this.rdbService.buildFromRequestUUID(requestId); + } + + private getInvocationFormData(files: File[]): FormData { + const form: FormData = new FormData(); + files.forEach((file: File) => { + form.append('file', file); + }); + return form; + } +} diff --git a/src/app/submission/sections/section-coar-notify/section-coar-notify-service.resource-type.ts b/src/app/submission/sections/section-coar-notify/section-coar-notify-service.resource-type.ts new file mode 100644 index 00000000000..53e41783ced --- /dev/null +++ b/src/app/submission/sections/section-coar-notify/section-coar-notify-service.resource-type.ts @@ -0,0 +1,13 @@ +/** + * The resource type for Ldn-Services + * + * Needs to be in a separate file to prevent circular + * dependencies in webpack. + */ +import { ResourceType } from '../../../core/shared/resource-type'; + + +export const SUBMISSION_COAR_NOTIFY_CONFIG = new ResourceType('submissioncoarnotifyconfig'); + +export const COAR_NOTIFY_WORKSPACEITEM = new ResourceType('workspaceitem'); + diff --git a/src/app/submission/sections/section-coar-notify/section-coar-notify.component.html b/src/app/submission/sections/section-coar-notify/section-coar-notify.component.html new file mode 100644 index 00000000000..49742892caa --- /dev/null +++ b/src/app/submission/sections/section-coar-notify/section-coar-notify.component.html @@ -0,0 +1,149 @@ +
+ +
+
+ +
+
+
+
+
+ + +
+ +
+ +
+ + + {{'submission.section.section-coar-notify.small.notification' | translate : {pattern : ldnPattern.pattern} }} + + + + {{ error.message | translate}} + + +
+
+ +
+
{{ 'submission.section.section-coar-notify.selection.description' | translate }}
+
+ {{ ldnServiceByPattern[ldnPattern.pattern].services[serviceIndex].description }} +
+ + + {{ 'submission.section.section-coar-notify.selection.no-description' | translate }} + + +
+
+
+
+
+
+ + {{ 'submission.section.section-coar-notify.notification.error' | translate }} + +
+
+
+
+ +
+
+
+
+
+
+ +

+ {{ 'submission.section.section-coar-notify.info.no-pattern' | translate }} +

+
+
diff --git a/src/app/submission/sections/section-coar-notify/section-coar-notify.component.scss b/src/app/submission/sections/section-coar-notify/section-coar-notify.component.scss new file mode 100644 index 00000000000..4a3e7072a3b --- /dev/null +++ b/src/app/submission/sections/section-coar-notify/section-coar-notify.component.scss @@ -0,0 +1,5 @@ +// Getting styles for NgbDropdown +@import '../../../shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.scss'; +@import '../../../shared/form/form.component.scss'; + + 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 new file mode 100644 index 00000000000..635b0a9f444 --- /dev/null +++ b/src/app/submission/sections/section-coar-notify/section-coar-notify.component.spec.ts @@ -0,0 +1,445 @@ +import { ChangeDetectorRef } from '@angular/core'; +import { + ComponentFixture, + TestBed, +} from '@angular/core/testing'; +import { NgbDropdown } from '@ng-bootstrap/ng-bootstrap'; +import { TranslateModule } from '@ngx-translate/core'; +import { of } from 'rxjs'; + +import { LdnServicesService } from '../../../admin/admin-ldn-services/ldn-services-data/ldn-services-data.service'; +import { NotifyServicePattern } from '../../../admin/admin-ldn-services/ldn-services-model/ldn-service-patterns.model'; +import { + LdnService, + LdnServiceByPattern, +} from '../../../admin/admin-ldn-services/ldn-services-model/ldn-services.model'; +import { JsonPatchOperationsBuilder } from '../../../core/json-patch/builder/json-patch-operations-builder'; +import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; +import { createPaginatedList } from '../../../shared/testing/utils.test'; +import { SectionsService } from '../sections.service'; +import { CoarNotifyConfigDataService } from './coar-notify-config-data.service'; +import { SubmissionSectionCoarNotifyComponent } from './section-coar-notify.component'; +import { SubmissionCoarNotifyConfig } from './submission-coar-notify.config'; + +describe('SubmissionSectionCoarNotifyComponent', () => { + let component: SubmissionSectionCoarNotifyComponent; + let componentAsAny: any; + let fixture: ComponentFixture; + + let ldnServicesService: jasmine.SpyObj; + let coarNotifyConfigDataService: jasmine.SpyObj; + let operationsBuilder: jasmine.SpyObj; + let sectionService: jasmine.SpyObj; + let cdRefStub: any; + + + const patterns: SubmissionCoarNotifyConfig[] = Object.assign( + [new SubmissionCoarNotifyConfig()], + { + patterns: [{ pattern: 'review', multipleRequest: false }, { pattern: 'endorsment', multipleRequest: false }], + }, + ); + const patternsPL = createPaginatedList(patterns); + const coarNotifyConfig = createSuccessfulRemoteDataObject$(patternsPL); + + beforeEach(async () => { + ldnServicesService = jasmine.createSpyObj('LdnServicesService', [ + 'findByInboundPattern', + ]); + coarNotifyConfigDataService = jasmine.createSpyObj( + 'CoarNotifyConfigDataService', + ['findAll'], + ); + operationsBuilder = jasmine.createSpyObj('JsonPatchOperationsBuilder', [ + 'remove', + 'replace', + 'add', + 'flushOperation', + ]); + sectionService = jasmine.createSpyObj('SectionsService', [ + 'dispatchRemoveSectionErrors', + 'getSectionServerErrors', + 'setSectionError', + ]); + cdRefStub = Object.assign({ + detectChanges: () => fixture.detectChanges(), + }); + + await TestBed.configureTestingModule({ + imports: [TranslateModule.forRoot(), SubmissionSectionCoarNotifyComponent], + providers: [ + { provide: LdnServicesService, useValue: ldnServicesService }, + { provide: CoarNotifyConfigDataService, useValue: coarNotifyConfigDataService }, + { provide: JsonPatchOperationsBuilder, useValue: operationsBuilder }, + { provide: SectionsService, useValue: sectionService }, + { provide: ChangeDetectorRef, useValue: cdRefStub }, + { provide: 'collectionIdProvider', useValue: 'collectionId' }, + { provide: 'sectionDataProvider', useValue: { id: 'sectionId', data: {} } }, + { provide: 'submissionIdProvider', useValue: 'submissionId' }, + NgbDropdown, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(SubmissionSectionCoarNotifyComponent); + component = fixture.componentInstance; + componentAsAny = component; + + component.patterns = patterns[0].patterns; + coarNotifyConfigDataService.findAll.and.returnValue(coarNotifyConfig); + sectionService.getSectionServerErrors.and.returnValue( + of( + Object.assign([], { + path: 'sections/sectionId/data/notifyCoar', + message: 'error', + }), + ), + ); + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + describe('onSectionInit', () => { + it('should call setCoarNotifyConfig and getSectionServerErrorsAndSetErrorsToDisplay', () => { + spyOn(component, 'setCoarNotifyConfig'); + spyOn(componentAsAny, 'getSectionServerErrorsAndSetErrorsToDisplay'); + + component.onSectionInit(); + + expect(component.setCoarNotifyConfig).toHaveBeenCalled(); + expect(componentAsAny.getSectionServerErrorsAndSetErrorsToDisplay).toHaveBeenCalled(); + }); + }); + + describe('onChange', () => { + const ldnPattern = { pattern: 'review', multipleRequest: false }; + const index = 0; + const selectedService: LdnService = Object.assign(new LdnService(), { + id: 1, + name: 'service1', + notifyServiceInboundPatterns: [ + { + pattern: 'review', + }, + ], + description: '', + }); + + beforeEach(() => { + component.ldnServiceByPattern[ldnPattern.pattern] = { + allowsMultipleRequests: false, + services: [], + } as LdnServiceByPattern; + + component.patterns = []; + }); + + it('should do nothing if the selected value is the same as the previous one', () => { + + component.ldnServiceByPattern[ldnPattern.pattern].services[index] = selectedService; + component.onChange(ldnPattern.pattern, index, selectedService); + + expect(componentAsAny.operationsBuilder.remove).not.toHaveBeenCalled(); + expect(componentAsAny.operationsBuilder.replace).not.toHaveBeenCalled(); + expect(componentAsAny.operationsBuilder.add).not.toHaveBeenCalled(); + }); + + it('should remove the path when the selected value is null', () => { + component.ldnServiceByPattern[ldnPattern.pattern].services[index] = selectedService; + component.onChange(ldnPattern.pattern, index, null); + + expect(componentAsAny.operationsBuilder.flushOperation).toHaveBeenCalledWith( + componentAsAny.pathCombiner.getPath([ldnPattern.pattern, '-']), + ); + expect(component.ldnServiceByPattern[ldnPattern.pattern].services[index]).toBeNull(); + expect(component.previousServices[ldnPattern.pattern].services[index]).toBeNull(); + }); + + it('should replace the path when there is a previous value stored and it is different from the new one', () => { + const previousService: LdnService = Object.assign(new LdnService(), { + id: 2, + name: 'service2', + notifyServiceInboundPatterns: [ + { + pattern: 'endorsement', + }, + ], + description: 'test', + }); + component.ldnServiceByPattern[ldnPattern.pattern].services[index] = previousService; + component.previousServices[ldnPattern.pattern] = { + allowsMultipleRequests: false, + services: [previousService], + } as LdnServiceByPattern; + + component.onChange(ldnPattern.pattern, index, selectedService); + + expect(componentAsAny.operationsBuilder.add).toHaveBeenCalledWith( + componentAsAny.pathCombiner.getPath([ldnPattern.pattern, '-']), + [selectedService.id], + false, + true, + ); + expect(component.ldnServiceByPattern[ldnPattern.pattern].services[index]).toEqual( + selectedService, + ); + expect(component.previousServices[ldnPattern.pattern].services[index].id).toEqual( + selectedService.id, + ); + }); + + it('should add the path when there is no previous value stored', () => { + component.onChange(ldnPattern.pattern, index, selectedService); + + expect(componentAsAny.operationsBuilder.add).toHaveBeenCalledWith( + componentAsAny.pathCombiner.getPath([ldnPattern.pattern, '-']), + [selectedService.id], + false, + true, + ); + expect(component.ldnServiceByPattern[ldnPattern.pattern].services[index]).toEqual( + selectedService, + ); + expect(component.previousServices[ldnPattern.pattern].services[index].id).toEqual( + selectedService.id, + ); + }); + }); + + describe('initSelectedServicesByPattern', () => { + const pattern1 = { pattern: 'review', multipleRequest: false }; + const pattern2 = { pattern: 'endorsement', multipleRequest: false }; + const service1: LdnService = Object.assign(new LdnService(), { + id: 1, + uuid: 1, + name: 'service1', + notifyServiceInboundPatterns: [ + Object.assign(new NotifyServicePattern(), { + pattern: pattern1, + }), + ], + }); + const service2: LdnService = Object.assign(new LdnService(), { + id: 2, + uuid: 2, + name: 'service2', + notifyServiceInboundPatterns: [ + Object.assign(new NotifyServicePattern(), { + pattern: pattern2, + }), + ], + }); + const service3: LdnService = Object.assign(new LdnService(), { + id: 3, + uuid: 3, + name: 'service3', + notifyServiceInboundPatterns: [ + Object.assign(new NotifyServicePattern(), { + pattern: pattern1, + }), + Object.assign(new NotifyServicePattern(), { + pattern: pattern2, + }), + ], + }); + + const services = [service1, service2, service3]; + + beforeEach(() => { + ldnServicesService.findByInboundPattern.and.returnValue( + createSuccessfulRemoteDataObject$(createPaginatedList(services)), + ); + component.ldnServiceByPattern[pattern1.pattern] = { + allowsMultipleRequests: false, + services: [], + } as LdnServiceByPattern; + + component.ldnServiceByPattern[pattern2.pattern] = { + allowsMultipleRequests: false, + services: [], + } as LdnServiceByPattern; + + component.patterns = [pattern1, pattern2]; + + spyOn(component, 'filterServices').and.callFake((pattern) => { + return of(services); + }); + }); + + it('should initialize the selected services by pattern', () => { + component.initSelectedServicesByPattern(); + + expect(component.ldnServiceByPattern[pattern1.pattern].services).toEqual([null]); + expect(component.ldnServiceByPattern[pattern2.pattern].services).toEqual([null]); + }); + + it('should add the service to the selected services by pattern if the section data has a value for the pattern', () => { + component.sectionData.data[pattern1.pattern] = [service1.uuid, service3.uuid]; + component.sectionData.data[pattern2.pattern] = [service2.uuid, service3.uuid]; + component.initSelectedServicesByPattern(); + + expect(component.ldnServiceByPattern[pattern1.pattern].services).toEqual([ + service1, + service3, + ]); + expect(component.ldnServiceByPattern[pattern2.pattern].services).toEqual([ + service2, + service3, + ]); + }); + }); + + describe('addService', () => { + const ldnPattern = { pattern: 'review', multipleRequest: false }; + const service: any = { + id: 1, + name: 'service1', + notifyServiceInboundPatterns: [{ pattern: ldnPattern.pattern }], + }; + + beforeEach(() => { + component.ldnServiceByPattern[ldnPattern.pattern] = { + allowsMultipleRequests: false, + services: [], + } as LdnServiceByPattern; + }); + + it('should push the new service to the array corresponding to the pattern', () => { + component.addService(ldnPattern, service); + + expect(component.ldnServiceByPattern[ldnPattern.pattern].services).toEqual([service]); + }); + }); + + describe('removeService', () => { + const ldnPattern = { pattern: 'review', multipleRequest: false }; + const service1: LdnService = Object.assign(new LdnService(), { + id: 1, + name: 'service1', + notifyServiceInboundPatterns: [ + Object.assign(new NotifyServicePattern(), { + pattern: ldnPattern.pattern, + }), + ], + }); + const service2: LdnService = Object.assign(new LdnService(), { + id: 2, + name: 'service2', + notifyServiceInboundPatterns: [ + Object.assign(new NotifyServicePattern(), { + pattern: ldnPattern.pattern, + }), + ], + }); + const service3: LdnService = Object.assign(new LdnService(), { + id: 3, + name: 'service3', + notifyServiceInboundPatterns: [ + Object.assign(new NotifyServicePattern(), { + pattern: ldnPattern.pattern, + }), + ], + }); + + beforeEach(() => { + component.ldnServiceByPattern[ldnPattern.pattern] = { + allowsMultipleRequests: false, + services: [], + } as LdnServiceByPattern; + }); + + + it('should remove the service at the specified index from the array corresponding to the pattern', () => { + component.ldnServiceByPattern[ldnPattern.pattern].services = [service1, service2, service3]; + component.removeService(ldnPattern, 1); + + expect(component.ldnServiceByPattern[ldnPattern.pattern].services).toEqual([ + service1, + service3, + ]); + }); + }); + + describe('filterServices', () => { + const pattern = 'review'; + const service1: any = { + id: 1, + name: 'service1', + notifyServiceInboundPatterns: [{ pattern: pattern }], + }; + const service2: any = { + id: 2, + name: 'service2', + notifyServiceInboundPatterns: [{ pattern: pattern }], + }; + const service3: any = { + id: 3, + name: 'service3', + notifyServiceInboundPatterns: [{ pattern: pattern }], + }; + const services = [service1, service2, service3]; + + beforeEach(() => { + ldnServicesService.findByInboundPattern.and.returnValue( + createSuccessfulRemoteDataObject$(createPaginatedList(services)), + ); + }); + + it('should return an observable of the services that match the given pattern', () => { + component.filterServices(pattern).subscribe((result) => { + expect(result).toEqual(services); + }); + }); + }); + + describe('hasInboundPattern', () => { + const pattern = 'review'; + const service: any = { + id: 1, + name: 'service1', + notifyServiceInboundPatterns: [{ pattern: pattern }], + }; + + it('should return true if the service has the specified inbound pattern type', () => { + expect(component.hasInboundPattern(service, pattern)).toBeTrue(); + }); + + it('should return false if the service does not have the specified inbound pattern type', () => { + expect(component.hasInboundPattern(service, 'endorsement')).toBeFalse(); + }); + }); + + describe('getSectionServerErrorsAndSetErrorsToDisplay', () => { + it('should set the validation errors for the current section to display', () => { + const validationErrors = [ + { path: 'sections/sectionId/data/notifyCoar', message: 'error' }, + ]; + sectionService.getSectionServerErrors.and.returnValue( + of(validationErrors), + ); + + componentAsAny.getSectionServerErrorsAndSetErrorsToDisplay(); + + expect(sectionService.setSectionError).toHaveBeenCalledWith( + component.submissionId, + component.sectionData.id, + validationErrors[0], + ); + }); + }); + + describe('onSectionDestroy', () => { + it('should unsubscribe from all subscriptions', () => { + const sub1 = of(null).subscribe(); + const sub2 = of(null).subscribe(); + componentAsAny.subs = [sub1, sub2]; + spyOn(sub1, 'unsubscribe'); + spyOn(sub2, 'unsubscribe'); + component.onSectionDestroy(); + expect(sub1.unsubscribe).toHaveBeenCalled(); + expect(sub2.unsubscribe).toHaveBeenCalled(); + }); + }); +}); 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 new file mode 100644 index 00000000000..c47e4c644de --- /dev/null +++ b/src/app/submission/sections/section-coar-notify/section-coar-notify.component.ts @@ -0,0 +1,377 @@ +import { + AsyncPipe, + NgClass, + NgForOf, + NgIf, +} from '@angular/common'; +import { + ChangeDetectorRef, + Component, + Inject, +} from '@angular/core'; +import { + NgbDropdown, + NgbDropdownModule, +} from '@ng-bootstrap/ng-bootstrap'; +import { TranslateModule } from '@ngx-translate/core'; +import { InfiniteScrollModule } from 'ngx-infinite-scroll'; +import { + Observable, + Subscription, +} from 'rxjs'; +import { + filter, + map, + take, + tap, +} from 'rxjs/operators'; + +import { LdnServicesService } from '../../../admin/admin-ldn-services/ldn-services-data/ldn-services-data.service'; +import { + LdnService, + LdnServiceByPattern, +} from '../../../admin/admin-ldn-services/ldn-services-model/ldn-services.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, + getPaginatedListPayload, + getRemoteDataPayload, +} from '../../../core/shared/operators'; +import { + hasValue, + isEmpty, + isNotEmpty, +} from '../../../shared/empty.util'; +import { SubmissionSectionError } from '../../objects/submission-section-error.model'; +import { SectionModelComponent } from '../models/section.model'; +import { SectionDataObject } from '../models/section-data.model'; +import { SectionsService } from '../sections.service'; +import { CoarNotifyConfigDataService } from './coar-notify-config-data.service'; +import { LdnPattern } from './submission-coar-notify.config'; + +/** + * This component represents a section that contains the submission section-coar-notify form. + */ +@Component({ + 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], +}) +export class SubmissionSectionCoarNotifyComponent extends SectionModelComponent { + + hasSectionData = false; + /** + * Contains an array of string patterns. + */ + patterns: LdnPattern[] = []; + /** + * An object that maps string keys to arrays of LdnService objects. + * Used to store LdnService objects by pattern. + */ + ldnServiceByPattern: { [key: string]: LdnServiceByPattern } = {}; + /** + * A map representing all services for each pattern + * { + * 'pattern': { + * 'index': 'service.id' + * } + * } + * + * @type {{ [key: string]: {[key: number]: number} }} + * @memberof SubmissionSectionCoarNotifyComponent + */ + previousServices: { [key: string]: LdnServiceByPattern } = {}; + + /** + * The [[JsonPatchOperationPathCombiner]] object + * @type {JsonPatchOperationPathCombiner} + */ + protected pathCombiner: JsonPatchOperationPathCombiner; + /** + * A map representing all field on their way to be removed + * @type {Map} + */ + protected fieldsOnTheirWayToBeRemoved: Map = new Map(); + /** + * Array to track all subscriptions and unsubscribe them onDestroy + * @type {Array} + */ + protected subs: Subscription[] = []; + + private filteredServicesByPattern = {}; + + constructor(protected ldnServicesService: LdnServicesService, + // protected formOperationsService: SectionFormOperationsService, + protected operationsBuilder: JsonPatchOperationsBuilder, + protected sectionService: SectionsService, + protected coarNotifyConfigDataService: CoarNotifyConfigDataService, + protected chd: ChangeDetectorRef, + @Inject('collectionIdProvider') public injectedCollectionId: string, + @Inject('sectionDataProvider') public injectedSectionData: SectionDataObject, + @Inject('submissionIdProvider') public injectedSubmissionId: string) { + super(injectedCollectionId, injectedSectionData, injectedSubmissionId); + } + + /** + * Initialize all instance variables + */ + onSectionInit() { + this.setCoarNotifyConfig(); + this.getSectionServerErrorsAndSetErrorsToDisplay(); + this.pathCombiner = new JsonPatchOperationPathCombiner('sections', this.sectionData.id); + } + + /** + * Method called when section is initialized + * Retrieve available NotifyConfigs + */ + setCoarNotifyConfig() { + this.subs.push( + this.coarNotifyConfigDataService.findAll().pipe( + getFirstCompletedRemoteData(), + ).subscribe((data) => { + if (data.hasSucceeded) { + this.patterns = data.payload.page[0].patterns; + this.initSelectedServicesByPattern(); + } + })); + } + + /** + * Handles the change event of a select element. + * @param pattern - The pattern of the select element. + * @param index - The index of the select element. + * @param selectedService - The selected LDN service. + */ + onChange(pattern: string, index: number, selectedService: LdnService | null) { + // do nothing if the selected value is the same as the previous one + if (this.ldnServiceByPattern[pattern].services[index]?.id === selectedService?.id) { + return; + } + + // initialize the previousServices object for the pattern if it does not exist + if (!this.previousServices[pattern]) { + this.previousServices[pattern] = { + services: [], + allowsMultipleRequests: this.patterns.find(ldnPattern => ldnPattern.pattern === pattern)?.multipleRequest, + }; + } + + // store the previous value + this.previousServices[pattern].services[index] = this.ldnServiceByPattern[pattern].services[index]; + // set the new value + this.ldnServiceByPattern[pattern].services[index] = selectedService; + + const hasPrevValueStored = hasValue(this.previousServices[pattern].services[index]) && this.previousServices[pattern].services[index].id !== selectedService?.id; + if (hasPrevValueStored) { + // when there is a previous value stored and it is different from the new one + this.operationsBuilder.flushOperation(this.pathCombiner.getPath([pattern, '-'])); + if (this.filteredServicesByPattern[pattern]?.includes(this.previousServices[pattern].services[index])){ + this.operationsBuilder.remove(this.pathCombiner.getPath([pattern, index.toString()])); + } + } + + if (!hasPrevValueStored || (selectedService?.id && hasPrevValueStored)) { + // add the path when there is no previous value stored + this.operationsBuilder.add(this.pathCombiner.getPath([pattern, '-']), [selectedService.id], false, true); + } + // set the previous value to the new value + this.previousServices[pattern].services[index] = this.ldnServiceByPattern[pattern].services[index]; + this.sectionService.dispatchRemoveSectionErrors(this.submissionId, this.sectionData.id); + this.chd.detectChanges(); + } + + /** + * Initializes the selected services by pattern. + * Loops through each pattern and filters the services based on the pattern. + * If the section data has a value for the pattern, it adds the service to the selected services by pattern. + * If the section data does not have a value for the pattern, it adds a null service to the selected services by pattern, + * so that the select element is initialized with a null value and to display the default select input. + */ + initSelectedServicesByPattern(): void { + this.patterns.forEach((ldnPattern) => { + if (hasValue(this.sectionData.data[ldnPattern.pattern])) { + this.subs.push( + this.filterServices(ldnPattern.pattern) + .subscribe((services: LdnService[]) => { + + if (!this.ldnServiceByPattern[ldnPattern.pattern]) { + this.ldnServiceByPattern[ldnPattern.pattern] = { + services: [], + allowsMultipleRequests: ldnPattern.multipleRequest, + }; + } + + this.ldnServiceByPattern[ldnPattern.pattern].services = services.filter((service) => { + const selection = (this.sectionData.data[ldnPattern.pattern] as LdnService[]).find((s: LdnService) => s.id === service.id); + this.addService(ldnPattern, selection); + return this.sectionData.data[ldnPattern.pattern].includes(service.uuid); + }); + }), + ); + } else { + this.ldnServiceByPattern[ldnPattern.pattern] = { + services: [], + allowsMultipleRequests: ldnPattern.multipleRequest, + }; + this.addService(ldnPattern, null); + } + }); + } + + /** + * Adds a new service to the selected services for the given pattern. + * @param ldnPattern - The pattern to add the new service to. + * @param newService - The new service to add. + */ + addService(ldnPattern: LdnPattern, newService: LdnService) { + // Your logic to add a new service to the selected services for the pattern + // Example: Push the newService to the array corresponding to the pattern + if (!this.ldnServiceByPattern[ldnPattern.pattern]) { + this.ldnServiceByPattern[ldnPattern.pattern] = { + services: [], + allowsMultipleRequests: ldnPattern.multipleRequest, + }; + } + this.ldnServiceByPattern[ldnPattern.pattern].services.push(newService); + } + + /** + * Removes the service at the specified index from the array corresponding to the pattern. + * @param ldnPattern - The LDN pattern from which to remove the service + * @param serviceIndex - the service index to remove + */ + removeService(ldnPattern: LdnPattern, serviceIndex: number) { + if (this.ldnServiceByPattern[ldnPattern.pattern]) { + // Remove the service at the specified index from the array + this.ldnServiceByPattern[ldnPattern.pattern].services.splice(serviceIndex, 1); + this.previousServices[ldnPattern.pattern]?.services.splice(serviceIndex, 1); + this.operationsBuilder.flushOperation(this.pathCombiner.getPath([ldnPattern.pattern, '-'])); + this.sectionService.dispatchRemoveSectionErrors(this.submissionId, this.sectionData.id); + } + if (!this.ldnServiceByPattern[ldnPattern.pattern].services.length) { + this.addNewService(ldnPattern); + } + } + + /** + * Method called when dropdowns for the section are initialized + * Retrieve services with corresponding patterns to the dropdowns. + */ + filterServices(pattern: string): Observable { + return this.ldnServicesService.findByInboundPattern(pattern).pipe( + getFirstCompletedRemoteData(), + tap((rd) => { + if (rd.hasFailed) { + throw new Error(`Failed to retrieve services for pattern ${pattern}`); + } + }), + filter((rd) => rd.hasSucceeded), + getRemoteDataPayload(), + getPaginatedListPayload(), + tap(res => { + if (!this.filteredServicesByPattern[pattern]){ + this.filteredServicesByPattern[pattern] = []; + } + if (this.filteredServicesByPattern[pattern].length === 0) { + this.filteredServicesByPattern[pattern].push(...res); + } + }), + map((res: LdnService[]) => res.filter((service) => { + if (!this.hasSectionData){ + this.hasSectionData = this.hasInboundPattern(service, pattern); + } + return this.hasInboundPattern(service, pattern); + })), + ); + } + + /** + * Checks if the given service has the specified inbound pattern type. + * @param service - The service to check. + * @param patternType - The inbound pattern type to look for. + * @returns True if the service has the specified inbound pattern type, false otherwise. + */ + hasInboundPattern(service: any, patternType: string): boolean { + return service.notifyServiceInboundPatterns.some((pattern: { pattern: string }) => { + return pattern.pattern === patternType; + }); + } + + /** + * Retrieves server errors for the current section and sets them to display. + * @returns An Observable that emits the validation errors for the current section. + */ + private getSectionServerErrorsAndSetErrorsToDisplay() { + this.subs.push( + this.sectionService.getSectionServerErrors(this.submissionId, this.sectionData.id).pipe( + take(1), + filter((validationErrors) => isNotEmpty(validationErrors)), + ).subscribe((validationErrors: SubmissionSectionError[]) => { + if (isNotEmpty(validationErrors)) { + validationErrors.forEach((error) => { + this.sectionService.setSectionError(this.submissionId, this.sectionData.id, error); + }); + } + })); + } + + /** + * Returns an observable of the errors for the current section that match the given pattern and index. + * @param pattern - The pattern to match against the error paths. + * @param index - The index to match against the error paths. + * @returns An observable of the errors for the current section that match the given pattern and index. + */ + public getShownSectionErrors$(pattern: string, index: number): Observable { + return this.sectionService.getShownSectionErrors(this.submissionId, this.sectionData.id, this.sectionData.sectionType) + .pipe( + take(1), + filter((validationErrors) => isNotEmpty(validationErrors)), + map((validationErrors: SubmissionSectionError[]) => { + return validationErrors.filter((error) => { + const path = `${pattern}/${index}`; + return error.path.includes(path); + }); + }), + ); + } + + /** + * @returns An observable that emits a boolean indicating whether the section has any server errors or not. + */ + protected getSectionStatus(): Observable { + return this.sectionService.getSectionServerErrors(this.submissionId, this.sectionData.id).pipe( + map((validationErrors) => isEmpty(validationErrors), + )); + } + + /** + * Unsubscribe from all subscriptions + */ + onSectionDestroy() { + this.subs + .filter((subscription) => hasValue(subscription)) + .forEach((subscription) => subscription.unsubscribe()); + } + + /** + * Add new row to dropdown for multiple service selection + * @param ldnPattern - the related LDN pattern where the service is added + */ + addNewService(ldnPattern: LdnPattern): void { + //idle new service for new selection + this.ldnServiceByPattern[ldnPattern.pattern].services.push(null); + } +} 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 new file mode 100644 index 00000000000..b80244c2723 --- /dev/null +++ b/src/app/submission/sections/section-coar-notify/submission-coar-notify-workspaceitem.model.ts @@ -0,0 +1,39 @@ +import { + autoserialize, + deserialize, + deserializeAs, + inheritSerialization, +} from 'cerialize'; + +import { typedObject } from '../../../core/cache/builders/build-decorators'; +import { CacheableObject } from '../../../core/cache/cacheable-object.model'; +import { excludeFromEquals } from '../../../core/utilities/equals.decorators'; +import { COAR_NOTIFY_WORKSPACEITEM } from './section-coar-notify-service.resource-type'; + +/** An CoarNotify and its properties. */ +@typedObject +@inheritSerialization(CacheableObject) +export class SubmissionCoarNotifyWorkspaceitemModel extends CacheableObject { + static type = COAR_NOTIFY_WORKSPACEITEM; + + @excludeFromEquals + @autoserialize + endorsement?: number[]; + + @deserializeAs('id') + review?: number[]; + + @autoserialize + ingest?: number[]; + + @deserialize + _links: { + self: { + href: string; + }; + }; + + get self(): string { + return this._links.self.href; + } +} 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 new file mode 100644 index 00000000000..04decc64599 --- /dev/null +++ b/src/app/submission/sections/section-coar-notify/submission-coar-notify.config.ts @@ -0,0 +1,47 @@ +import { + autoserialize, + deserialize, + deserializeAs, + inheritSerialization, +} from 'cerialize'; + +import { typedObject } from '../../../core/cache/builders/build-decorators'; +import { CacheableObject } from '../../../core/cache/cacheable-object.model'; +import { ResourceType } from '../../../core/shared/resource-type'; +import { excludeFromEquals } from '../../../core/utilities/equals.decorators'; +import { SUBMISSION_COAR_NOTIFY_CONFIG } from './section-coar-notify-service.resource-type'; + +export interface LdnPattern { + pattern: string, + multipleRequest: boolean +} +/** A SubmissionCoarNotifyConfig and its properties. */ +@typedObject +@inheritSerialization(CacheableObject) +export class SubmissionCoarNotifyConfig extends CacheableObject { + static type = SUBMISSION_COAR_NOTIFY_CONFIG; + + @excludeFromEquals + @autoserialize + type: ResourceType; + + @autoserialize + id: string; + + @deserializeAs('id') + uuid: string; + + @autoserialize + patterns: LdnPattern[]; + + @deserialize + _links: { + self: { + href: string; + }; + }; + + get self(): string { + return this._links.self.href; + } +} diff --git a/src/app/submission/sections/sections-decorator.ts b/src/app/submission/sections/sections-decorator.ts index 7e7840adfd0..8e0df1cb239 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 6bca8a72526..60b4cedfdc9 100644 --- a/src/app/submission/sections/sections-type.ts +++ b/src/app/submission/sections/sections-type.ts @@ -4,9 +4,10 @@ export enum SectionsType { Upload = 'upload', License = 'license', CcLicense = 'cclicense', - collection = 'collection', AccessesCondition = 'accessCondition', SherpaPolicies = 'sherpaPolicy', Identifiers = 'identifiers', Collection = 'collection', + CoarNotify = 'coarnotify', + Duplicates = 'duplicates' } diff --git a/src/app/submission/sections/sections.directive.ts b/src/app/submission/sections/sections.directive.ts index de6d43e1eb1..7cf1921ff0e 100644 --- a/src/app/submission/sections/sections.directive.ts +++ b/src/app/submission/sections/sections.directive.ts @@ -1,22 +1,35 @@ -import { ChangeDetectorRef, Directive, Input, OnDestroy, OnInit } from '@angular/core'; - -import { Observable, Subscription } from 'rxjs'; -import { map } from 'rxjs/operators'; +import { + ChangeDetectorRef, + Directive, + Input, + OnDestroy, + OnInit, +} from '@angular/core'; import uniq from 'lodash/uniq'; +import { + Observable, + Subscription, +} from 'rxjs'; +import { map } from 'rxjs/operators'; -import { SectionsService } from './sections.service'; -import { hasValue, isNotEmpty, isNotNull } from '../../shared/empty.util'; -import parseSectionErrorPaths, { SectionErrorPath } from '../utils/parseSectionErrorPaths'; +import { + hasValue, + isNotEmpty, + isNotNull, +} from '../../shared/empty.util'; +import { SubmissionSectionError } from '../objects/submission-section-error.model'; import { SubmissionService } from '../submission.service'; +import parseSectionErrorPaths, { SectionErrorPath } from '../utils/parseSectionErrorPaths'; +import { SectionsService } from './sections.service'; import { SectionsType } from './sections-type'; -import { SubmissionSectionError } from '../objects/submission-section-error.model'; /** * Directive for handling generic section functionality */ @Directive({ selector: '[dsSection]', - exportAs: 'sectionRef' + exportAs: 'sectionRef', + standalone: true, }) export class SectionsDirective implements OnDestroy, OnInit { @@ -140,7 +153,7 @@ export class SectionsDirective implements OnDestroy, OnInit { this.submissionService.dispatchSave(this.submissionId); } } - }) + }), ); this.enabled = this.sectionService.isSectionEnabled(this.submissionId, this.sectionId); diff --git a/src/app/submission/sections/sections.service.spec.ts b/src/app/submission/sections/sections.service.spec.ts index 5aa47d1447b..d8644a0db86 100644 --- a/src/app/submission/sections/sections.service.spec.ts +++ b/src/app/submission/sections/sections.service.spec.ts @@ -1,42 +1,54 @@ -import { TestBed, waitForAsync } from '@angular/core/testing'; - -import { cold, getTestScheduler } from 'jasmine-marbles'; -import { of as observableOf } from 'rxjs'; -import { Store, StoreModule } from '@ngrx/store'; +import { + TestBed, + waitForAsync, +} from '@angular/core/testing'; +import { + Store, + StoreModule, +} from '@ngrx/store'; +import { + TranslateLoader, + TranslateModule, + TranslateService, +} from '@ngx-translate/core'; import { ScrollToService } from '@nicky-lenaers/ngx-scroll-to'; -import { TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core'; +import { + cold, + getTestScheduler, +} from 'jasmine-marbles'; +import { of as observableOf } from 'rxjs'; -import { submissionReducers } from '../submission.reducers'; -import { TranslateLoaderMock } from '../../shared/mocks/translate-loader.mock'; -import { NotificationsService } from '../../shared/notifications/notifications.service'; -import { SubmissionService } from '../submission.service'; -import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub'; -import { SubmissionServiceStub } from '../../shared/testing/submission-service.stub'; -import { getMockTranslateService } from '../../shared/mocks/translate.service.mock'; -import { SectionsService } from './sections.service'; +import { storeModuleConfig } from '../../app.reducer'; +import { SubmissionScopeType } from '../../core/submission/submission-scope-type'; +import { FormClearErrorsAction } from '../../shared/form/form.actions'; +import { FormService } from '../../shared/form/form.service'; +import { getMockFormService } from '../../shared/mocks/form-service.mock'; +import { getMockScrollToService } from '../../shared/mocks/scroll-to-service.mock'; import { mockSectionsData, mockSectionsErrors, mockSubmissionState, - mockSubmissionStateWithoutUpload + mockSubmissionStateWithoutUpload, } from '../../shared/mocks/submission.mock'; +import { getMockTranslateService } from '../../shared/mocks/translate.service.mock'; +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 { DisableSectionAction, EnableSectionAction, InertSectionErrorsAction, RemoveSectionErrorsAction, SectionStatusChangeAction, - UpdateSectionDataAction + UpdateSectionDataAction, } from '../objects/submission-objects.actions'; -import { FormClearErrorsAction } from '../../shared/form/form.actions'; +import { SubmissionSectionError } from '../objects/submission-section-error.model'; +import { submissionReducers } from '../submission.reducers'; +import { SubmissionService } from '../submission.service'; import parseSectionErrors from '../utils/parseSectionErrors'; -import { SubmissionScopeType } from '../../core/submission/submission-scope-type'; -import { getMockScrollToService } from '../../shared/mocks/scroll-to-service.mock'; -import { storeModuleConfig } from '../../app.reducer'; +import { SectionsService } from './sections.service'; import { SectionsType } from './sections-type'; -import { FormService } from '../../shared/form/form.service'; -import { getMockFormService } from '../../shared/mocks/form-service.mock'; -import { SubmissionSectionError } from '../objects/submission-section-error.model'; describe('SectionsService test suite', () => { let notificationsServiceStub: NotificationsServiceStub; @@ -56,7 +68,7 @@ describe('SectionsService test suite', () => { const store: any = jasmine.createSpyObj('store', { dispatch: jasmine.createSpy('dispatch'), - select: jasmine.createSpy('select') + select: jasmine.createSpy('select'), }); const formService: any = getMockFormService(); @@ -68,9 +80,9 @@ describe('SectionsService test suite', () => { TranslateModule.forRoot({ loader: { provide: TranslateLoader, - useClass: TranslateLoaderMock - } - }) + useClass: TranslateLoaderMock, + }, + }), ], providers: [ { provide: NotificationsService, useClass: NotificationsServiceStub }, @@ -79,8 +91,8 @@ describe('SectionsService test suite', () => { { provide: TranslateService, useValue: getMockTranslateService() }, { provide: Store, useValue: store }, { provide: FormService, useValue: formService }, - SectionsService - ] + SectionsService, + ], }).compileComponents(); })); @@ -161,7 +173,7 @@ describe('SectionsService test suite', () => { store.select.and.returnValue(observableOf(sectionData[sectionId])); const expected = cold('(b|)', { - b: sectionData[sectionId] + b: sectionData[sectionId], }); expect(service.getSectionData(submissionId, sectionId, SectionsType.SubmissionForm)).toBeObservable(expected); @@ -173,7 +185,7 @@ describe('SectionsService test suite', () => { store.select.and.returnValue(observableOf(sectionErrors[sectionId])); const expected = cold('(b|)', { - b: sectionErrors[sectionId] + b: sectionErrors[sectionId], }); expect(service.getSectionErrors(submissionId, sectionId)).toBeObservable(expected); @@ -185,7 +197,7 @@ describe('SectionsService test suite', () => { store.select.and.returnValue(observableOf(sectionState)); const expected = cold('(b|)', { - b: sectionState + b: sectionState, }); expect(service.getSectionState(submissionId, sectionId, SectionsType.SubmissionForm)).toBeObservable(expected); @@ -197,7 +209,7 @@ describe('SectionsService test suite', () => { store.select.and.returnValue(observableOf({ isValid: false })); let expected = cold('(b|)', { - b: false + b: false, }); expect(service.isSectionValid(submissionId, sectionId)).toBeObservable(expected); @@ -205,7 +217,7 @@ describe('SectionsService test suite', () => { store.select.and.returnValue(observableOf({ isValid: true })); expected = cold('(b|)', { - b: true + b: true, }); expect(service.isSectionValid(submissionId, sectionId)).toBeObservable(expected); @@ -217,7 +229,7 @@ describe('SectionsService test suite', () => { submissionServiceStub.getActiveSectionId.and.returnValue(observableOf(sectionId)); let expected = cold('(b|)', { - b: true + b: true, }); expect(service.isSectionActive(submissionId, sectionId)).toBeObservable(expected); @@ -225,7 +237,7 @@ describe('SectionsService test suite', () => { submissionServiceStub.getActiveSectionId.and.returnValue(observableOf('test')); expected = cold('(b|)', { - b: false + b: false, }); expect(service.isSectionActive(submissionId, sectionId)).toBeObservable(expected); @@ -237,7 +249,7 @@ describe('SectionsService test suite', () => { store.select.and.returnValue(observableOf({ enabled: false })); let expected = cold('(b|)', { - b: false + b: false, }); expect(service.isSectionEnabled(submissionId, sectionId)).toBeObservable(expected); @@ -245,7 +257,7 @@ describe('SectionsService test suite', () => { store.select.and.returnValue(observableOf({ enabled: true })); expected = cold('(b|)', { - b: true + b: true, }); expect(service.isSectionEnabled(submissionId, sectionId)).toBeObservable(expected); @@ -257,12 +269,12 @@ describe('SectionsService test suite', () => { store.select.and.returnValue(observableOf({ visibility: { main: null, - other: 'READONLY' - } + other: 'READONLY', + }, })); const expected = cold('(b|)', { - b: true + b: true, }); expect(service.isSectionReadOnly(submissionId, sectionId, SubmissionScopeType.WorkflowItem)).toBeObservable(expected); @@ -272,12 +284,12 @@ describe('SectionsService test suite', () => { store.select.and.returnValue(observableOf({ visibility: { main: null, - other: 'READONLY' - } + other: 'READONLY', + }, })); const expected = cold('(b|)', { - b: false + b: false, }); expect(service.isSectionReadOnly(submissionId, sectionId, SubmissionScopeType.WorkspaceItem)).toBeObservable(expected); @@ -285,11 +297,11 @@ describe('SectionsService test suite', () => { it('should return an observable of false when it\'s not a readonly section', () => { store.select.and.returnValue(observableOf({ - visibility: null + visibility: null, })); const expected = cold('(b|)', { - b: false + b: false, }); expect(service.isSectionReadOnly(submissionId, sectionId, SubmissionScopeType.WorkflowItem)).toBeObservable(expected); @@ -301,7 +313,7 @@ describe('SectionsService test suite', () => { store.select.and.returnValue(observableOf(submissionState)); const expected = cold('(b|)', { - b: true + b: true, }); expect(service.isSectionAvailable(submissionId, sectionId)).toBeObservable(expected); @@ -311,7 +323,7 @@ describe('SectionsService test suite', () => { store.select.and.returnValue(observableOf(submissionState)); const expected = cold('(b|)', { - b: false + b: false, }); expect(service.isSectionAvailable(submissionId, 'test')).toBeObservable(expected); @@ -323,7 +335,7 @@ describe('SectionsService test suite', () => { store.select.and.returnValue(observableOf(submissionState)); const expected = cold('(b|)', { - b: true + b: true, }); expect(service.isSectionTypeAvailable(submissionId, SectionsType.Upload)).toBeObservable(expected); @@ -333,7 +345,7 @@ describe('SectionsService test suite', () => { store.select.and.returnValue(observableOf(submissionStateWithoutUpload)); const expected = cold('(b|)', { - b: false + b: false, }); expect(service.isSectionAvailable(submissionId, SectionsType.Upload)).toBeObservable(expected); @@ -345,7 +357,7 @@ describe('SectionsService test suite', () => { store.select.and.returnValue(observableOf(submissionState)); const expected = cold('(b|)', { - b: true + b: true, }); expect(service.isSectionType(submissionId, 'upload', SectionsType.Upload)).toBeObservable(expected); @@ -355,7 +367,7 @@ describe('SectionsService test suite', () => { store.select.and.returnValue(observableOf(submissionState)); const expected = cold('(b|)', { - b: false + b: false, }); expect(service.isSectionType(submissionId, sectionId, SectionsType.Upload)).toBeObservable(expected); @@ -365,7 +377,7 @@ describe('SectionsService test suite', () => { store.select.and.returnValue(observableOf(submissionState)); const expected = cold('(b|)', { - b: false + b: false, }); expect(service.isSectionType(submissionId, 'no-such-id', SectionsType.Upload)).toBeObservable(expected); @@ -396,7 +408,7 @@ describe('SectionsService test suite', () => { const error: SubmissionSectionError = { path: 'test', - message: 'message test' + message: 'message test', }; service.setSectionError(submissionId, sectionId, error); @@ -446,10 +458,10 @@ describe('SectionsService test suite', () => { rows: [{ fields: [{ selectableMetadata: [{ - metadata: 'dc.contributor.author' - }] - }] - }] + metadata: 'dc.contributor.author', + }], + }], + }], }; const expectedConfiguredMetadata = [ 'dc.contributor.author' ]; diff --git a/src/app/submission/sections/sections.service.ts b/src/app/submission/sections/sections.service.ts index 0ea62322370..6cc064283ee 100644 --- a/src/app/submission/sections/sections.service.ts +++ b/src/app/submission/sections/sections.service.ts @@ -1,16 +1,41 @@ import { Injectable } from '@angular/core'; - -import { combineLatest, Observable } from 'rxjs'; -import { distinctUntilChanged, filter, map, mergeMap, take } from 'rxjs/operators'; +import { parseReviver } from '@ng-dynamic-forms/core'; import { Store } from '@ngrx/store'; import { TranslateService } from '@ngx-translate/core'; -import { ScrollToConfigOptions, ScrollToService } from '@nicky-lenaers/ngx-scroll-to'; +import { + ScrollToConfigOptions, + ScrollToService, +} from '@nicky-lenaers/ngx-scroll-to'; import findIndex from 'lodash/findIndex'; import findKey from 'lodash/findKey'; import isEqual from 'lodash/isEqual'; +import { + combineLatest, + Observable, +} from 'rxjs'; +import { + distinctUntilChanged, + filter, + map, + mergeMap, + take, +} from 'rxjs/operators'; -import { SubmissionState } from '../submission.reducers'; -import { hasValue, isEmpty, isNotEmpty, isNotUndefined } from '../../shared/empty.util'; +import { SubmissionFormsModel } from '../../core/config/models/config-submission-forms.model'; +import { JsonPatchOperationPathCombiner } from '../../core/json-patch/builder/json-patch-operation-path-combiner'; +import { WorkspaceitemSectionDataType } from '../../core/submission/models/workspaceitem-sections.model'; +import { normalizeSectionData } from '../../core/submission/submission-response-parsing.service'; +import { SubmissionScopeType } from '../../core/submission/submission-scope-type'; +import { + hasValue, + isEmpty, + isNotEmpty, + isNotUndefined, +} from '../../shared/empty.util'; +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 { DisableSectionAction, EnableSectionAction, @@ -18,38 +43,27 @@ import { RemoveSectionErrorsAction, SectionStatusChangeAction, SetSectionFormId, - UpdateSectionDataAction + UpdateSectionDataAction, } from '../objects/submission-objects.actions'; -import { - SubmissionObjectEntry -} from '../objects/submission-objects.reducer'; +import { SubmissionObjectEntry } from '../objects/submission-objects.reducer'; +import { SubmissionSectionError } from '../objects/submission-section-error.model'; +import { SubmissionSectionObject } from '../objects/submission-section-object.model'; import { submissionObjectFromIdSelector, submissionSectionDataFromIdSelector, submissionSectionErrorsFromIdSelector, submissionSectionFromIdSelector, - submissionSectionServerErrorsFromIdSelector + submissionSectionServerErrorsFromIdSelector, } from '../selectors'; -import { SubmissionScopeType } from '../../core/submission/submission-scope-type'; -import parseSectionErrorPaths, { SectionErrorPath } from '../utils/parseSectionErrorPaths'; -import { FormClearErrorsAction } from '../../shared/form/form.actions'; -import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { SubmissionState } from '../submission.reducers'; import { SubmissionService } from '../submission.service'; -import { WorkspaceitemSectionDataType } from '../../core/submission/models/workspaceitem-sections.model'; +import parseSectionErrorPaths, { SectionErrorPath } from '../utils/parseSectionErrorPaths'; import { SectionsType } from './sections-type'; -import { normalizeSectionData } from '../../core/submission/submission-response-parsing.service'; -import { SubmissionFormsModel } from '../../core/config/models/config-submission-forms.model'; -import { parseReviver } from '@ng-dynamic-forms/core'; -import { FormService } from '../../shared/form/form.service'; -import { JsonPatchOperationPathCombiner } from '../../core/json-patch/builder/json-patch-operation-path-combiner'; -import { FormError } from '../../shared/form/form.reducer'; -import { SubmissionSectionObject } from '../objects/submission-section-object.model'; -import { SubmissionSectionError } from '../objects/submission-section-error.model'; /** * A service that provides methods used in submission process. */ -@Injectable() +@Injectable({ providedIn: 'root' }) export class SectionsService { /** @@ -110,7 +124,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); @@ -197,12 +211,12 @@ export class SectionsService { const sectionErrors = formErrors .map((error) => ({ path: pathCombiner.getPath(error.fieldId.replace(/\_/g, '.')).path, - message: error.message + message: error.message, } as SubmissionSectionError)) .filter((sectionError: SubmissionSectionError) => findIndex(state.errorsToShow, { path: sectionError.path }) === -1); return [...state.errorsToShow, ...sectionErrors]; - }) - )) + }), + )), ); } @@ -257,13 +271,13 @@ export class SectionsService { map((sectionState: SubmissionSectionObject) => { if (hasValue(sectionState.data) && sectionType === SectionsType.SubmissionForm) { return Object.assign({}, sectionState, { - data: normalizeSectionData(sectionState.data) + data: normalizeSectionData(sectionState.data), }); } else { return sectionState; } }), - distinctUntilChanged() + distinctUntilChanged(), ); } @@ -336,7 +350,7 @@ export class SectionsService { return isNotEmpty(sectionObj.visibility) && ((sectionObj.visibility.other === 'READONLY' && submissionScope !== SubmissionScopeType.WorkspaceItem) || (sectionObj.visibility.main === 'READONLY' && submissionScope === SubmissionScopeType.WorkspaceItem) - ); + ); }), distinctUntilChanged()); } @@ -407,7 +421,7 @@ export class SectionsService { this.store.dispatch(new EnableSectionAction(submissionId, sectionId)); const config: ScrollToConfigOptions = { target: sectionId, - offset: -70 + offset: -70, }; this.scrollToService.scrollTo(config); @@ -449,7 +463,7 @@ export class SectionsService { data: WorkspaceitemSectionDataType, errorsToShow: SubmissionSectionError[] = [], serverValidationErrors: SubmissionSectionError[] = [], - metadata?: string[] + metadata?: string[], ) { if (isNotEmpty(data)) { const isAvailable$ = this.isSectionAvailable(submissionId, sectionId); 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 b65cb5e00fe..2abe8e02ec6 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 @@ -1,13 +1,18 @@ -import { NgbCollapseModule } from '@ng-bootstrap/ng-bootstrap'; -import { TranslateLoaderMock } from '../../../../shared/mocks/translate-loader.mock'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; - -import { ContentAccordionComponent } from './content-accordion.component'; - import { DebugElement } from '@angular/core'; +import { + ComponentFixture, + TestBed, +} from '@angular/core/testing'; import { By } from '@angular/platform-browser'; -import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; +import { NgbCollapseModule } from '@ng-bootstrap/ng-bootstrap'; +import { + TranslateLoader, + TranslateModule, +} from '@ngx-translate/core'; + import { SherpaDataResponse } from '../../../../shared/mocks/section-sherpa-policies.service.mock'; +import { TranslateLoaderMock } from '../../../../shared/mocks/translate-loader.mock'; +import { ContentAccordionComponent } from './content-accordion.component'; describe('ContentAccordionComponent', () => { let component: ContentAccordionComponent; @@ -20,12 +25,12 @@ describe('ContentAccordionComponent', () => { TranslateModule.forRoot({ loader: { provide: TranslateLoader, - useClass: TranslateLoaderMock - } + useClass: TranslateLoaderMock, + }, }), - NgbCollapseModule + 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 0e7bc863ad3..2fde4f37cd4 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,4 +1,14 @@ -import { Component, Input } from '@angular/core'; +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'; @@ -8,7 +18,15 @@ import { PermittedVersions } from '../../../../core/submission/models/sherpa-pol @Component({ selector: 'ds-content-accordion', templateUrl: './content-accordion.component.html', - styleUrls: ['./content-accordion.component.scss'] + 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 9a60a6d0106..5accbf3c5ff 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 @@ -1,12 +1,17 @@ -import { TranslateLoaderMock } from '../../../../shared/testing/translate-loader.mock'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; - -import { MetadataInformationComponent } from './metadata-information.component'; - import { DebugElement } from '@angular/core'; +import { + ComponentFixture, + TestBed, +} from '@angular/core/testing'; import { By } from '@angular/platform-browser'; -import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; +import { + TranslateLoader, + TranslateModule, +} from '@ngx-translate/core'; + import { SherpaDataResponse } from '../../../../shared/mocks/section-sherpa-policies.service.mock'; +import { TranslateLoaderMock } from '../../../../shared/testing/translate-loader.mock'; +import { MetadataInformationComponent } from './metadata-information.component'; describe('MetadataInformationComponent', () => { let component: MetadataInformationComponent; @@ -19,11 +24,11 @@ describe('MetadataInformationComponent', () => { TranslateModule.forRoot({ loader: { provide: TranslateLoader, - useClass: TranslateLoaderMock - } + 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 307c18d8ccf..4eb9567abb4 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,14 +1,28 @@ -import { Component, Input } from '@angular/core'; +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'] + 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 c5dc896858f..fae68cd8a48 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 @@ -1,11 +1,17 @@ -import { TranslateLoaderMock } from '../../../../shared/testing/translate-loader.mock'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; - -import { PublicationInformationComponent } from './publication-information.component'; import { DebugElement } from '@angular/core'; +import { + ComponentFixture, + TestBed, +} from '@angular/core/testing'; import { By } from '@angular/platform-browser'; -import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; +import { + TranslateLoader, + TranslateModule, +} from '@ngx-translate/core'; + import { SherpaDataResponse } from '../../../../shared/mocks/section-sherpa-policies.service.mock'; +import { TranslateLoaderMock } from '../../../../shared/testing/translate-loader.mock'; +import { PublicationInformationComponent } from './publication-information.component'; describe('PublicationInformationComponent', () => { let component: PublicationInformationComponent; @@ -19,11 +25,11 @@ describe('PublicationInformationComponent', () => { TranslateModule.forRoot({ loader: { provide: TranslateLoader, - useClass: TranslateLoaderMock - } + 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 cfe42adf7bc..8f256700a07 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,4 +1,12 @@ -import { Component, Input } from '@angular/core'; +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'; @@ -8,7 +16,13 @@ import { Journal } from '../../../../core/submission/models/sherpa-policies-deta @Component({ selector: 'ds-publication-information', templateUrl: './publication-information.component.html', - styleUrls: ['./publication-information.component.scss'] + 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 3e2c33481ab..773c416f1c8 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 @@ -1,11 +1,18 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { PublisherPolicyComponent } from './publisher-policy.component'; -import { By } from '@angular/platform-browser'; import { DebugElement } from '@angular/core'; -import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; +import { + ComponentFixture, + TestBed, +} from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { + TranslateLoader, + TranslateModule, +} from '@ngx-translate/core'; 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', () => { let component: PublisherPolicyComponent; @@ -18,12 +25,17 @@ describe('PublisherPolicyComponent', () => { TranslateModule.forRoot({ loader: { provide: TranslateLoader, - useClass: TranslateLoaderMock - } + 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 96ada3904c2..8cbe2f69043 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,15 +1,33 @@ -import { Component, Input } from '@angular/core'; +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/aletr-type'; +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'] + 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 76a980ed3c7..5882a277e63 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 @@ -1,23 +1,36 @@ -import { SharedModule } from '../../../shared/shared.module'; +import { DebugElement } from '@angular/core'; +import { + ComponentFixture, + inject, + TestBed, +} from '@angular/core/testing'; +import { + BrowserModule, + By, +} from '@angular/platform-browser'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { NgbCollapseModule } from '@ng-bootstrap/ng-bootstrap'; -import { SubmissionServiceStub } from '../../../shared/testing/submission-service.stub'; -import { SherpaDataResponse } from '../../../shared/mocks/section-sherpa-policies.service.mock'; -import { ComponentFixture, inject, TestBed } from '@angular/core/testing'; - -import { SectionsService } from '../sections.service'; -import { SectionsServiceStub } from '../../../shared/testing/sections-service.stub'; -import { JsonPatchOperationsBuilder } from '../../../core/json-patch/builder/json-patch-operations-builder'; -import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; -import { BrowserModule, By } from '@angular/platform-browser'; - import { Store } from '@ngrx/store'; +import { + TranslateLoader, + TranslateModule, +} 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 { SubmissionSectionSherpaPoliciesComponent } from './section-sherpa-policies.component'; -import { SubmissionService } from '../../submission.service'; -import { DebugElement } from '@angular/core'; +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 { of as observableOf } from 'rxjs'; -import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +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', () => { let component: SubmissionSectionSherpaPoliciesComponent; @@ -45,7 +58,7 @@ describe('SubmissionSectionSherpaPoliciesComponent', () => { errorsToShow: [], serverValidationErrors: [], isLoading: false, - isValid: true + isValid: true, }; describe('SubmissionSectionSherpaPoliciesComponent', () => { @@ -58,13 +71,12 @@ describe('SubmissionSectionSherpaPoliciesComponent', () => { TranslateModule.forRoot({ loader: { provide: TranslateLoader, - useClass: TranslateLoaderMock - } + useClass: TranslateLoaderMock, + }, }), NgbCollapseModule, - SharedModule + SubmissionSectionSherpaPoliciesComponent, ], - declarations: [SubmissionSectionSherpaPoliciesComponent], providers: [ { provide: SectionsService, useValue: sectionsServiceStub }, { provide: JsonPatchOperationsBuilder, useValue: operationsBuilder }, @@ -72,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 e55b75146f6..86d55ce8d00 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,30 +1,60 @@ -import { AlertType } from '../../../shared/alert/aletr-type'; -import { Component, Inject } from '@angular/core'; - -import { BehaviorSubject, Observable, of, Subscription } from 'rxjs'; +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, + of, + Subscription, +} from 'rxjs'; 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 { - WorkspaceitemSectionSherpaPoliciesObject -} from '../../../core/submission/models/workspaceitem-section-sherpa-policies.model'; -import { renderSectionFor } from '../sections-decorator'; -import { SectionsType } from '../sections-type'; + 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 { SectionModelComponent } from '../models/section.model'; -import { SubmissionService } from '../../submission.service'; -import { hasValue, isEmpty } from '../../../shared/empty.util'; +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'] + 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 { /** @@ -95,7 +125,7 @@ export class SubmissionSectionSherpaPoliciesComponent extends SectionModelCompon this.sectionService.getSectionData(this.submissionId, this.sectionData.id, this.sectionData.sectionType) .subscribe((sherpaPolicies: WorkspaceitemSectionSherpaPoliciesObject) => { this.sherpaPoliciesData$.next(sherpaPolicies); - }) + }), ); } diff --git a/src/app/submission/sections/upload/accessConditions/submission-section-upload-access-conditions.component.html b/src/app/submission/sections/upload/accessConditions/submission-section-upload-access-conditions.component.html index 7c5a979eed9..f217abf88e3 100644 --- a/src/app/submission/sections/upload/accessConditions/submission-section-upload-access-conditions.component.html +++ b/src/app/submission/sections/upload/accessConditions/submission-section-upload-access-conditions.component.html @@ -1,8 +1,8 @@ - + {{accessCondition.name}} {{accessCondition.startDate}} {{accessCondition.endDate}} - {{accessCondition.name}} from {{accessCondition.endDate}} - {{accessCondition.name}} until {{accessCondition.startDate}} + {{accessCondition.name}} from {{accessCondition.endDate}} + {{accessCondition.name}} until {{accessCondition.startDate}}
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 1a6a05e7b69..84eff58f723 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,13 +1,20 @@ -import { Component, Input, OnInit } from '@angular/core'; - -import { find } from 'rxjs/operators'; +import { + NgForOf, + NgIf, +} from '@angular/common'; +import { + Component, + Input, + OnInit, +} from '@angular/core'; +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'; -import { Group } from '../../../../core/eperson/models/group.model'; -import { RemoteData } from '../../../../core/data/remote-data'; -import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service'; /** * This component represents a badge that describe an access condition @@ -15,6 +22,11 @@ import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service'; @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 { @@ -43,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.html b/src/app/submission/sections/upload/file/edit/section-upload-file-edit.component.html index 2baa6c1555a..0cb79e51cb0 100644 --- a/src/app/submission/sections/upload/file/edit/section-upload-file-edit.component.html +++ b/src/app/submission/sections/upload/file/edit/section-upload-file-edit.component.html @@ -6,7 +6,6 @@
@@ -15,15 +16,23 @@
- {{entry.value | dsTruncate:['150']}} + {{entry.value | dsTruncate:['150']}} - {{'submission.sections.upload.no-entry' | translate}} {{fileDescrKey}} + {{'submission.sections.upload.no-entry' | translate}} {{fileDescrKey}} + +
+ {{'admin.registries.bitstream-formats.edit.head' | translate:{format: fileFormat} }} +
+
+ Checksum {{fileCheckSum.checkSumAlgorithm}}: {{fileCheckSum.value}} +
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 dddc6cdc363..1ff35abc48e 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,14 +1,23 @@ -import { Component, NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, inject, TestBed, waitForAsync } from '@angular/core/testing'; - +import { + ChangeDetectionStrategy, + Component, + NO_ERRORS_SCHEMA, +} from '@angular/core'; +import { + ComponentFixture, + inject, + TestBed, + waitForAsync, +} from '@angular/core/testing'; import { TranslateModule } from '@ngx-translate/core'; -import { mockUploadFiles } from '../../../../../shared/mocks/submission.mock'; -import { FormComponent } from '../../../../../shared/form/form.component'; -import { SubmissionSectionUploadFileViewComponent } from './section-upload-file-view.component'; -import { TruncatePipe } from '../../../../../shared/utils/truncate.pipe'; import { Metadata } from '../../../../../core/shared/metadata.utils'; +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', () => { @@ -18,22 +27,29 @@ describe('SubmissionSectionUploadFileViewComponent test suite', () => { const fileData: any = mockUploadFiles[0]; - beforeEach(waitForAsync(() => { - TestBed.configureTestingModule({ + beforeEach(waitForAsync(async () => { + await TestBed.configureTestingModule({ imports: [ - TranslateModule.forRoot() - ], - declarations: [ + TranslateModule.forRoot(), TruncatePipe, FormComponent, SubmissionSectionUploadFileViewComponent, - TestComponent + TestComponent, ], providers: [ - SubmissionSectionUploadFileViewComponent + SubmissionSectionUploadFileViewComponent, ], - schemas: [NO_ERRORS_SCHEMA] - }).compileComponents().then(); + schemas: [NO_ERRORS_SCHEMA], + }) + .overrideComponent(SubmissionSectionUploadFileViewComponent, { + remove: { + imports: [SubmissionSectionUploadAccessConditionsComponent], + }, + add: { + changeDetection: ChangeDetectionStrategy.Default, + }, + }) + .compileComponents().then(); })); describe('', () => { @@ -92,7 +108,8 @@ describe('SubmissionSectionUploadFileViewComponent test suite', () => { // declare a test component @Component({ selector: 'ds-test-cmp', - template: `` + 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 bb2fea20f83..f065fc9e190 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,9 +1,24 @@ -import { Component, Input, OnInit } from '@angular/core'; +import { + NgForOf, + NgIf, +} from '@angular/common'; +import { + Component, + Input, + OnInit, +} from '@angular/core'; +import { TranslateModule } from '@ngx-translate/core'; +import { + MetadataMap, + MetadataValue, +} from '../../../../../core/shared/metadata.models'; +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 { Metadata } from '../../../../../core/shared/metadata.utils'; -import { MetadataMap, MetadataValue } from '../../../../../core/shared/metadata.models'; +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 @@ -11,6 +26,15 @@ import { MetadataMap, MetadataValue } from '../../../../../core/shared/metadata. @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 { @@ -38,6 +62,13 @@ export class SubmissionSectionUploadFileViewComponent implements OnInit { */ public fileDescrKey = 'Description'; + public fileFormat!: string; + + public fileCheckSum!: { + checkSumAlgorithm: string; + value: string; + }; + /** * Initialize instance variables */ @@ -46,6 +77,8 @@ export class SubmissionSectionUploadFileViewComponent implements OnInit { this.metadata[this.fileTitleKey] = Metadata.all(this.fileData.metadata, 'dc.title'); this.metadata[this.fileDescrKey] = Metadata.all(this.fileData.metadata, 'dc.description'); } + this.fileCheckSum = this.fileData.checkSum; + this.fileFormat = this.fileData.format.shortDescription; } /** 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 00000000000..26f35a094df --- /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 b57b4542885..9d916a4f982 100644 --- a/src/app/submission/sections/upload/section-upload.component.html +++ b/src/app/submission/sections/upload/section-upload.component.html @@ -2,15 +2,7 @@ [dismissible]="true" [type]="AlertTypeEnum.Info"> - -
-
-

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

-
-
-
- - +
@@ -26,18 +18,28 @@

{{'submission.sections.upload.n

- - - +
+ {{ 'bitstream.edit.form.primaryBitstream.label' | translate }} +
+
+
+
+
+
+
+ + + [submissionId]="submissionId">

@@ -45,3 +47,11 @@

{{'submission.sections.upload.n

+ + +
+
+
{{'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 068fc5c7660..61db6c68851 100644 --- a/src/app/submission/sections/upload/section-upload.component.spec.ts +++ b/src/app/submission/sections/upload/section-upload.component.spec.ts @@ -1,22 +1,33 @@ -import { ChangeDetectorRef, Component, NO_ERRORS_SCHEMA } from '@angular/core'; -import { ComponentFixture, inject, TestBed, waitForAsync } from '@angular/core/testing'; -import { BrowserModule } from '@angular/platform-browser'; import { CommonModule } from '@angular/common'; - +import { + ChangeDetectorRef, + Component, + NO_ERRORS_SCHEMA, +} from '@angular/core'; +import { + ComponentFixture, + inject, + TestBed, + waitForAsync, +} from '@angular/core/testing'; import { TranslateModule } from '@ngx-translate/core'; import { cold } from 'jasmine-marbles'; import { of as observableOf } from 'rxjs'; -import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; -import { createTestComponent } from '../../../shared/testing/utils.test'; -import { SubmissionObjectState } from '../../objects/submission-objects.reducer'; -import { SubmissionService } from '../../submission.service'; -import { SubmissionServiceStub } from '../../../shared/testing/submission-service.stub'; -import { SectionsService } from '../sections.service'; -import { SectionsServiceStub } from '../../../shared/testing/sections-service.stub'; +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 { SectionDataObject } from '../models/section-data.model'; -import { SectionsType } from '../sections-type'; +import { SubmissionUploadsConfigDataService } from '../../../core/config/submission-uploads-config-data.service'; +import { CollectionDataService } from '../../../core/data/collection-data.service'; +import { buildPaginatedList } from '../../../core/data/paginated-list.model'; +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 { 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, mockSubmissionCollectionId, @@ -25,20 +36,21 @@ import { mockUploadConfigResponse, mockUploadConfigResponseNotRequired, mockUploadFiles, + mockUploadFilesData, } from '../../../shared/mocks/submission.mock'; -import { SubmissionUploadsConfigDataService } from '../../../core/config/submission-uploads-config-data.service'; -import { SectionUploadService } from './section-upload.service'; +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'; +import { SectionsService } from '../sections.service'; +import { SectionsType } from '../sections-type'; import { SubmissionSectionUploadComponent } from './section-upload.component'; -import { CollectionDataService } from '../../../core/data/collection-data.service'; -import { GroupDataService } from '../../../core/eperson/group-data.service'; -import { Collection } from '../../../core/shared/collection.model'; -import { ResourcePolicy } from '../../../core/resource-policy/models/resource-policy.model'; -import { ResourcePolicyDataService } from '../../../core/resource-policy/resource-policy-data.service'; -import { Group } from '../../../core/eperson/models/group.model'; -import { getMockSectionUploadService } from '../../../shared/mocks/section-upload.service.mock'; -import { SubmissionUploadsModel } from '../../../core/config/models/config-submission-uploads.model'; -import { buildPaginatedList } from '../../../core/data/paginated-list.model'; -import { PageInfo } from '../../../core/shared/page-info.model'; +import { SectionUploadService } from './section-upload.service'; function getMockSubmissionUploadsConfigService(): SubmissionFormsConfigDataService { return jasmine.createSpyObj('SubmissionUploadsConfigService', { @@ -46,13 +58,13 @@ function getMockSubmissionUploadsConfigService(): SubmissionFormsConfigDataServi getConfigByHref: jasmine.createSpy('getConfigByHref'), getConfigByName: jasmine.createSpy('getConfigByName'), getConfigBySearch: jasmine.createSpy('getConfigBySearch'), - findByHref: jasmine.createSpy('findByHref') + findByHref: jasmine.createSpy('findByHref'), }); } function getMockCollectionDataService(): CollectionDataService { return jasmine.createSpyObj('CollectionDataService', { - findById: jasmine.createSpy('findById') + findById: jasmine.createSpy('findById'), }); } @@ -65,7 +77,7 @@ function getMockGroupEpersonService(): GroupDataService { function getMockResourcePolicyService(): ResourcePolicyDataService { return jasmine.createSpyObj('ResourcePolicyService', { - findByHref: jasmine.createSpy('findByHref') + findByHref: jasmine.createSpy('findByHref'), }); } @@ -96,13 +108,13 @@ describe('SubmissionSectionUploadComponent test suite', () => { config: 'https://dspace7.4science.it/or2018/api/config/submissionforms/upload', mandatory: true, data: { - files: [] + files: [], }, errorsToShow: [], serverValidationErrors: [], header: 'submit.progressbar.describe.upload', id: 'upload-id', - sectionType: SectionsType.Upload + sectionType: SectionsType.Upload, }; submissionId = mockSubmissionId; collectionId = mockSubmissionCollectionId; @@ -114,18 +126,18 @@ describe('SubmissionSectionUploadComponent test suite', () => { { key: 'dc.title', language: 'en_US', - value: 'Community 1-Collection 1' + value: 'Community 1-Collection 1', }], _links: { - defaultAccessConditions: collectionId + '/defaultAccessConditions' - } + defaultAccessConditions: collectionId + '/defaultAccessConditions', + }, }); mockDefaultAccessCondition = Object.assign(new ResourcePolicy(), { name: null, groupUUID: '11cc35e5-a11d-4b64-b5b9-0052a5d15509', id: 20, - uuid: 'resource-policy-20' + uuid: 'resource-policy-20', }); uploadsConfigService = getMockSubmissionUploadsConfigService(); @@ -145,32 +157,30 @@ describe('SubmissionSectionUploadComponent test suite', () => { submissionServiceStub.getSubmissionObject.and.returnValue(observableOf(submissionState)); collectionDataService.findById.and.returnValue(createSuccessfulRemoteDataObject$(Object.assign(new Collection(), mockCollection, { - defaultAccessConditions: createSuccessfulRemoteDataObject$(mockDefaultAccessCondition) + defaultAccessConditions: createSuccessfulRemoteDataObject$(mockDefaultAccessCondition), }))); resourcePolicyService.findByHref.and.returnValue(createSuccessfulRemoteDataObject$(mockDefaultAccessCondition)); uploadsConfigService.findByHref.and.returnValue(createSuccessfulRemoteDataObject$( - buildPaginatedList(new PageInfo(), [mockUploadConfigResponse as any])) + buildPaginatedList(new PageInfo(), [mockUploadConfigResponse as any])), ); groupService.findById.and.returnValues( createSuccessfulRemoteDataObject$(Object.assign(new Group(), mockGroup)), - createSuccessfulRemoteDataObject$(Object.assign(new Group(), mockGroup)) + createSuccessfulRemoteDataObject$(Object.assign(new Group(), mockGroup)), ); bitstreamService.getUploadedFileList.and.returnValue(observableOf([])); + bitstreamService.getUploadedFilesData.and.returnValue(observableOf({ primary: null, files: [] })); }; TestBed.configureTestingModule({ imports: [ - BrowserModule, CommonModule, - TranslateModule.forRoot() - ], - declarations: [ + TranslateModule.forRoot(), SubmissionSectionUploadComponent, - TestComponent + TestComponent, ], providers: [ { provide: CollectionDataService, useValue: collectionDataService }, @@ -182,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 + SubmissionSectionUploadComponent, ], - schemas: [NO_ERRORS_SCHEMA] - }).compileComponents().then(); + schemas: [NO_ERRORS_SCHEMA], + }) + .overrideComponent(SubmissionSectionUploadComponent, { + remove: { + imports: [AlertComponent], + }, + }) + .compileComponents().then(); })); describe('', () => { @@ -230,11 +248,11 @@ describe('SubmissionSectionUploadComponent test suite', () => { }); it('should init component properly', () => { - + bitstreamService.getUploadedFilesData.and.returnValue(observableOf({ primary: null, files: [] })); submissionServiceStub.getSubmissionObject.and.returnValue(observableOf(submissionState)); collectionDataService.findById.and.returnValue(createSuccessfulRemoteDataObject$(Object.assign(new Collection(), mockCollection, { - defaultAccessConditions: createSuccessfulRemoteDataObject$(mockDefaultAccessCondition) + defaultAccessConditions: createSuccessfulRemoteDataObject$(mockDefaultAccessCondition), }))); resourcePolicyService.findByHref.and.returnValue(createSuccessfulRemoteDataObject$(mockDefaultAccessCondition)); @@ -243,18 +261,11 @@ describe('SubmissionSectionUploadComponent test suite', () => { groupService.findById.and.returnValues( createSuccessfulRemoteDataObject$(Object.assign(new Group(), mockGroup)), - createSuccessfulRemoteDataObject$(Object.assign(new Group(), mockGroup)) + createSuccessfulRemoteDataObject$(Object.assign(new Group(), mockGroup)), ); - bitstreamService.getUploadedFileList.and.returnValue(observableOf([])); - comp.onSectionInit(); - const expectedGroupsMap = new Map([ - [mockUploadConfigResponse.accessConditionOptions[1].name, [mockGroup as any]], - [mockUploadConfigResponse.accessConditionOptions[2].name, [mockGroup as any]], - ]); - expect(comp.collectionId).toBe(collectionId); expect(comp.collectionName).toBe(mockCollection.name); expect(comp.availableAccessConditionOptions.length).toBe(4); @@ -262,12 +273,12 @@ describe('SubmissionSectionUploadComponent test suite', () => { expect(comp.required$.getValue()).toBe(true); expect(compAsAny.subs.length).toBe(2); expect(compAsAny.fileList).toEqual([]); - expect(compAsAny.fileIndexes).toEqual([]); expect(compAsAny.fileNames).toEqual([]); - + expect(compAsAny.primaryBitstreamUUID).toEqual(null); }); it('should init file list properly', () => { + bitstreamService.getUploadedFilesData.and.returnValue(observableOf({ primary: null, files: [] })); submissionServiceStub.getSubmissionObject.and.returnValue(observableOf(submissionState)); @@ -279,10 +290,10 @@ describe('SubmissionSectionUploadComponent test suite', () => { groupService.findById.and.returnValues( createSuccessfulRemoteDataObject$(Object.assign(new Group(), mockGroup)), - createSuccessfulRemoteDataObject$(Object.assign(new Group(), mockGroup)) + createSuccessfulRemoteDataObject$(Object.assign(new Group(), mockGroup)), ); - bitstreamService.getUploadedFileList.and.returnValue(observableOf(mockUploadFiles)); + bitstreamService.getUploadedFilesData.and.returnValue(observableOf(mockUploadFilesData)); comp.onSectionInit(); @@ -298,12 +309,14 @@ describe('SubmissionSectionUploadComponent test suite', () => { expect(comp.required$.getValue()).toBe(true); expect(compAsAny.subs.length).toBe(2); expect(compAsAny.fileList).toEqual(mockUploadFiles); - expect(compAsAny.fileIndexes).toEqual(['123456-test-upload']); + expect(compAsAny.primaryBitstreamUUID).toEqual(null); expect(compAsAny.fileNames).toEqual(['123456-test-upload.jpg']); }); it('should properly read the section status when required is true', () => { + bitstreamService.getUploadedFilesData.and.returnValue(observableOf({ primary: null, files: [] })); + submissionServiceStub.getSubmissionObject.and.returnValue(observableOf(submissionState)); collectionDataService.findById.and.returnValue(createSuccessfulRemoteDataObject$(mockCollection)); @@ -314,12 +327,12 @@ describe('SubmissionSectionUploadComponent test suite', () => { groupService.findById.and.returnValues( createSuccessfulRemoteDataObject$(Object.assign(new Group(), mockGroup)), - createSuccessfulRemoteDataObject$(Object.assign(new Group(), mockGroup)) + createSuccessfulRemoteDataObject$(Object.assign(new Group(), mockGroup)), ); bitstreamService.getUploadedFileList.and.returnValue(cold('-a-b', { a: [], - b: mockUploadFiles + b: mockUploadFiles, })); comp.onSectionInit(); @@ -328,13 +341,14 @@ describe('SubmissionSectionUploadComponent test suite', () => { expect(compAsAny.getSectionStatus()).toBeObservable(cold('-c-d', { c: false, - d: true + d: true, })); }); it('should properly read the section status when required is false', () => { submissionServiceStub.getSubmissionObject.and.returnValue(observableOf(submissionState)); + bitstreamService.getUploadedFilesData.and.returnValue(observableOf({ primary: null, files: [] })); collectionDataService.findById.and.returnValue(createSuccessfulRemoteDataObject$(mockCollection)); resourcePolicyService.findByHref.and.returnValue(createSuccessfulRemoteDataObject$(mockDefaultAccessCondition)); @@ -343,12 +357,12 @@ describe('SubmissionSectionUploadComponent test suite', () => { groupService.findById.and.returnValues( createSuccessfulRemoteDataObject$(Object.assign(new Group(), mockGroup)), - createSuccessfulRemoteDataObject$(Object.assign(new Group(), mockGroup)) + createSuccessfulRemoteDataObject$(Object.assign(new Group(), mockGroup)), ); bitstreamService.getUploadedFileList.and.returnValue(cold('-a-b', { a: [], - b: mockUploadFiles + b: mockUploadFiles, })); comp.onSectionInit(); @@ -357,7 +371,7 @@ describe('SubmissionSectionUploadComponent test suite', () => { expect(compAsAny.getSectionStatus()).toBeObservable(cold('-c-d', { c: true, - d: true + d: true, })); }); }); @@ -366,7 +380,10 @@ describe('SubmissionSectionUploadComponent test suite', () => { // declare a test component @Component({ selector: 'ds-test-cmp', - template: `` + 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 eefed8a36b5..58008c9dfb6 100644 --- a/src/app/submission/sections/upload/section-upload.component.ts +++ b/src/app/submission/sections/upload/section-upload.component.ts @@ -1,36 +1,60 @@ -import { ChangeDetectorRef, Component, Inject } from '@angular/core'; - +import { + AsyncPipe, + NgForOf, + NgIf, +} from '@angular/common'; +import { + ChangeDetectorRef, + Component, + Inject, +} from '@angular/core'; +import { TranslateModule } from '@ngx-translate/core'; import { BehaviorSubject, + combineLatest, combineLatest as observableCombineLatest, Observable, - Subscription + Subscription, } from 'rxjs'; -import { distinctUntilChanged, filter, map, mergeMap, switchMap, tap } from 'rxjs/operators'; +import { + distinctUntilChanged, + filter, + map, + mergeMap, + switchMap, + tap, +} from 'rxjs/operators'; +import { WorkspaceitemSectionUploadObject } from 'src/app/core/submission/models/workspaceitem-section-upload.model'; -import { SectionModelComponent } from '../models/section.model'; -import { hasValue, isNotEmpty, isNotUndefined, isUndefined } from '../../../shared/empty.util'; -import { SectionUploadService } from './section-upload.service'; -import { CollectionDataService } from '../../../core/data/collection-data.service'; -import { GroupDataService } from '../../../core/eperson/group-data.service'; -import { ResourcePolicyDataService } from '../../../core/resource-policy/resource-policy-data.service'; -import { SubmissionUploadsConfigDataService } from '../../../core/config/submission-uploads-config-data.service'; -import { SubmissionUploadsModel } from '../../../core/config/models/config-submission-uploads.model'; +import { DSONameService } from '../../../core/breadcrumbs/dso-name.service'; +import { AccessConditionOption } from '../../../core/config/models/config-access-condition-option.model'; import { SubmissionFormsModel } from '../../../core/config/models/config-submission-forms.model'; -import { SectionsType } from '../sections-type'; -import { renderSectionFor } from '../sections-decorator'; -import { SectionDataObject } from '../models/section-data.model'; -import { SubmissionObjectEntry } from '../../objects/submission-objects.reducer'; -import { AlertType } from '../../../shared/alert/aletr-type'; +import { SubmissionUploadsModel } from '../../../core/config/models/config-submission-uploads.model'; +import { SubmissionUploadsConfigDataService } from '../../../core/config/submission-uploads-config-data.service'; +import { CollectionDataService } from '../../../core/data/collection-data.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 { SectionsService } from '../sections.service'; -import { SubmissionService } from '../../submission.service'; +import { ResourcePolicyDataService } from '../../../core/resource-policy/resource-policy-data.service'; import { Collection } from '../../../core/shared/collection.model'; -import { AccessConditionOption } from '../../../core/config/models/config-access-condition-option.model'; -import { followLink } from '../../../shared/utils/follow-link-config.model'; import { getFirstSucceededRemoteData } from '../../../core/shared/operators'; -import { DSONameService } from '../../../core/breadcrumbs/dso-name.service'; +import { AlertComponent } from '../../../shared/alert/alert.component'; +import { AlertType } from '../../../shared/alert/alert-type'; +import { + hasValue, + isNotEmpty, + isNotUndefined, + isUndefined, +} from '../../../shared/empty.util'; +import { followLink } from '../../../shared/utils/follow-link-config.model'; +import { SubmissionObjectEntry } from '../../objects/submission-objects.reducer'; +import { SubmissionService } from '../../submission.service'; +import { SectionModelComponent } from '../models/section.model'; +import { SectionDataObject } from '../models/section-data.model'; +import { SectionsService } from '../sections.service'; +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 export const POLICY_DEFAULT_WITH_LIST = 2; // Banner2 @@ -47,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 { /** @@ -58,10 +91,10 @@ export class SubmissionSectionUploadComponent extends SectionModelComponent { public AlertTypeEnum = AlertType; /** - * The array containing the keys of file list array + * The uuid of primary bitstream file * @type {Array} */ - public fileIndexes: string[] = []; + public primaryBitstreamUUID: string | null = null; /** * The file list @@ -158,8 +191,8 @@ export class SubmissionSectionUploadComponent extends SectionModelComponent { switchMap((config: SubmissionUploadsModel) => config.metadata.pipe( getFirstSucceededRemoteData(), - map((remoteData: RemoteData) => remoteData.payload) - ) + map((remoteData: RemoteData) => remoteData.payload), + ), )); this.subs.push( @@ -171,7 +204,7 @@ export class SubmissionSectionUploadComponent extends SectionModelComponent { filter((rd: RemoteData) => isNotUndefined((rd.payload))), tap((collectionRemoteData: RemoteData) => this.collectionName = this.dsoNameService.getName(collectionRemoteData.payload)), // TODO review this part when https://github.com/DSpace/dspace-angular/issues/575 is resolved -/* mergeMap((collectionRemoteData: RemoteData) => { + /* mergeMap((collectionRemoteData: RemoteData) => { return this.resourcePolicyService.findByHref( (collectionRemoteData.payload as any)._links.defaultAccessConditions.href ); @@ -194,29 +227,21 @@ export class SubmissionSectionUploadComponent extends SectionModelComponent { this.changeDetectorRef.detectChanges(); }), - // retrieve submission's bitstreams from state - observableCombineLatest(this.configMetadataForm$, - this.bitstreamService.getUploadedFileList(this.submissionId, this.sectionData.id)).pipe( - filter(([configMetadataForm, fileList]: [SubmissionFormsModel, any[]]) => { - return isNotEmpty(configMetadataForm) && isNotUndefined(fileList); + // retrieve submission's bitstream data from state + 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, fileList]: [SubmissionFormsModel, any[]]) => { - this.fileList = []; - this.fileIndexes = []; - this.fileNames = []; - this.changeDetectorRef.detectChanges(); - if (isNotUndefined(fileList) && fileList.length > 0) { - fileList.forEach((file) => { - this.fileList.push(file); - this.fileIndexes.push(file.uuid); - this.fileNames.push(this.getFileName(configMetadataForm, file)); - }); - } - - this.changeDetectorRef.detectChanges(); - } - ) + 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.spec.ts b/src/app/submission/sections/upload/section-upload.service.spec.ts new file mode 100644 index 00000000000..c57aac0ac93 --- /dev/null +++ b/src/app/submission/sections/upload/section-upload.service.spec.ts @@ -0,0 +1,70 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { + TestBed, + waitForAsync, +} from '@angular/core/testing'; +import { + Store, + StoreModule, +} from '@ngrx/store'; +import { JsonPatchOperationPathCombiner } from 'src/app/core/json-patch/builder/json-patch-operation-path-combiner'; +import { JsonPatchOperationsBuilder } from 'src/app/core/json-patch/builder/json-patch-operations-builder'; + +import { SectionUploadService } from './section-upload.service'; + +const jsonPatchOpBuilder: any = jasmine.createSpyObj('jsonPatchOpBuilder', { + add: jasmine.createSpy('add'), + replace: jasmine.createSpy('replace'), + remove: jasmine.createSpy('remove'), +}); + +describe('SectionUploadService test suite', () => { + let sectionUploadService: SectionUploadService; + let operationsBuilder: any; + const pathCombiner = new JsonPatchOperationPathCombiner('sections', 'upload'); + const primaryPath = pathCombiner.getPath('primary'); + const fileId = 'test'; + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [StoreModule], + providers: [ + { provide: Store, useValue: {} }, + SectionUploadService, + { provide: JsonPatchOperationsBuilder, useValue: jsonPatchOpBuilder }, + ], + schemas: [NO_ERRORS_SCHEMA], + }); + })); + + beforeEach(() => { + sectionUploadService = TestBed.inject(SectionUploadService); + operationsBuilder = TestBed.inject(JsonPatchOperationsBuilder); + }); + + [ + { + initialPrimary: null, + primary: true, + operationName: 'add', + expected: [primaryPath, fileId, false, true], + }, + { + initialPrimary: true, + primary: false, + operationName: 'remove', + expected: [primaryPath], + }, + { + initialPrimary: false, + primary: true, + operationName: 'replace', + expected: [primaryPath, fileId, true], + }, + ].forEach(({ initialPrimary, primary, operationName, expected }) => { + it(`updatePrimaryBitstreamOperation should add ${operationName} operation`, () => { + const path = pathCombiner.getPath('primary'); + sectionUploadService.updatePrimaryBitstreamOperation(path, initialPrimary, primary, fileId); + expect(operationsBuilder[operationName]).toHaveBeenCalledWith(...expected); + }); + }); +}); diff --git a/src/app/submission/sections/upload/section-upload.service.ts b/src/app/submission/sections/upload/section-upload.service.ts index a851fa9dafa..b4abe366c04 100644 --- a/src/app/submission/sections/upload/section-upload.service.ts +++ b/src/app/submission/sections/upload/section-upload.service.ts @@ -1,31 +1,87 @@ import { Injectable } from '@angular/core'; - -import { Observable } from 'rxjs'; -import { distinctUntilChanged, filter, map } from 'rxjs/operators'; import { Store } from '@ngrx/store'; +import { Observable } from 'rxjs'; +import { + distinctUntilChanged, + filter, + map, +} from 'rxjs/operators'; +import { JsonPatchOperationPathObject } from 'src/app/core/json-patch/builder/json-patch-operation-path-combiner'; +import { JsonPatchOperationsBuilder } from 'src/app/core/json-patch/builder/json-patch-operations-builder'; +import { WorkspaceitemSectionUploadObject } from 'src/app/core/submission/models/workspaceitem-section-upload.model'; -import { SubmissionState } from '../../submission.reducers'; +import { WorkspaceitemSectionUploadFileObject } from '../../../core/submission/models/workspaceitem-section-upload-file.model'; +import { isUndefined } from '../../../shared/empty.util'; import { DeleteUploadedFileAction, EditFileDataAction, - NewUploadedFileAction + EditFilePrimaryBitstreamAction, + NewUploadedFileAction, } from '../../objects/submission-objects.actions'; -import { submissionUploadedFileFromUuidSelector, submissionUploadedFilesFromIdSelector } from '../../selectors'; -import { isUndefined } from '../../../shared/empty.util'; -import { WorkspaceitemSectionUploadFileObject } from '../../../core/submission/models/workspaceitem-section-upload-file.model'; +import { + submissionSectionDataFromIdSelector, + submissionUploadedFileFromUuidSelector, + submissionUploadedFilesFromIdSelector, +} from '../../selectors'; +import { SubmissionState } from '../../submission.reducers'; /** * A service that provides methods to handle submission's bitstream state. */ -@Injectable() +@Injectable({ providedIn: 'root' }) export class SectionUploadService { /** * Initialize service variables * * @param {Store} store + * @param {JsonPatchOperationsBuilder} operationsBuilder */ - constructor(private store: Store) {} + constructor(private store: Store, private operationsBuilder: JsonPatchOperationsBuilder) {} + + /** + * Define and add an operation based on a change + * + * @param path + * The path to endpoint + * @param intitialPrimary + * The initial primary indicator + * @param primary + * the new primary indicator + * @param fileId + * The file id + * @returns {void} + */ + public updatePrimaryBitstreamOperation(path: JsonPatchOperationPathObject, intitialPrimary: boolean | null, primary: boolean | null, fileId: string): void { + if (intitialPrimary === null && primary) { + this.operationsBuilder.add(path, fileId, false, true); + return; + } + + if (intitialPrimary !== primary) { + if (primary) { + this.operationsBuilder.replace(path, fileId, true); + return; + } + this.operationsBuilder.remove(path); + } + } + + /** + * Return submission's bitstream data from state + * + * @param submissionId + * The submission id + * @param sectionId + * The section id + * @returns {WorkspaceitemSectionUploadObject} + * Returns submission's bitstream data + */ + public getUploadedFilesData(submissionId: string, sectionId: string): Observable { + return this.store.select(submissionSectionDataFromIdSelector(submissionId, sectionId)).pipe( + map((state) => state), + distinctUntilChanged()); + } /** * Return submission's bitstream list from state @@ -100,7 +156,23 @@ export class SectionUploadService { */ public addUploadedFile(submissionId: string, sectionId: string, fileUUID: string, data: WorkspaceitemSectionUploadFileObject) { this.store.dispatch( - new NewUploadedFileAction(submissionId, sectionId, fileUUID, data) + new NewUploadedFileAction(submissionId, sectionId, fileUUID, data), + ); + } + + /** + * Update primary bitstream into the state + * + * @param submissionId + * The submission id + * @param sectionId + * The section id + * @param fileUUID + * The bitstream UUID + */ + public updateFilePrimaryBitstream(submissionId: string, sectionId: string, fileUUID: string | null) { + this.store.dispatch( + new EditFilePrimaryBitstreamAction(submissionId, sectionId, fileUUID), ); } @@ -118,7 +190,7 @@ export class SectionUploadService { */ public updateFileData(submissionId: string, sectionId: string, fileUUID: string, data: WorkspaceitemSectionUploadFileObject) { this.store.dispatch( - new EditFileDataAction(submissionId, sectionId, fileUUID, data) + new EditFileDataAction(submissionId, sectionId, fileUUID, data), ); } @@ -134,7 +206,7 @@ export class SectionUploadService { */ public removeUploadedFile(submissionId: string, sectionId: string, fileUUID: string) { this.store.dispatch( - new DeleteUploadedFileAction(submissionId, sectionId, fileUUID) + new DeleteUploadedFileAction(submissionId, sectionId, fileUUID), ); } } diff --git a/src/app/submission/selectors.ts b/src/app/submission/selectors.ts index 6284aca8925..432a5377a5a 100644 --- a/src/app/submission/selectors.ts +++ b/src/app/submission/selectors.ts @@ -1,9 +1,16 @@ -import { createSelector, MemoizedSelector, Selector } from '@ngrx/store'; +import { + createSelector, + MemoizedSelector, + Selector, +} from '@ngrx/store'; import { hasValue } from '../shared/empty.util'; -import { submissionSelector, SubmissionState } from './submission.reducers'; -import { SubmissionObjectEntry} from './objects/submission-objects.reducer'; +import { SubmissionObjectEntry } from './objects/submission-objects.reducer'; import { SubmissionSectionObject } from './objects/submission-section-object.model'; +import { + submissionSelector, + SubmissionState, +} from './submission.reducers'; /** * Export a function to return a subset of the state by key diff --git a/src/app/submission/server-submission.service.ts b/src/app/submission/server-submission.service.ts index 3aa55a9d58e..993e38cdaad 100644 --- a/src/app/submission/server-submission.service.ts +++ b/src/app/submission/server-submission.service.ts @@ -1,10 +1,12 @@ import { Injectable } from '@angular/core'; +import { + Observable, + of as observableOf, +} from 'rxjs'; -import { Observable, of as observableOf } from 'rxjs'; - -import { SubmissionService } from './submission.service'; -import { SubmissionObject } from '../core/submission/models/submission-object.model'; import { RemoteData } from '../core/data/remote-data'; +import { SubmissionObject } from '../core/submission/models/submission-object.model'; +import { SubmissionService } from './submission.service'; /** * Instance of SubmissionService used on SSR. diff --git a/src/app/submission/submission.effects.ts b/src/app/submission/submission.effects.ts index 30e01451d13..5a4105e4ab3 100644 --- a/src/app/submission/submission.effects.ts +++ b/src/app/submission/submission.effects.ts @@ -1,5 +1,5 @@ import { SubmissionObjectEffects } from './objects/submission-objects.effects'; export const submissionEffects = [ - SubmissionObjectEffects + SubmissionObjectEffects, ]; diff --git a/src/app/submission/submission.module.ts b/src/app/submission/submission.module.ts deleted file mode 100644 index cf0ab2b369a..00000000000 --- a/src/app/submission/submission.module.ts +++ /dev/null @@ -1,155 +0,0 @@ -import { NgModule } from '@angular/core'; -import { CoreModule } from '../core/core.module'; -import { SharedModule } from '../shared/shared.module'; - -import { SubmissionSectionFormComponent } from './sections/form/section-form.component'; -import { SectionsDirective } from './sections/sections.directive'; -import { SectionsService } from './sections/sections.service'; -import { SubmissionFormCollectionComponent } from './form/collection/submission-form-collection.component'; -import { SubmissionFormFooterComponent } from './form/footer/submission-form-footer.component'; -import { SubmissionFormComponent } from './form/submission-form.component'; -import { SubmissionFormSectionAddComponent } from './form/section-add/submission-form-section-add.component'; -import { SubmissionSectionContainerComponent } from './sections/container/section-container.component'; -import { CommonModule } from '@angular/common'; -import { Action, StoreConfig, StoreModule } from '@ngrx/store'; -import { EffectsModule } from '@ngrx/effects'; -import { submissionReducers, SubmissionState } from './submission.reducers'; -import { submissionEffects } from './submission.effects'; -import { SubmissionSectionUploadComponent } from './sections/upload/section-upload.component'; -import { SectionUploadService } from './sections/upload/section-upload.service'; -import { SubmissionUploadFilesComponent } from './form/submission-upload-files/submission-upload-files.component'; -import { SubmissionSectionLicenseComponent } from './sections/license/section-license.component'; -import { SubmissionUploadsConfigDataService } from '../core/config/submission-uploads-config-data.service'; -import { SubmissionEditComponent } from './edit/submission-edit.component'; -import { SubmissionSectionUploadFileComponent } from './sections/upload/file/section-upload-file.component'; -import { - SubmissionSectionUploadFileEditComponent -} from './sections/upload/file/edit/section-upload-file-edit.component'; -import { - SubmissionSectionUploadFileViewComponent -} from './sections/upload/file/view/section-upload-file-view.component'; -import { - SubmissionSectionUploadAccessConditionsComponent -} from './sections/upload/accessConditions/submission-section-upload-access-conditions.component'; -import { SubmissionSubmitComponent } from './submit/submission-submit.component'; -import { storeModuleConfig } from '../app.reducer'; -import { SubmissionImportExternalComponent } from './import-external/submission-import-external.component'; -import { - SubmissionImportExternalSearchbarComponent -} from './import-external/import-external-searchbar/submission-import-external-searchbar.component'; -import { - SubmissionImportExternalPreviewComponent -} from './import-external/import-external-preview/submission-import-external-preview.component'; -import { - SubmissionImportExternalCollectionComponent -} from './import-external/import-external-collection/submission-import-external-collection.component'; -import { SubmissionSectionCcLicensesComponent } from './sections/cc-license/submission-section-cc-licenses.component'; -import { JournalEntitiesModule } from '../entity-groups/journal-entities/journal-entities.module'; -import { ResearchEntitiesModule } from '../entity-groups/research-entities/research-entities.module'; -import { ThemedSubmissionEditComponent } from './edit/themed-submission-edit.component'; -import { ThemedSubmissionSubmitComponent } from './submit/themed-submission-submit.component'; -import { ThemedSubmissionImportExternalComponent } from './import-external/themed-submission-import-external.component'; -import { ThemedSubmissionSectionUploadFileComponent } from './sections/upload/file/themed-section-upload-file.component'; -import { FormModule } from '../shared/form/form.module'; -import { NgbAccordionModule, NgbCollapseModule, NgbModalModule } from '@ng-bootstrap/ng-bootstrap'; -import { SubmissionSectionAccessesComponent } from './sections/accesses/section-accesses.component'; -import { SubmissionAccessesConfigDataService } from '../core/config/submission-accesses-config-data.service'; -import { SectionAccessesService } from './sections/accesses/section-accesses.service'; -import { SubmissionSectionSherpaPoliciesComponent } from './sections/sherpa-policies/section-sherpa-policies.component'; -import { ContentAccordionComponent } from './sections/sherpa-policies/content-accordion/content-accordion.component'; -import { PublisherPolicyComponent } from './sections/sherpa-policies/publisher-policy/publisher-policy.component'; -import { - PublicationInformationComponent -} from './sections/sherpa-policies/publication-information/publication-information.component'; -import { UploadModule } from '../shared/upload/upload.module'; -import { - MetadataInformationComponent -} from './sections/sherpa-policies/metadata-information/metadata-information.component'; -import { SectionFormOperationsService } from './sections/form/section-form-operations.service'; -import {SubmissionSectionIdentifiersComponent} from './sections/identifiers/section-identifiers.component'; - -const ENTRY_COMPONENTS = [ - // put only entry components that use custom decorator - SubmissionSectionUploadComponent, - SubmissionSectionFormComponent, - SubmissionSectionLicenseComponent, - SubmissionSectionCcLicensesComponent, - SubmissionSectionAccessesComponent, - SubmissionSectionSherpaPoliciesComponent, -]; - -const DECLARATIONS = [ - ...ENTRY_COMPONENTS, - SectionsDirective, - SubmissionEditComponent, - ThemedSubmissionEditComponent, - SubmissionFormSectionAddComponent, - SubmissionFormCollectionComponent, - SubmissionFormComponent, - SubmissionFormFooterComponent, - SubmissionSubmitComponent, - ThemedSubmissionSubmitComponent, - SubmissionUploadFilesComponent, - SubmissionSectionContainerComponent, - SubmissionSectionUploadAccessConditionsComponent, - SubmissionSectionUploadFileComponent, - SubmissionSectionUploadFileEditComponent, - SubmissionSectionUploadFileViewComponent, - SubmissionSectionIdentifiersComponent, - 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(submissionEffects), - JournalEntitiesModule.withEntryComponents(), - ResearchEntitiesModule.withEntryComponents(), - FormModule, - NgbModalModule, - NgbCollapseModule, - NgbAccordionModule, - UploadModule, - ], - declarations: DECLARATIONS, - exports: [ - ...DECLARATIONS, - FormModule, - ], - providers: [ - SectionUploadService, - SectionsService, - SubmissionUploadsConfigDataService, - SubmissionAccessesConfigDataService, - SectionAccessesService, - SectionFormOperationsService, - ] -}) - -/** - * 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.reducers.ts b/src/app/submission/submission.reducers.ts index 8d98a7d5c19..23cacbf13fe 100644 --- a/src/app/submission/submission.reducers.ts +++ b/src/app/submission/submission.reducers.ts @@ -1,8 +1,11 @@ -import { ActionReducerMap, createFeatureSelector } from '@ngrx/store'; +import { + ActionReducerMap, + createFeatureSelector, +} from '@ngrx/store'; import { submissionObjectReducer, - SubmissionObjectState + SubmissionObjectState, } from './objects/submission-objects.reducer'; /** diff --git a/src/app/submission/submission.service.spec.ts b/src/app/submission/submission.service.spec.ts index 1e2be5b6121..0ba8e5d9a21 100644 --- a/src/app/submission/submission.service.spec.ts +++ b/src/app/submission/submission.service.spec.ts @@ -1,25 +1,57 @@ -import { StoreModule } from '@ngrx/store'; -import { fakeAsync, flush, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { ActivatedRoute, Router } from '@angular/router'; import { HttpHeaders } from '@angular/common/http'; - -import { of as observableOf, throwError as observableThrowError } from 'rxjs'; +import { + fakeAsync, + flush, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; +import { + ActivatedRoute, + Router, +} from '@angular/router'; +import { StoreModule } from '@ngrx/store'; +import { + TranslateLoader, + TranslateModule, + TranslateService, +} from '@ngx-translate/core'; +import { + cold, + getTestScheduler, + hot, +} from 'jasmine-marbles'; +import { + of as observableOf, + throwError as observableThrowError, +} from 'rxjs'; import { TestScheduler } from 'rxjs/testing'; -import { TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core'; -import { cold, getTestScheduler, hot, } from 'jasmine-marbles'; -import { RouterMock } from '../shared/mocks/router.mock'; -import { SubmissionService } from './submission.service'; -import { submissionReducers } from './submission.reducers'; -import { SubmissionRestService } from '../core/submission/submission-rest.service'; -import { RouteService } from '../core/services/route.service'; -import { SubmissionRestServiceStub } from '../shared/testing/submission-rest-service.stub'; -import { MockActivatedRoute } from '../shared/mocks/active-router.mock'; +import { environment } from '../../environments/environment'; +import { storeModuleConfig } from '../app.reducer'; +import { ErrorResponse } from '../core/cache/response.models'; +import { RequestService } from '../core/data/request.service'; +import { RequestError } from '../core/data/request-error.model'; import { HttpOptions } from '../core/dspace-rest/dspace-rest.service'; +import { RouteService } from '../core/services/route.service'; +import { Item } from '../core/shared/item.model'; +import { SearchService } from '../core/shared/search/search.service'; +import { SubmissionJsonPatchOperationsService } from '../core/submission/submission-json-patch-operations.service'; +import { SubmissionRestService } from '../core/submission/submission-rest.service'; import { SubmissionScopeType } from '../core/submission/submission-scope-type'; -import { mockSubmissionDefinition, mockSubmissionRestResponse } from '../shared/mocks/submission.mock'; -import { NotificationsService } from '../shared/notifications/notifications.service'; +import { MockActivatedRoute } from '../shared/mocks/active-router.mock'; +import { getMockRequestService } from '../shared/mocks/request.service.mock'; +import { RouterMock } from '../shared/mocks/router.mock'; +import { getMockSearchService } from '../shared/mocks/search-service.mock'; +import { + mockSubmissionDefinition, + mockSubmissionRestResponse, +} from '../shared/mocks/submission.mock'; import { TranslateLoaderMock } from '../shared/mocks/translate-loader.mock'; +import { NotificationsService } from '../shared/notifications/notifications.service'; +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 { CancelSubmissionFormAction, ChangeSubmissionCollectionAction, @@ -30,18 +62,10 @@ import { SaveForLaterSubmissionFormAction, SaveSubmissionFormAction, SaveSubmissionSectionFormAction, - SetActiveSectionAction + SetActiveSectionAction, } from './objects/submission-objects.actions'; -import { createFailedRemoteDataObject, } from '../shared/remote-data.utils'; -import { getMockSearchService } from '../shared/mocks/search-service.mock'; -import { getMockRequestService } from '../shared/mocks/request.service.mock'; -import { RequestService } from '../core/data/request.service'; -import { SearchService } from '../core/shared/search/search.service'; -import { Item } from '../core/shared/item.model'; -import { storeModuleConfig } from '../app.reducer'; -import { environment } from '../../environments/environment'; -import { SubmissionJsonPatchOperationsService } from '../core/submission/submission-json-patch-operations.service'; -import { SubmissionJsonPatchOperationsServiceStub } from '../shared/testing/submission-json-patch-operations-service.stub'; +import { submissionReducers } from './submission.reducers'; +import { SubmissionService } from './submission.service'; describe('SubmissionService test suite', () => { const collectionId = '43fe1f8c-09a6-4fcf-9c78-5d4fed8f2c8f'; @@ -61,7 +85,7 @@ describe('SubmissionService test suite', () => { sectionType: 'utils', visibility: { main: 'HIDDEN', - other: 'HIDDEN' + other: 'HIDDEN', }, collapsed: false, enabled: true, @@ -69,7 +93,7 @@ describe('SubmissionService test suite', () => { errorsToShow: [], serverValidationErrors: [], isLoading: false, - isValid: false + isValid: false, }, collection: { config: '', @@ -77,7 +101,7 @@ describe('SubmissionService test suite', () => { sectionType: 'collection', visibility: { main: 'HIDDEN', - other: 'HIDDEN' + other: 'HIDDEN', }, collapsed: false, enabled: true, @@ -85,7 +109,7 @@ describe('SubmissionService test suite', () => { errorsToShow: [], serverValidationErrors: [], isLoading: false, - isValid: false + isValid: false, }, keyinformation: { header: 'submit.progressbar.describe.keyinformation', @@ -98,7 +122,7 @@ describe('SubmissionService test suite', () => { errorsToShow: [], serverValidationErrors: [], isLoading: false, - isValid: false + isValid: false, }, indexing: { header: 'submit.progressbar.describe.indexing', @@ -111,7 +135,7 @@ describe('SubmissionService test suite', () => { errorsToShow: [], serverValidationErrors: [], isLoading: false, - isValid: false + isValid: false, }, publicationchannel: { header: 'submit.progressbar.describe.publicationchannel', @@ -124,7 +148,7 @@ describe('SubmissionService test suite', () => { errorsToShow: [], serverValidationErrors: [], isLoading: false, - isValid: true + isValid: true, }, acknowledgement: { header: 'submit.progressbar.describe.acknowledgement', @@ -137,7 +161,7 @@ describe('SubmissionService test suite', () => { errorsToShow: [], serverValidationErrors: [], isLoading: false, - isValid: false + isValid: false, }, identifiers: { header: 'submit.progressbar.describe.identifiers', @@ -150,7 +174,7 @@ describe('SubmissionService test suite', () => { errorsToShow: [], serverValidationErrors: [], isLoading: false, - isValid: false + isValid: false, }, references: { header: 'submit.progressbar.describe.references', @@ -163,7 +187,7 @@ describe('SubmissionService test suite', () => { errorsToShow: [], serverValidationErrors: [], isLoading: false, - isValid: false + isValid: false, }, upload: { header: 'submit.progressbar.upload', @@ -176,7 +200,7 @@ describe('SubmissionService test suite', () => { errorsToShow: [], serverValidationErrors: [], isLoading: false, - isValid: false + isValid: false, }, license: { header: 'submit.progressbar.license', @@ -185,7 +209,7 @@ describe('SubmissionService test suite', () => { sectionType: 'license', visibility: { main: null, - other: 'READONLY' + other: 'READONLY', }, collapsed: false, enabled: true, @@ -193,14 +217,14 @@ describe('SubmissionService test suite', () => { errorsToShow: [], serverValidationErrors: [], isLoading: false, - isValid: false - } + isValid: false, + }, }, isLoading: false, savePending: false, - depositPending: false - } - } + depositPending: false, + }, + }, }; const validSubState = { objects: { @@ -216,7 +240,7 @@ describe('SubmissionService test suite', () => { sectionType: 'utils', visibility: { main: 'HIDDEN', - other: 'HIDDEN' + other: 'HIDDEN', }, collapsed: false, enabled: true, @@ -224,7 +248,7 @@ describe('SubmissionService test suite', () => { errorsToShow: [], serverValidationErrors: [], isLoading: false, - isValid: false + isValid: false, }, collection: { config: '', @@ -232,7 +256,7 @@ describe('SubmissionService test suite', () => { sectionType: 'collection', visibility: { main: 'HIDDEN', - other: 'HIDDEN' + other: 'HIDDEN', }, collapsed: false, enabled: true, @@ -240,7 +264,7 @@ describe('SubmissionService test suite', () => { errorsToShow: [], serverValidationErrors: [], isLoading: false, - isValid: false + isValid: false, }, keyinformation: { header: 'submit.progressbar.describe.keyinformation', @@ -253,7 +277,7 @@ describe('SubmissionService test suite', () => { errorsToShow: [], serverValidationErrors: [], isLoading: false, - isValid: true + isValid: true, }, indexing: { header: 'submit.progressbar.describe.indexing', @@ -266,7 +290,7 @@ describe('SubmissionService test suite', () => { errorsToShow: [], serverValidationErrors: [], isLoading: false, - isValid: false + isValid: false, }, publicationchannel: { header: 'submit.progressbar.describe.publicationchannel', @@ -279,7 +303,7 @@ describe('SubmissionService test suite', () => { errorsToShow: [], serverValidationErrors: [], isLoading: false, - isValid: true + isValid: true, }, acknowledgement: { header: 'submit.progressbar.describe.acknowledgement', @@ -292,7 +316,7 @@ describe('SubmissionService test suite', () => { errorsToShow: [], serverValidationErrors: [], isLoading: false, - isValid: false + isValid: false, }, identifiers: { header: 'submit.progressbar.describe.identifiers', @@ -305,7 +329,7 @@ describe('SubmissionService test suite', () => { errorsToShow: [], serverValidationErrors: [], isLoading: false, - isValid: false + isValid: false, }, references: { header: 'submit.progressbar.describe.references', @@ -318,7 +342,7 @@ describe('SubmissionService test suite', () => { errorsToShow: [], serverValidationErrors: [], isLoading: false, - isValid: false + isValid: false, }, upload: { header: 'submit.progressbar.upload', @@ -331,7 +355,7 @@ describe('SubmissionService test suite', () => { errorsToShow: [], serverValidationErrors: [], isLoading: false, - isValid: true + isValid: true, }, license: { header: 'submit.progressbar.license', @@ -340,7 +364,7 @@ describe('SubmissionService test suite', () => { sectionType: 'license', visibility: { main: null, - other: 'READONLY' + other: 'READONLY', }, collapsed: false, enabled: true, @@ -348,14 +372,14 @@ describe('SubmissionService test suite', () => { errorsToShow: [], serverValidationErrors: [], isLoading: false, - isValid: true - } + isValid: true, + }, }, isLoading: false, savePending: false, - depositPending: false - } - } + depositPending: false, + }, + }, }; const restService = new SubmissionRestServiceStub(); const router = new RouterMock(); @@ -378,9 +402,9 @@ describe('SubmissionService test suite', () => { TranslateModule.forRoot({ loader: { provide: TranslateLoader, - useClass: TranslateLoaderMock - } - }) + useClass: TranslateLoaderMock, + }, + }), ], providers: [ { provide: Router, useValue: router }, @@ -392,8 +416,8 @@ describe('SubmissionService test suite', () => { NotificationsService, RouteService, SubmissionService, - TranslateService - ] + TranslateService, + ], }).compileComponents(); })); @@ -470,7 +494,7 @@ describe('SubmissionService test suite', () => { submissionDefinition, {}, new Item(), - null + null, ); const expected = new InitSubmissionFormAction( collectionId, @@ -487,7 +511,7 @@ describe('SubmissionService test suite', () => { describe('dispatchDeposit', () => { it('should dispatch a new SaveAndDepositSubmissionAction', () => { - service.dispatchDeposit(submissionId,); + service.dispatchDeposit(submissionId); const expected = new SaveAndDepositSubmissionAction(submissionId); expect((service as any).store.dispatch).toHaveBeenCalledWith(expected); @@ -496,7 +520,7 @@ describe('SubmissionService test suite', () => { describe('dispatchDiscard', () => { it('should dispatch a new DiscardSubmissionAction', () => { - service.dispatchDiscard(submissionId,); + service.dispatchDiscard(submissionId); const expected = new DiscardSubmissionAction(submissionId); expect((service as any).store.dispatch).toHaveBeenCalledWith(expected); @@ -521,7 +545,7 @@ describe('SubmissionService test suite', () => { describe('dispatchSaveForLater', () => { it('should dispatch a new SaveForLaterSubmissionFormAction', () => { - service.dispatchSaveForLater(submissionId,); + service.dispatchSaveForLater(submissionId); const expected = new SaveForLaterSubmissionFormAction(submissionId); expect((service as any).store.dispatch).toHaveBeenCalledWith(expected); @@ -540,7 +564,7 @@ describe('SubmissionService test suite', () => { describe('getSubmissionObject', () => { it('should return submission object state from the store', () => { spyOn((service as any).store, 'select').and.returnValue(hot('a', { - a: subState.objects[826] + a: subState.objects[826], })); const result = service.getSubmissionObject('826'); @@ -553,7 +577,7 @@ describe('SubmissionService test suite', () => { describe('getActiveSectionId', () => { it('should return current active submission form section', () => { spyOn((service as any).store, 'select').and.returnValue(hot('a', { - a: subState.objects[826] + a: subState.objects[826], })); const result = service.getActiveSectionId('826'); @@ -567,7 +591,7 @@ describe('SubmissionService test suite', () => { describe('getSubmissionSections', () => { it('should return submission form sections', () => { spyOn((service as any).store, 'select').and.returnValue(hot('a|', { - a: subState.objects[826] + a: subState.objects[826], })); const result = service.getSubmissionSections('826'); @@ -583,7 +607,7 @@ describe('SubmissionService test suite', () => { sectionType: 'submission-form', data: {}, errorsToShow: [], - serverValidationErrors: [] + serverValidationErrors: [], }, { header: 'submit.progressbar.describe.indexing', @@ -593,7 +617,7 @@ describe('SubmissionService test suite', () => { sectionType: 'submission-form', data: {}, errorsToShow: [], - serverValidationErrors: [] + serverValidationErrors: [], }, { header: 'submit.progressbar.describe.publicationchannel', @@ -603,7 +627,7 @@ describe('SubmissionService test suite', () => { sectionType: 'submission-form', data: {}, errorsToShow: [], - serverValidationErrors: [] + serverValidationErrors: [], }, { header: 'submit.progressbar.describe.acknowledgement', @@ -613,7 +637,7 @@ describe('SubmissionService test suite', () => { sectionType: 'submission-form', data: {}, errorsToShow: [], - serverValidationErrors: [] + serverValidationErrors: [], }, { header: 'submit.progressbar.describe.identifiers', @@ -623,7 +647,7 @@ describe('SubmissionService test suite', () => { sectionType: 'submission-form', data: {}, errorsToShow: [], - serverValidationErrors: [] + serverValidationErrors: [], }, { header: 'submit.progressbar.describe.references', @@ -633,7 +657,7 @@ describe('SubmissionService test suite', () => { sectionType: 'submission-form', data: {}, errorsToShow: [], - serverValidationErrors: [] + serverValidationErrors: [], }, { header: 'submit.progressbar.upload', @@ -643,7 +667,7 @@ describe('SubmissionService test suite', () => { sectionType: 'upload', data: {}, errorsToShow: [], - serverValidationErrors: [] + serverValidationErrors: [], }, { header: 'submit.progressbar.license', @@ -653,9 +677,9 @@ describe('SubmissionService test suite', () => { sectionType: 'license', data: {}, errorsToShow: [], - serverValidationErrors: [] - } - ] + serverValidationErrors: [], + }, + ], }); expect(result).toBeObservable(expected); @@ -665,7 +689,7 @@ describe('SubmissionService test suite', () => { describe('getDisabledSectionsList', () => { it('should return list of submission disabled sections', () => { spyOn((service as any).store, 'select').and.returnValue(hot('-a|', { - a: subState.objects[826] + a: subState.objects[826], })); const result = service.getDisabledSectionsList('826'); @@ -688,8 +712,8 @@ describe('SubmissionService test suite', () => { { header: 'submit.progressbar.describe.references', id: 'references', - } - ] + }, + ], }); expect(result).toBeObservable(expected); @@ -737,12 +761,12 @@ describe('SubmissionService test suite', () => { it('should return properly submission status', () => { spyOn((service as any).store, 'select').and.returnValue(hot('-a-b', { a: subState, - b: validSubState + b: validSubState, })); const result = service.getSubmissionStatus('826'); const expected = cold('cc-d', { c: false, - d: true + d: true, }); expect(result).toBeObservable(expected); @@ -752,12 +776,12 @@ describe('SubmissionService test suite', () => { describe('getSubmissionSaveProcessingStatus', () => { it('should return submission save processing status', () => { spyOn((service as any).store, 'select').and.returnValue(hot('-a', { - a: subState.objects[826] + a: subState.objects[826], })); const result = service.getSubmissionSaveProcessingStatus('826'); const expected = cold('bb', { - b: false + b: false, }); expect(result).toBeObservable(expected); @@ -767,12 +791,12 @@ describe('SubmissionService test suite', () => { describe('getSubmissionDepositProcessingStatus', () => { it('should return submission deposit processing status', () => { spyOn((service as any).store, 'select').and.returnValue(hot('-a', { - a: subState.objects[826] + a: subState.objects[826], })); const result = service.getSubmissionDepositProcessingStatus('826'); const expected = cold('bb', { - b: false + b: false, }); expect(result).toBeObservable(expected); @@ -802,7 +826,7 @@ describe('SubmissionService test suite', () => { sectionType: 'collection' as any, visibility: { main: 'HIDDEN', - other: 'HIDDEN' + other: 'HIDDEN', }, collapsed: false, enabled: true, @@ -810,7 +834,7 @@ describe('SubmissionService test suite', () => { errorsToShow: [], serverValidationErrors: [], isLoading: false, - isValid: false + isValid: false, }; expect(service.isSectionHidden(section)).toBeTruthy(); @@ -825,7 +849,7 @@ describe('SubmissionService test suite', () => { errorsToShow: [], serverValidationErrors: [], isLoading: false, - isValid: false + isValid: false, }; expect(service.isSectionHidden(section)).toBeFalsy(); }); @@ -836,7 +860,7 @@ describe('SubmissionService test suite', () => { const spy = spyOn(service, 'getSubmissionObject').and.returnValue(observableOf({ isLoading: true })); let expected = cold('(b|)', { - b: true + b: true, }); expect(service.isSubmissionLoading(submissionId)).toBeObservable(expected); @@ -844,7 +868,7 @@ describe('SubmissionService test suite', () => { spy.and.returnValue(observableOf({ isLoading: false })); expected = cold('(b|)', { - b: false + b: false, }); expect(service.isSubmissionLoading(submissionId)).toBeObservable(expected); @@ -906,7 +930,7 @@ describe('SubmissionService test suite', () => { selfUrl, submissionDefinition, {}, - new Item() + new Item(), ) ; const expected = new ResetSubmissionFormAction( @@ -915,7 +939,7 @@ describe('SubmissionService test suite', () => { selfUrl, {}, submissionDefinition, - new Item() + new Item(), ); expect((service as any).store.dispatch).toHaveBeenCalledWith(expected); @@ -925,23 +949,24 @@ describe('SubmissionService test suite', () => { describe('retrieveSubmission', () => { it('should retrieve submission from REST endpoint', () => { (service as any).restService.getDataById.and.returnValue(hot('a|', { - a: mockSubmissionRestResponse + a: mockSubmissionRestResponse, })); const result = service.retrieveSubmission('826'); const expected = cold('(b|)', { - b: jasmine.objectContaining({ payload: mockSubmissionRestResponse[0] }) + b: jasmine.objectContaining({ payload: mockSubmissionRestResponse[0] }), }); expect(result).toBeObservable(expected); }); it('should catch error from REST endpoint', () => { + const requestError = new RequestError('Internal Server Error'); + requestError.statusCode = 500; + const errorResponse = new ErrorResponse(requestError); + (service as any).restService.getDataById.and.callFake( - () => observableThrowError({ - statusCode: 500, - errorMessage: 'Internal Server Error', - }) + () => observableThrowError(errorResponse), ); service.retrieveSubmission('826').subscribe((r) => { diff --git a/src/app/submission/submission.service.ts b/src/app/submission/submission.service.ts index 9eb8cf110a5..19c8893a7ee 100644 --- a/src/app/submission/submission.service.ts +++ b/src/app/submission/submission.service.ts @@ -1,14 +1,56 @@ -import { Injectable } from '@angular/core'; import { HttpHeaders } from '@angular/common/http'; +import { Injectable } from '@angular/core'; import { Router } from '@angular/router'; - -import { Observable, of as observableOf, Subscription, timer as observableTimer } from 'rxjs'; -import { catchError, concatMap, distinctUntilChanged, filter, find, map, startWith, take, tap } from 'rxjs/operators'; -import { Store } from '@ngrx/store'; +import { + createSelector, + MemoizedSelector, + select, + Store, +} from '@ngrx/store'; import { TranslateService } from '@ngx-translate/core'; +import { + Observable, + of as observableOf, + Subscription, + timer as observableTimer, +} from 'rxjs'; +import { + catchError, + concatMap, + distinctUntilChanged, + filter, + find, + map, + startWith, + take, + tap, +} from 'rxjs/operators'; -import { submissionSelector, SubmissionState } from './submission.reducers'; -import { hasValue, isEmpty, isNotUndefined } from '../shared/empty.util'; +import { environment } from '../../environments/environment'; +import { ErrorResponse } from '../core/cache/response.models'; +import { SubmissionDefinitionsModel } from '../core/config/models/config-submission-definitions.model'; +import { RemoteData } from '../core/data/remote-data'; +import { RequestService } from '../core/data/request.service'; +import { HttpOptions } from '../core/dspace-rest/dspace-rest.service'; +import { RouteService } from '../core/services/route.service'; +import { Item } from '../core/shared/item.model'; +import { SearchService } from '../core/shared/search/search.service'; +import { SubmissionObject } from '../core/submission/models/submission-object.model'; +import { WorkspaceitemSectionsObject } from '../core/submission/models/workspaceitem-sections.model'; +import { SubmissionJsonPatchOperationsService } from '../core/submission/submission-json-patch-operations.service'; +import { SubmissionRestService } from '../core/submission/submission-rest.service'; +import { SubmissionScopeType } from '../core/submission/submission-scope-type'; +import { + hasValue, + isEmpty, + isNotUndefined, +} from '../shared/empty.util'; +import { NotificationsService } from '../shared/notifications/notifications.service'; +import { + createFailedRemoteDataObject$, + createSuccessfulRemoteDataObject, +} from '../shared/remote-data.utils'; +import { SubmissionError } from './objects/submission-error.model'; import { CancelSubmissionFormAction, ChangeSubmissionCollectionAction, @@ -19,33 +61,34 @@ import { SaveForLaterSubmissionFormAction, SaveSubmissionFormAction, SaveSubmissionSectionFormAction, - SetActiveSectionAction + SetActiveSectionAction, } from './objects/submission-objects.actions'; import { SubmissionObjectEntry, - SubmissionSectionEntry + SubmissionSectionEntry, } from './objects/submission-objects.reducer'; -import { submissionObjectFromIdSelector } from './selectors'; -import { HttpOptions } from '../core/dspace-rest/dspace-rest.service'; -import { SubmissionRestService } from '../core/submission/submission-rest.service'; +import { SubmissionSectionObject } from './objects/submission-section-object.model'; import { SectionDataObject } from './sections/models/section-data.model'; -import { SubmissionScopeType } from '../core/submission/submission-scope-type'; -import { SubmissionObject } from '../core/submission/models/submission-object.model'; -import { RouteService } from '../core/services/route.service'; import { SectionsType } from './sections/sections-type'; -import { NotificationsService } from '../shared/notifications/notifications.service'; -import { SubmissionDefinitionsModel } from '../core/config/models/config-submission-definitions.model'; -import { WorkspaceitemSectionsObject } from '../core/submission/models/workspaceitem-sections.model'; -import { RemoteData } from '../core/data/remote-data'; -import { ErrorResponse } from '../core/cache/response.models'; -import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject } from '../shared/remote-data.utils'; -import { RequestService } from '../core/data/request.service'; -import { SearchService } from '../core/shared/search/search.service'; -import { Item } from '../core/shared/item.model'; -import { environment } from '../../environments/environment'; -import { SubmissionJsonPatchOperationsService } from '../core/submission/submission-json-patch-operations.service'; -import { SubmissionSectionObject } from './objects/submission-section-object.model'; -import { SubmissionError } from './objects/submission-error.model'; +import { submissionObjectFromIdSelector } from './selectors'; +import { + submissionSelector, + 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. @@ -96,10 +139,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 * @@ -217,7 +269,7 @@ export class SubmissionService { */ dispatchSave(submissionId, manual?: boolean) { this.getSubmissionSaveProcessingStatus(submissionId).pipe( - find((isPending: boolean) => !isPending) + find((isPending: boolean) => !isPending), ).subscribe(() => { this.store.dispatch(new SaveSubmissionFormAction(submissionId, manual)); }); @@ -505,7 +557,7 @@ export class SubmissionService { } else { this.router.navigateByUrl(previousUrl); } - }))) + }))), ).subscribe(); } @@ -536,7 +588,7 @@ export class SubmissionService { selfUrl: string, submissionDefinition: SubmissionDefinitionsModel, sections: WorkspaceitemSectionsObject, - item: Item + item: Item, ) { this.store.dispatch(new ResetSubmissionFormAction(collectionId, submissionId, selfUrl, sections, submissionDefinition, item)); } @@ -552,9 +604,11 @@ export class SubmissionService { find((submissionObjects: SubmissionObject[]) => isNotUndefined(submissionObjects)), map((submissionObjects: SubmissionObject[]) => createSuccessfulRemoteDataObject( submissionObjects[0])), - catchError((errorResponse: ErrorResponse) => { - return createFailedRemoteDataObject$(errorResponse.errorMessage, errorResponse.statusCode); - }) + catchError((errorResponse: unknown) => { + if (errorResponse instanceof ErrorResponse) { + return createFailedRemoteDataObject$(errorResponse.errorMessage, errorResponse.statusCode); + } + }), ); } diff --git a/src/app/submission/submit/submission-submit.component.spec.ts b/src/app/submission/submit/submission-submit.component.spec.ts index da569e4e5d7..d54eaaeccb7 100644 --- a/src/app/submission/submit/submission-submit.component.spec.ts +++ b/src/app/submission/submit/submission-submit.component.spec.ts @@ -1,22 +1,34 @@ -import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing'; +import { + NO_ERRORS_SCHEMA, + ViewContainerRef, +} from '@angular/core'; +import { + ComponentFixture, + TestBed, + waitForAsync, +} from '@angular/core/testing'; +import { + ActivatedRoute, + Router, +} from '@angular/router'; import { RouterTestingModule } from '@angular/router/testing'; -import { ActivatedRoute, Router } from '@angular/router'; -import { NO_ERRORS_SCHEMA, ViewContainerRef } from '@angular/core'; - +import { + TranslateModule, + TranslateService, +} from '@ngx-translate/core'; import { of as observableOf } from 'rxjs'; -import { TranslateModule, TranslateService } from '@ngx-translate/core'; +import { ItemDataService } from '../../core/data/item-data.service'; +import { mockSubmissionObject } from '../../shared/mocks/submission.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 { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub'; -import { SubmissionService } from '../submission.service'; -import { SubmissionServiceStub } from '../../shared/testing/submission-service.stub'; -import { getMockTranslateService } from '../../shared/mocks/translate.service.mock'; import { RouterStub } from '../../shared/testing/router.stub'; -import { mockSubmissionObject } from '../../shared/mocks/submission.mock'; +import { SubmissionServiceStub } from '../../shared/testing/submission-service.stub'; +import { SubmissionService } from '../submission.service'; import { SubmissionSubmitComponent } from './submission-submit.component'; -import { ActivatedRouteStub } from '../../shared/testing/active-router.stub'; -import { ItemDataService } from '../../core/data/item-data.service'; -import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; describe('SubmissionSubmitComponent Component', () => { @@ -37,9 +49,9 @@ describe('SubmissionSubmitComponent Component', () => { TranslateModule.forRoot(), RouterTestingModule.withRoutes([ { path: '', component: SubmissionSubmitComponent, pathMatch: 'full' }, - ]) + ]), + SubmissionSubmitComponent, ], - declarations: [SubmissionSubmitComponent], providers: [ { provide: NotificationsService, useClass: NotificationsServiceStub }, { provide: SubmissionService, useClass: SubmissionServiceStub }, @@ -47,9 +59,9 @@ describe('SubmissionSubmitComponent Component', () => { { provide: TranslateService, useValue: getMockTranslateService() }, { provide: Router, useValue: new RouterStub() }, { provide: ActivatedRoute, useValue: new ActivatedRouteStub() }, - ViewContainerRef + ViewContainerRef, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); })); @@ -78,11 +90,11 @@ describe('SubmissionSubmitComponent Component', () => { it('should redirect to workspaceitem edit when a not empty SubmissionObject has been retrieved',() => { - submissionServiceStub.createSubmission.and.returnValue(observableOf({ id: '1234'})); + submissionServiceStub.createSubmission.and.returnValue(observableOf({ id: '1234' })); fixture.detectChanges(); - expect(router.navigate).toHaveBeenCalledWith(['/workspaceitems', '1234', 'edit'], { replaceUrl: true}); + expect(router.navigate).toHaveBeenCalledWith(['/workspaceitems', '1234', 'edit'], { replaceUrl: true }); }); diff --git a/src/app/submission/submit/submission-submit.component.ts b/src/app/submission/submit/submission-submit.component.ts index a66d1e46566..c9e087f7d81 100644 --- a/src/app/submission/submit/submission-submit.component.ts +++ b/src/app/submission/submit/submission-submit.component.ts @@ -1,28 +1,48 @@ -import { ChangeDetectorRef, Component, OnDestroy, OnInit, ViewContainerRef } from '@angular/core'; -import { ActivatedRoute, Router } from '@angular/router'; - -import { BehaviorSubject, Subscription } from 'rxjs'; -import { debounceTime, switchMap } from 'rxjs/operators'; +import { + ChangeDetectorRef, + Component, + OnDestroy, + OnInit, + ViewContainerRef, +} from '@angular/core'; +import { + ActivatedRoute, + Router, +} from '@angular/router'; import { TranslateService } from '@ngx-translate/core'; +import { + BehaviorSubject, + Subscription, +} from 'rxjs'; +import { + debounceTime, + switchMap, +} from 'rxjs/operators'; -import { hasValue, isEmpty, isNotEmptyOperator, isNotNull } from '../../shared/empty.util'; import { SubmissionDefinitionsModel } from '../../core/config/models/config-submission-definitions.model'; -import { NotificationsService } from '../../shared/notifications/notifications.service'; -import { SubmissionService } from '../submission.service'; -import { SubmissionObject } from '../../core/submission/models/submission-object.model'; +import { ItemDataService } from '../../core/data/item-data.service'; +import { RemoteData } from '../../core/data/remote-data'; import { Item } from '../../core/shared/item.model'; -import { WorkspaceitemSectionsObject } from '../../core/submission/models/workspaceitem-sections.model'; import { getAllSucceededRemoteData } from '../../core/shared/operators'; -import { RemoteData } from '../../core/data/remote-data'; -import { ItemDataService } from '../../core/data/item-data.service'; +import { SubmissionObject } from '../../core/submission/models/submission-object.model'; +import { WorkspaceitemSectionsObject } from '../../core/submission/models/workspaceitem-sections.model'; +import { + hasValue, + isEmpty, + isNotEmptyOperator, + isNotNull, +} from '../../shared/empty.util'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +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' + templateUrl: './submission-submit.component.html', + standalone: true, }) export class SubmissionSubmitComponent implements OnDestroy, OnInit { @@ -120,14 +140,14 @@ export class SubmissionSubmitComponent implements OnDestroy, OnInit { this.notificationsService.info(null, this.translate.get('submission.general.cannot_submit')); this.router.navigate(['/mydspace']); } else { - this.router.navigate(['/workspaceitems', submissionObject.id, 'edit'], { replaceUrl: true}); + this.router.navigate(['/workspaceitems', submissionObject.id, 'edit'], { replaceUrl: true }); } } }), this.itemLink$.pipe( isNotEmptyOperator(), switchMap((itemLink: string) => - this.itemDataService.findByHref(itemLink) + this.itemDataService.findByHref(itemLink), ), getAllSucceededRemoteData(), // Multiple sources can update the item in quick succession. @@ -136,7 +156,7 @@ export class SubmissionSubmitComponent implements OnDestroy, OnInit { ).subscribe((itemRd: RemoteData) => { this.item = itemRd.payload; this.changeDetectorRef.detectChanges(); - }) + }), ); } diff --git a/src/app/submission/submit/themed-submission-submit.component.ts b/src/app/submission/submit/themed-submission-submit.component.ts index 328113252f9..ee5ee74746e 100644 --- a/src/app/submission/submit/themed-submission-submit.component.ts +++ b/src/app/submission/submit/themed-submission-submit.component.ts @@ -1,4 +1,5 @@ import { Component } from '@angular/core'; + import { ThemedComponent } from '../../shared/theme-support/themed.component'; import { SubmissionSubmitComponent } from './submission-submit.component'; @@ -6,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' + templateUrl: './../../shared/theme-support/themed.component.html', + standalone: true, + imports: [SubmissionSubmitComponent], }) export class ThemedSubmissionSubmitComponent extends ThemedComponent { protected getComponentName(): string { diff --git a/src/app/submission/utils/parseSectionErrorPaths.ts b/src/app/submission/utils/parseSectionErrorPaths.ts index 4c973dedcff..b6c818f919f 100644 --- a/src/app/submission/utils/parseSectionErrorPaths.ts +++ b/src/app/submission/utils/parseSectionErrorPaths.ts @@ -39,21 +39,21 @@ const parseSectionErrorPaths = (path: string | string[]): SectionErrorPath[] => const paths = typeof path === 'string' ? [path] : path; return paths.map((item) => { - if (item.match(regex) && item.match(regex).length > 2) { - return { - sectionId: item.match(regex)[1], - fieldId: item.match(regex)[2], - fieldIndex: hasValue(item.match(regex)[3]) ? +item.match(regex)[3] : 0, - originalPath: item, - }; - } else { - return { - sectionId: item.match(regexShort)[1], - originalPath: item, - }; - } - + if (item.match(regex) && item.match(regex).length > 2) { + return { + sectionId: item.match(regex)[1], + fieldId: item.match(regex)[2], + fieldIndex: hasValue(item.match(regex)[3]) ? +item.match(regex)[3] : 0, + originalPath: item, + }; + } else { + return { + sectionId: item.match(regexShort)[1], + originalPath: item, + }; } + + }, ); }; diff --git a/src/app/submission/utils/parseSectionErrors.ts b/src/app/submission/utils/parseSectionErrors.ts index 5f2867c8b85..e2e405915d9 100644 --- a/src/app/submission/utils/parseSectionErrors.ts +++ b/src/app/submission/utils/parseSectionErrors.ts @@ -1,5 +1,8 @@ import { SubmissionObjectError } from '../../core/submission/models/submission-object.model'; -import { default as parseSectionErrorPaths, SectionErrorPath } from './parseSectionErrorPaths'; +import { + default as parseSectionErrorPaths, + SectionErrorPath, +} from './parseSectionErrorPaths'; /** * the following method accept an array of SubmissionObjectError and return a section errors object @@ -13,7 +16,7 @@ const parseSectionErrors = (errors: SubmissionObjectError[] = []): any => { const paths: SectionErrorPath[] = parseSectionErrorPaths(error.paths); paths.forEach((path: SectionErrorPath) => { - const sectionError = {path: path.originalPath, message: error.message}; + const sectionError = { path: path.originalPath, message: error.message }; if (!errorsList[path.sectionId]) { errorsList[path.sectionId] = []; } 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 00000000000..338a81af3ed --- /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 1572580b6a4..00000000000 --- 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 { ThemedSubmissionSubmitComponent } from '../submission/submit/themed-submission-submit.component'; -import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver'; - -@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 6942628e9ed..00000000000 --- a/src/app/submit-page/submit-page.module.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { CommonModule } from '@angular/common'; -import { NgModule } from '@angular/core'; -import { SharedModule } from '../shared/shared.module'; -import { SubmitPageRoutingModule } from './submit-page-routing.module'; -import { SubmissionModule } from '../submission/submission.module'; -import { FormModule } from '../shared/form/form.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 00000000000..0dbdad2f020 --- /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 149c9a415fc..00000000000 --- a/src/app/subscriptions-page/subscriptions-page-routing.module.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { NgModule } from '@angular/core'; -import { RouterModule } from '@angular/router'; -import { SubscriptionsPageModule } from './subscriptions-page.module'; -import { SubscriptionsPageComponent } from './subscriptions-page.component'; - - -@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 ed31e16759b..e8dfc8fc6ac 100644 --- a/src/app/subscriptions-page/subscriptions-page.component.html +++ b/src/app/subscriptions-page/subscriptions-page.component.html @@ -1,13 +1,13 @@
-

{{'subscriptions.title' | translate}}

+

{{'subscriptions.title' | translate}}

- + - {{'subscriptions.title' | translate}}
- + {{ 'subscriptions.table.empty.message' | translate }} diff --git a/src/app/subscriptions-page/subscriptions-page.component.spec.ts b/src/app/subscriptions-page/subscriptions-page.component.spec.ts index 4f443924281..cd582ddf414 100644 --- a/src/app/subscriptions-page/subscriptions-page.component.spec.ts +++ b/src/app/subscriptions-page/subscriptions-page.component.spec.ts @@ -1,31 +1,46 @@ -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { DebugElement, NO_ERRORS_SCHEMA } from '@angular/core'; import { CommonModule } from '@angular/common'; -import { BrowserModule, By } from '@angular/platform-browser'; +import { + DebugElement, + NO_ERRORS_SCHEMA, +} from '@angular/core'; +import { + ComponentFixture, + TestBed, + waitForAsync, +} from '@angular/core/testing'; +import { + BrowserModule, + By, +} from '@angular/platform-browser'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { ActivatedRoute } from '@angular/router'; import { RouterTestingModule } from '@angular/router/testing'; -import { NoopAnimationsModule } from '@angular/platform-browser/animations'; - +import { + TranslateLoader, + TranslateModule, +} from '@ngx-translate/core'; import { of as observableOf } from 'rxjs'; -import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; -import { SubscriptionsPageComponent } from './subscriptions-page.component'; +import { AuthService } from '../core/auth/auth.service'; +import { buildPaginatedList } from '../core/data/paginated-list.model'; import { PaginationService } from '../core/pagination/pagination.service'; +import { PageInfo } from '../core/shared/page-info.model'; +import { AlertComponent } from '../shared/alert/alert.component'; +import { ThemedLoadingComponent } from '../shared/loading/themed-loading.component'; +import { MockActivatedRoute } from '../shared/mocks/active-router.mock'; +import { TranslateLoaderMock } from '../shared/mocks/translate-loader.mock'; +import { PaginationComponent } from '../shared/pagination/pagination.component'; +import { createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils'; +import { SubscriptionViewComponent } from '../shared/subscriptions/subscription-view/subscription-view.component'; import { SubscriptionsDataService } from '../shared/subscriptions/subscriptions-data.service'; import { PaginationServiceStub } from '../shared/testing/pagination-service.stub'; -import { AuthService } from '../core/auth/auth.service'; -import { TranslateLoaderMock } from '../shared/mocks/translate-loader.mock'; import { mockSubscriptionEperson, subscriptionMock, - subscriptionMock2 + subscriptionMock2, } from '../shared/testing/subscriptions-data.mock'; -import { MockActivatedRoute } from '../shared/mocks/active-router.mock'; import { VarDirective } from '../shared/utils/var.directive'; -import { SubscriptionViewComponent } from '../shared/subscriptions/subscription-view/subscription-view.component'; -import { PageInfo } from '../core/shared/page-info.model'; -import { createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils'; -import { buildPaginatedList } from '../core/data/paginated-list.model'; +import { SubscriptionsPageComponent } from './subscriptions-page.component'; describe('SubscriptionsPageComponent', () => { let component: SubscriptionsPageComponent; @@ -33,11 +48,11 @@ describe('SubscriptionsPageComponent', () => { let de: DebugElement; const authServiceStub = jasmine.createSpyObj('authorizationService', { - getAuthenticatedUserFromStore: observableOf(mockSubscriptionEperson) + getAuthenticatedUserFromStore: observableOf(mockSubscriptionEperson), }); const subscriptionServiceStub = jasmine.createSpyObj('SubscriptionsDataService', { - findByEPerson: jasmine.createSpy('findByEPerson') + findByEPerson: jasmine.createSpy('findByEPerson'), }); const paginationService = new PaginationServiceStub(); @@ -45,11 +60,11 @@ describe('SubscriptionsPageComponent', () => { const mockSubscriptionList = [subscriptionMock, subscriptionMock2]; const emptyPageInfo = Object.assign(new PageInfo(), { - totalElements: 0 + totalElements: 0, }); const pageInfo = Object.assign(new PageInfo(), { - totalElements: 2 + totalElements: 2, }); beforeEach(waitForAsync(() => { @@ -61,20 +76,25 @@ describe('SubscriptionsPageComponent', () => { TranslateModule.forRoot({ loader: { provide: TranslateLoader, - useClass: TranslateLoaderMock - } + useClass: TranslateLoaderMock, + }, }), - NoopAnimationsModule + NoopAnimationsModule, + SubscriptionsPageComponent, SubscriptionViewComponent, VarDirective, ], - declarations: [SubscriptionsPageComponent, SubscriptionViewComponent, VarDirective], providers: [ { provide: SubscriptionsDataService, useValue: subscriptionServiceStub }, { provide: ActivatedRoute, useValue: new MockActivatedRoute() }, { provide: AuthService, useValue: authServiceStub }, - { provide: PaginationService, useValue: paginationService } + { provide: PaginationService, useValue: paginationService }, ], - schemas: [NO_ERRORS_SCHEMA] + 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 05c587ba12c..5d0b4b258d8 100644 --- a/src/app/subscriptions-page/subscriptions-page.component.ts +++ b/src/app/subscriptions-page/subscriptions-page.component.ts @@ -1,24 +1,55 @@ -import { Component, OnDestroy, OnInit } from '@angular/core'; +import { + AsyncPipe, + NgFor, + NgIf, +} from '@angular/common'; +import { + Component, + OnDestroy, + OnInit, +} from '@angular/core'; +import { TranslateModule } from '@ngx-translate/core'; +import { + BehaviorSubject, + combineLatestWith, + Observable, + shareReplay, + Subscription as rxjsSubscription, +} from 'rxjs'; +import { + map, + switchMap, + take, + tap, +} from 'rxjs/operators'; -import { BehaviorSubject, combineLatestWith, Observable, shareReplay, Subscription as rxjsSubscription } from 'rxjs'; -import { map, switchMap, take, tap } from 'rxjs/operators'; - -import { Subscription } from '../shared/subscriptions/models/subscription.model'; -import { buildPaginatedList, PaginatedList } from '../core/data/paginated-list.model'; -import { SubscriptionsDataService } from '../shared/subscriptions/subscriptions-data.service'; -import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model'; -import { PaginationService } from '../core/pagination/pagination.service'; -import { PageInfo } from '../core/shared/page-info.model'; import { AuthService } from '../core/auth/auth.service'; +import { + buildPaginatedList, + PaginatedList, +} from '../core/data/paginated-list.model'; +import { RemoteData } from '../core/data/remote-data'; import { EPerson } from '../core/eperson/models/eperson.model'; +import { PaginationService } from '../core/pagination/pagination.service'; import { getAllCompletedRemoteData } from '../core/shared/operators'; -import { RemoteData } from '../core/data/remote-data'; +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'] + 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 @@ -36,7 +67,7 @@ export class SubscriptionsPageComponent implements OnInit, OnDestroy { config: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), { id: 'elp', pageSize: 10, - currentPage: 1 + currentPage: 1, }); /** @@ -54,10 +85,12 @@ export class SubscriptionsPageComponent implements OnInit, OnDestroy { */ sub: rxjsSubscription = null; + readonly AlertType = AlertType; + constructor( private paginationService: PaginationService, private authService: AuthService, - private subscriptionService: SubscriptionsDataService + private subscriptionService: SubscriptionsDataService, ) { } @@ -69,7 +102,7 @@ export class SubscriptionsPageComponent implements OnInit, OnDestroy { this.ePersonId$ = this.authService.getAuthenticatedUserFromStore().pipe( take(1), map((ePerson: EPerson) => ePerson.id), - shareReplay() + shareReplay({ refCount: false }), ); this.retrieveSubscriptions(); } @@ -85,9 +118,9 @@ export class SubscriptionsPageComponent implements OnInit, OnDestroy { tap(() => this.loading$.next(true)), switchMap(([currentPagination, ePersonId]) => this.subscriptionService.findByEPerson(ePersonId,{ currentPage: currentPagination.currentPage, - elementsPerPage: currentPagination.pageSize + elementsPerPage: currentPagination.pageSize, })), - getAllCompletedRemoteData() + getAllCompletedRemoteData(), ).subscribe((res: RemoteData>) => { if (res.hasSucceeded) { this.subscriptions$.next(res.payload); 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 f7a4dc33446..00000000000 --- a/src/app/subscriptions-page/subscriptions-page.module.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { NgModule } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { SubscriptionsPageComponent } from './subscriptions-page.component'; -import { SharedModule } from '../shared/shared.module'; -import { SubscriptionsModule } from '../shared/subscriptions/subscriptions.module'; - -@NgModule({ - declarations: [SubscriptionsPageComponent], - imports: [ - CommonModule, - SharedModule, - SubscriptionsModule, - ] -}) -export class SubscriptionsPageModule { } 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 00000000000..f270a1ef663 --- /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-paths.ts b/src/app/suggestions-page/suggestions-page-routing-paths.ts new file mode 100644 index 00000000000..0f3aa782d21 --- /dev/null +++ b/src/app/suggestions-page/suggestions-page-routing-paths.ts @@ -0,0 +1,11 @@ +import { URLCombiner } from '../core/url-combiner/url-combiner'; + +export const SUGGESTION_MODULE_PATH = 'suggestions'; + +export function getSuggestionModuleRoute() { + return `/${SUGGESTION_MODULE_PATH}`; +} + +export function getSuggestionPageRoute(SuggestionId: string) { + return new URLCombiner(getSuggestionModuleRoute(), SuggestionId).toString(); +} diff --git a/src/app/suggestions-page/suggestions-page.component.html b/src/app/suggestions-page/suggestions-page.component.html new file mode 100644 index 00000000000..45bdff32bf5 --- /dev/null +++ b/src/app/suggestions-page/suggestions-page.component.html @@ -0,0 +1,50 @@ +
+
+
+ + +
+ +

+ {{'suggestion.suggestionFor' | translate}} + {{researcherName}} + {{'suggestion.from.source' | translate}} {{ translateSuggestionSource() | translate }} +

+ +
+ + ({{ getSelectedSuggestionsCount() }}) + + +
+ +
    +
  • + +
  • +
+
+
+ + {{'suggestion.count.missing' | translate}} + +
+
+
+
diff --git a/src/themes/custom/app/shared/object-list/sidebar-search-list-element/item-types/publication-sidebar-search-list-element.component.html b/src/app/suggestions-page/suggestions-page.component.scss similarity index 100% rename from src/themes/custom/app/shared/object-list/sidebar-search-list-element/item-types/publication-sidebar-search-list-element.component.html rename to src/app/suggestions-page/suggestions-page.component.scss diff --git a/src/app/suggestions-page/suggestions-page.component.spec.ts b/src/app/suggestions-page/suggestions-page.component.spec.ts new file mode 100644 index 00000000000..6c19405e943 --- /dev/null +++ b/src/app/suggestions-page/suggestions-page.component.spec.ts @@ -0,0 +1,228 @@ +import { CommonModule } from '@angular/common'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { + ComponentFixture, + fakeAsync, + TestBed, + waitForAsync, +} from '@angular/core/testing'; +import { BrowserModule } from '@angular/platform-browser'; +import { + ActivatedRoute, + Router, +} from '@angular/router'; +import { + TranslateModule, + TranslateService, +} from '@ngx-translate/core'; +import { getTestScheduler } from 'jasmine-marbles'; +import { of as observableOf } from 'rxjs'; +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 { 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 { + mockSuggestionPublicationOne, + mockSuggestionPublicationTwo, +} from '../shared/mocks/publication-claim.mock'; +import { mockSuggestionTargetsObjectOne } from '../shared/mocks/publication-claim-targets.mock'; +import { + getMockSuggestionNotificationsStateService, + getMockSuggestionsService, +} from '../shared/mocks/suggestion.mock'; +import { getMockTranslateService } from '../shared/mocks/translate.service.mock'; +import { NotificationsService } from '../shared/notifications/notifications.service'; +import { createSuccessfulRemoteDataObject } from '../shared/remote-data.utils'; +import { NotificationsServiceStub } from '../shared/testing/notifications-service.stub'; +import { PaginationServiceStub } from '../shared/testing/pagination-service.stub'; +import { RouterStub } from '../shared/testing/router.stub'; +import { ObjectKeysPipe } from '../shared/utils/object-keys-pipe'; +import { VarDirective } from '../shared/utils/var.directive'; +import { SuggestionsPageComponent } from './suggestions-page.component'; + +describe('SuggestionPageComponent', () => { + let component: SuggestionsPageComponent; + let fixture: ComponentFixture; + let scheduler: TestScheduler; + const mockSuggestionsService = getMockSuggestionsService(); + const mockSuggestionsTargetStateService = getMockSuggestionNotificationsStateService(); + const router = new RouterStub(); + const routeStub = { + data: observableOf({ + suggestionTargets: createSuccessfulRemoteDataObject(mockSuggestionTargetsObjectOne), + }), + queryParams: observableOf({}), + }; + const workspaceitemServiceMock = jasmine.createSpyObj('WorkspaceitemDataService', { + importExternalSourceEntry: jasmine.createSpy('importExternalSourceEntry'), + }); + + const authService = jasmine.createSpyObj('authService', { + isAuthenticated: observableOf(true), + setRedirectUrl: {}, + }); + const paginationService = new PaginationServiceStub(); + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [ + BrowserModule, + CommonModule, + TranslateModule.forRoot(), + SuggestionEvidencesComponent, + SuggestionListElementComponent, + SuggestionsPageComponent, + ObjectKeysPipe, + VarDirective, + ], + providers: [ + { provide: AuthService, useValue: authService }, + { provide: ActivatedRoute, useValue: routeStub }, + { provide: WorkspaceitemDataService, useValue: workspaceitemServiceMock }, + { provide: Router, useValue: router }, + { provide: SuggestionsService, useValue: mockSuggestionsService }, + { provide: SuggestionTargetsStateService, useValue: mockSuggestionsTargetStateService }, + { provide: NotificationsService, useValue: new NotificationsServiceStub() }, + { provide: TranslateService, useValue: getMockTranslateService() }, + { provide: PaginationService, useValue: paginationService }, + SuggestionsPageComponent, + ], + schemas: [NO_ERRORS_SCHEMA], + }) + .compileComponents().then(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(SuggestionsPageComponent); + component = fixture.componentInstance; + scheduler = getTestScheduler(); + }); + + it('should create', () => { + spyOn(component, 'updatePage').and.callThrough(); + + scheduler.schedule(() => fixture.detectChanges()); + scheduler.flush(); + + expect(component).toBeTruthy(); + expect(component.suggestionId).toBe(mockSuggestionTargetsObjectOne.id); + expect(component.researcherName).toBe(mockSuggestionTargetsObjectOne.display); + expect(component.updatePage).toHaveBeenCalled(); + }); + + it('should update page on pagination change', () => { + spyOn(component, 'updatePage').and.callThrough(); + component.targetId$ = observableOf('testid'); + + scheduler.schedule(() => component.onPaginationChange()); + scheduler.flush(); + + expect(component.updatePage).toHaveBeenCalled(); + }); + + it('should update suggestion on page update', () => { + spyOn(component.processing$, 'next'); + spyOn(component.suggestionsRD$, 'next'); + + component.targetId$ = observableOf('testid'); + scheduler.schedule(() => component.updatePage().subscribe()); + scheduler.flush(); + + 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.callThrough(); + component.targetId$ = observableOf('testid'); + + scheduler.schedule(() => component.ignoreSuggestion('1')); + scheduler.flush(); + + expect(mockSuggestionsService.ignoreSuggestion).toHaveBeenCalledWith('1'); + expect(mockSuggestionsTargetStateService.dispatchRefreshUserSuggestionsAction).toHaveBeenCalled(); + expect(component.updatePage).toHaveBeenCalled(); + })); + + it('should flag all suggestion for deletion', () => { + spyOn(component, 'updatePage').and.callThrough(); + component.targetId$ = observableOf('testid'); + + scheduler.schedule(() => component.ignoreSuggestionAllSelected()); + scheduler.flush(); + + expect(mockSuggestionsService.ignoreSuggestionMultiple).toHaveBeenCalled(); + expect(mockSuggestionsTargetStateService.dispatchRefreshUserSuggestionsAction).toHaveBeenCalled(); + expect(component.updatePage).toHaveBeenCalled(); + }); + + it('should approve and import', () => { + spyOn(component, 'updatePage').and.callThrough(); + component.targetId$ = observableOf('testid'); + + scheduler.schedule(() => component.approveAndImport({ collectionId: '1234' } as unknown as SuggestionApproveAndImport)); + scheduler.flush(); + + expect(mockSuggestionsService.approveAndImport).toHaveBeenCalled(); + expect(mockSuggestionsTargetStateService.dispatchRefreshUserSuggestionsAction).toHaveBeenCalled(); + expect(component.updatePage).toHaveBeenCalled(); + }); + + it('should approve and import multiple suggestions', () => { + spyOn(component, 'updatePage').and.callThrough(); + component.targetId$ = observableOf('testid'); + + scheduler.schedule(() => component.approveAndImportAllSelected({ collectionId: '1234' } as unknown as SuggestionApproveAndImport)); + scheduler.flush(); + + expect(mockSuggestionsService.approveAndImportMultiple).toHaveBeenCalled(); + expect(mockSuggestionsTargetStateService.dispatchRefreshUserSuggestionsAction).toHaveBeenCalled(); + expect(component.updatePage).toHaveBeenCalled(); + }); + + it('should select and deselect suggestion', () => { + component.selectedSuggestions = {}; + component.onSelected(mockSuggestionPublicationOne, true); + expect(component.selectedSuggestions[mockSuggestionPublicationOne.id]).toBe(mockSuggestionPublicationOne); + component.onSelected(mockSuggestionPublicationOne, false); + expect(component.selectedSuggestions[mockSuggestionPublicationOne.id]).toBeUndefined(); + }); + + it('should toggle all suggestions', () => { + component.selectedSuggestions = {}; + component.onToggleSelectAll([mockSuggestionPublicationOne, mockSuggestionPublicationTwo]); + expect(component.selectedSuggestions[mockSuggestionPublicationOne.id]).toEqual(mockSuggestionPublicationOne); + expect(component.selectedSuggestions[mockSuggestionPublicationTwo.id]).toEqual(mockSuggestionPublicationTwo); + component.onToggleSelectAll([mockSuggestionPublicationOne, mockSuggestionPublicationTwo]); + expect(component.selectedSuggestions).toEqual({}); + }); + + it('should return all selected suggestions count', () => { + component.selectedSuggestions = {}; + component.onToggleSelectAll([mockSuggestionPublicationOne, mockSuggestionPublicationTwo]); + expect(component.getSelectedSuggestionsCount()).toEqual(2); + }); + + it('should check if all collection is fixed', () => { + component.isCollectionFixed([mockSuggestionPublicationOne, mockSuggestionPublicationTwo]); + expect(mockSuggestionsService.isCollectionFixed).toHaveBeenCalled(); + }); + + it('should translate suggestion source', () => { + component.translateSuggestionSource(); + expect(mockSuggestionsService.translateSuggestionSource).toHaveBeenCalled(); + }); + + it('should translate suggestion type', () => { + component.translateSuggestionType(); + expect(mockSuggestionsService.translateSuggestionType).toHaveBeenCalled(); + }); +}); diff --git a/src/app/suggestions-page/suggestions-page.component.ts b/src/app/suggestions-page/suggestions-page.component.ts new file mode 100644 index 00000000000..0fc790a125d --- /dev/null +++ b/src/app/suggestions-page/suggestions-page.component.ts @@ -0,0 +1,345 @@ +import { + AsyncPipe, + NgForOf, + NgIf, +} from '@angular/common'; +import { + Component, + OnInit, +} from '@angular/core'; +import { + ActivatedRoute, + Data, + Router, + RouterLink, +} from '@angular/router'; +import { + TranslateModule, + TranslateService, +} from '@ngx-translate/core'; +import { + BehaviorSubject, + combineLatest, + Observable, +} from 'rxjs'; +import { + distinctUntilChanged, + map, + switchMap, + tap, +} from 'rxjs/operators'; + +import { AuthService } from '../core/auth/auth.service'; +import { + SortDirection, + SortOptions, +} from '../core/cache/models/sort-options.model'; +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/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 { + getFirstCompletedRemoteData, + getFirstSucceededRemoteDataPayload, +} from '../core/shared/operators'; +import { WorkspaceItem } from '../core/submission/models/workspaceitem.model'; +import { WorkspaceitemDataService } from '../core/submission/workspaceitem-data.service'; +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, +}) + +/** + * Component used to visualize one of the suggestions from the publication claim page or from the notification pop up + */ + +export class SuggestionsPageComponent implements OnInit { + + /** + * The pagination configuration + */ + paginationOptions: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), { + id: 'sp', + pageSizeOptions: [5, 10, 20, 40, 60], + }); + + /** + * The sorting configuration + */ + paginationSortConfig: SortOptions = new SortOptions('trust', SortDirection.DESC); + + /** + * The FindListOptions object + */ + defaultConfig: FindListOptions = Object.assign(new FindListOptions(), { sort: this.paginationSortConfig }); + + /** + * A boolean representing if results are loading + */ + public processing$ = new BehaviorSubject(false); + + /** + * A list of remote data objects of suggestions + */ + suggestionsRD$: BehaviorSubject> = new BehaviorSubject>({} as any); + + targetRD$: Observable>; + targetId$: Observable; + + suggestionTarget: SuggestionTarget; + suggestionId: any; + suggestionSource: any; + researcherName: any; + researcherUuid: any; + + selectedSuggestions: { [id: string]: Suggestion } = {}; + isBulkOperationPending = false; + + constructor( + private authService: AuthService, + private notificationService: NotificationsService, + private paginationService: PaginationService, + private route: ActivatedRoute, + private router: Router, + private suggestionService: SuggestionsService, + private suggestionTargetsStateService: SuggestionTargetsStateService, + private translateService: TranslateService, + private workspaceItemService: WorkspaceitemDataService, + ) { + } + + ngOnInit(): void { + this.targetRD$ = this.route.data.pipe( + map((data: Data) => data.suggestionTargets as RemoteData), + redirectOn4xx(this.router, this.authService), + ); + + this.targetId$ = this.targetRD$.pipe( + getFirstSucceededRemoteDataPayload(), + map((target: SuggestionTarget) => target.id), + ); + this.targetRD$.pipe( + getFirstSucceededRemoteDataPayload(), + 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(); + } + + /** + * Called when one of the pagination settings is changed + */ + onPaginationChange() { + this.updatePage().subscribe(); + } + + /** + * Update the list of suggestions + */ + updatePage(): Observable>> { + this.processing$.next(true); + const pageConfig$: Observable = this.paginationService.getFindListOptions( + this.paginationOptions.id, + this.defaultConfig, + ).pipe( + distinctUntilChanged(), + ); + + return combineLatest([this.targetId$, pageConfig$]).pipe( + switchMap(([targetId, config]: [string, FindListOptions]) => { + return this.suggestionService.getSuggestions( + targetId, + config.elementsPerPage, + config.currentPage, + config.sort, + ); + }), + getFirstCompletedRemoteData(), + tap((resultsRD: RemoteData>) => { + this.processing$.next(false); + if (resultsRD.hasSucceeded) { + this.suggestionsRD$.next(resultsRD.payload); + } else { + this.suggestionsRD$.next(null); + } + + this.suggestionService.clearSuggestionRequests(); + }), + ); + } + + /** + * Used to delete a suggestion. + * @suggestionId + */ + ignoreSuggestion(suggestionId) { + this.suggestionService.ignoreSuggestion(suggestionId).pipe( + tap(() => this.suggestionTargetsStateService.dispatchRefreshUserSuggestionsAction()), + switchMap(() => this.updatePage()), + ).subscribe(); + } + + /** + * Used to delete all selected suggestions. + */ + ignoreSuggestionAllSelected() { + this.isBulkOperationPending = true; + this.suggestionService.ignoreSuggestionMultiple(Object.values(this.selectedSuggestions)).pipe( + tap((results: SuggestionBulkResult) => { + this.suggestionTargetsStateService.dispatchRefreshUserSuggestionsAction(); + this.isBulkOperationPending = false; + this.selectedSuggestions = {}; + if (results.success > 0) { + this.notificationService.success( + this.translateService.get('suggestion.ignoreSuggestion.bulk.success', + { count: results.success })); + } + if (results.fails > 0) { + this.notificationService.error( + this.translateService.get('suggestion.ignoreSuggestion.bulk.error', + { count: results.fails })); + } + }), + switchMap(() => this.updatePage()), + ).subscribe(); + } + + /** + * Used to approve & import. + * @param event contains the suggestion and the target collection + */ + approveAndImport(event: SuggestionApproveAndImport) { + 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(); + }), + switchMap(() => this.updatePage()), + ).subscribe(); + } + + /** + * Used to approve & import all selected suggestions. + * @param event contains the target collection + */ + approveAndImportAllSelected(event: SuggestionApproveAndImport) { + this.isBulkOperationPending = true; + this.suggestionService.approveAndImportMultiple(this.workspaceItemService, Object.values(this.selectedSuggestions), event.collectionId).pipe( + tap((results: SuggestionBulkResult) => { + this.suggestionTargetsStateService.dispatchRefreshUserSuggestionsAction(); + this.isBulkOperationPending = false; + this.selectedSuggestions = {}; + if (results.success > 0) { + this.notificationService.success( + this.translateService.get('suggestion.approveAndImport.bulk.success', + { count: results.success })); + } + if (results.fails > 0) { + this.notificationService.error( + this.translateService.get('suggestion.approveAndImport.bulk.error', + { count: results.fails })); + } + }), + switchMap(() => this.updatePage()), + ).subscribe(); + } + + /** + * When a specific suggestion is selected. + * @param object the suggestions + * @param selected the new selected value for the suggestion + */ + onSelected(object: Suggestion, selected: boolean) { + if (selected) { + this.selectedSuggestions[object.id] = object; + } else { + delete this.selectedSuggestions[object.id]; + } + } + + /** + * When Toggle Select All occurs. + * @param suggestions all the visible suggestions inside the page + */ + onToggleSelectAll(suggestions: Suggestion[]) { + if ( this.getSelectedSuggestionsCount() > 0) { + this.selectedSuggestions = {}; + } else { + suggestions.forEach((suggestion) => { + this.selectedSuggestions[suggestion.id] = suggestion; + }); + } + } + + /** + * The current number of selected suggestions. + */ + getSelectedSuggestionsCount(): number { + return Object.keys(this.selectedSuggestions).length; + } + + /** + * Return true if all the suggestion are configured with the same fixed collection in the configuration. + * @param suggestions + */ + isCollectionFixed(suggestions: Suggestion[]): boolean { + return this.suggestionService.isCollectionFixed(suggestions); + } + + /** + * Label to be used to translate the suggestion source. + */ + translateSuggestionSource() { + return this.suggestionService.translateSuggestionSource(this.suggestionSource); + } + + /** + * Label to be used to translate the suggestion type. + */ + translateSuggestionType() { + return this.suggestionService.translateSuggestionType(this.suggestionSource); + } + +} diff --git a/src/app/suggestions-page/suggestions-page.resolver.ts b/src/app/suggestions-page/suggestions-page.resolver.ts new file mode 100644 index 00000000000..1314846b6e6 --- /dev/null +++ b/src/app/suggestions-page/suggestions-page.resolver.ts @@ -0,0 +1,30 @@ +import { inject } from '@angular/core'; +import { + ActivatedRouteSnapshot, + ResolveFn, + RouterStateSnapshot, +} from '@angular/router'; +import { Observable } from 'rxjs'; + +import { RemoteData } from '../core/data/remote-data'; +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'; + +/** + * 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 + */ +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 5f741091f1e..c2c1942cdd3 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 @@ -1,27 +1,21 @@
-
+
- - {{'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 f767d0f196d..a1d6e3b27ee 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 @@ -1,16 +1,24 @@ -import { ComponentFixture, discardPeriodicTasks, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; -import { SystemWideAlertBannerComponent } from './system-wide-alert-banner.component'; -import { SystemWideAlertDataService } from '../../core/data/system-wide-alert-data.service'; -import { SystemWideAlert } from '../system-wide-alert.model'; -import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; -import { utcToZonedTime } from 'date-fns-tz'; -import { createPaginatedList } from '../../shared/testing/utils.test'; -import { TestScheduler } from 'rxjs/testing'; -import { getTestScheduler } from 'jasmine-marbles'; +import { + ComponentFixture, + discardPeriodicTasks, + fakeAsync, + TestBed, + tick, + waitForAsync, +} from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { TranslateModule } from '@ngx-translate/core'; +import { utcToZonedTime } from 'date-fns-tz'; +import { getTestScheduler } from 'jasmine-marbles'; +import { TestScheduler } from 'rxjs/testing'; + +import { SystemWideAlertDataService } from '../../core/data/system-wide-alert-data.service'; import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub'; +import { createPaginatedList } from '../../shared/testing/utils.test'; +import { SystemWideAlert } from '../system-wide-alert.model'; +import { SystemWideAlertBannerComponent } from './system-wide-alert-banner.component'; describe('SystemWideAlertBannerComponent', () => { @@ -33,7 +41,7 @@ describe('SystemWideAlertBannerComponent', () => { alertId: 1, message: 'Test alert message', active: true, - countdownTo: utcToZonedTime(countDownDate, 'UTC').toISOString() + countdownTo: utcToZonedTime(countDownDate, 'UTC').toISOString(), }); systemWideAlertDataService = jasmine.createSpyObj('systemWideAlertDataService', { @@ -41,12 +49,11 @@ describe('SystemWideAlertBannerComponent', () => { }); TestBed.configureTestingModule({ - imports: [TranslateModule.forRoot()], - declarations: [SystemWideAlertBannerComponent], + imports: [TranslateModule.forRoot(), SystemWideAlertBannerComponent], providers: [ - {provide: SystemWideAlertDataService, useValue: systemWideAlertDataService}, - {provide: NotificationsService, useValue: new NotificationsServiceStub()}, - ] + { provide: SystemWideAlertDataService, useValue: systemWideAlertDataService }, + { provide: NotificationsService, useValue: new NotificationsServiceStub() }, + ], }).compileComponents(); })); 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 27bdb14f1d7..79ecf2117a6 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,16 +1,38 @@ -import { Component, Inject, OnDestroy, OnInit, PLATFORM_ID } from '@angular/core'; -import { SystemWideAlertDataService } from '../../core/data/system-wide-alert-data.service'; import { - getAllSucceededRemoteDataPayload -} from '../../core/shared/operators'; -import { filter, map, switchMap } from 'rxjs/operators'; -import { PaginatedList } from '../../core/data/paginated-list.model'; -import { SystemWideAlert } from '../system-wide-alert.model'; -import { hasValue, isNotEmpty } from '../../shared/empty.util'; -import { BehaviorSubject, EMPTY, interval, Subscription } from 'rxjs'; + AsyncPipe, + isPlatformBrowser, + NgIf, +} from '@angular/common'; +import { + Component, + Inject, + OnDestroy, + OnInit, + PLATFORM_ID, +} from '@angular/core'; +import { TranslateModule } from '@ngx-translate/core'; import { zonedTimeToUtc } from 'date-fns-tz'; -import { isPlatformBrowser } from '@angular/common'; +import { + BehaviorSubject, + EMPTY, + interval, + Subscription, +} from 'rxjs'; +import { + filter, + map, + switchMap, +} from 'rxjs/operators'; + +import { PaginatedList } from '../../core/data/paginated-list.model'; +import { SystemWideAlertDataService } from '../../core/data/system-wide-alert-data.service'; +import { getAllSucceededRemoteDataPayload } from '../../core/shared/operators'; +import { + hasValue, + isNotEmpty, +} from '../../shared/empty.util'; import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { SystemWideAlert } from '../system-wide-alert.model'; /** * Component responsible for rendering a banner and the countdown for an active system-wide alert @@ -18,7 +40,9 @@ import { NotificationsService } from '../../shared/notifications/notifications.s @Component({ selector: 'ds-system-wide-alert-banner', styleUrls: ['./system-wide-alert-banner.component.scss'], - templateUrl: './system-wide-alert-banner.component.html' + templateUrl: './system-wide-alert-banner.component.html', + standalone: true, + imports: [NgIf, AsyncPipe, TranslateModule], }) export class SystemWideAlertBannerComponent implements OnInit, OnDestroy { @@ -48,7 +72,7 @@ export class SystemWideAlertBannerComponent implements OnInit, OnDestroy { subscriptions: Subscription[] = []; constructor( - @Inject(PLATFORM_ID) protected platformId: Object, + @Inject(PLATFORM_ID) protected platformId: any, protected systemWideAlertDataService: SystemWideAlertDataService, protected notificationsService: NotificationsService, ) { @@ -59,7 +83,7 @@ export class SystemWideAlertBannerComponent implements OnInit, OnDestroy { getAllSucceededRemoteDataPayload(), map((payload: PaginatedList) => payload.page), filter((page) => isNotEmpty(page)), - map((page) => page[0]) + map((page) => page[0]), ).subscribe((alert: SystemWideAlert) => { this.systemWideAlert$.next(alert); })); @@ -84,7 +108,7 @@ export class SystemWideAlertBannerComponent implements OnInit, OnDestroy { this.countDownHours.next(0); this.countDownMinutes.next(0); return EMPTY; - }) + }), ).subscribe(() => { this.setTimeDifference(this.systemWideAlert$.getValue().countdownTo); })); 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 169081e2777..770f465a4bc 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 @@ -1,27 +1,26 @@
- +

{{'system-wide-alert.form.header' | translate}}

+ [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,9 +28,8 @@
- + {{ 'system-wide-alert.form.label.countdownTo.enable' | translate }}
@@ -39,18 +37,11 @@
- - + +
@@ -66,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 505990b4455..8e0462d0842 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 @@ -1,19 +1,29 @@ -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -import { SystemWideAlertDataService } from '../../core/data/system-wide-alert-data.service'; -import { SystemWideAlert } from '../system-wide-alert.model'; -import { utcToZonedTime, zonedTimeToUtc } from 'date-fns-tz'; -import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; -import { createPaginatedList } from '../../shared/testing/utils.test'; +import { + ComponentFixture, + TestBed, + waitForAsync, +} from '@angular/core/testing'; +import { FormsModule } from '@angular/forms'; +import { Router } from '@angular/router'; import { TranslateModule } from '@ngx-translate/core'; -import { SystemWideAlertFormComponent } from './system-wide-alert-form.component'; +import { + utcToZonedTime, + zonedTimeToUtc, +} from 'date-fns-tz'; +import { UiSwitchModule } from 'ngx-ui-switch'; + import { RequestService } from '../../core/data/request.service'; +import { SystemWideAlertDataService } from '../../core/data/system-wide-alert-data.service'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { + createFailedRemoteDataObject$, + createSuccessfulRemoteDataObject$, +} from '../../shared/remote-data.utils'; import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub'; import { RouterStub } from '../../shared/testing/router.stub'; -import { NotificationsService } from '../../shared/notifications/notifications.service'; -import { Router } from '@angular/router'; -import { FormsModule } from '@angular/forms'; -import { UiSwitchModule } from 'ngx-ui-switch'; -import { SystemWideAlertModule } from '../system-wide-alert.module'; +import { createPaginatedList } from '../../shared/testing/utils.test'; +import { SystemWideAlert } from '../system-wide-alert.model'; +import { SystemWideAlertFormComponent } from './system-wide-alert-form.component'; describe('SystemWideAlertFormComponent', () => { let comp: SystemWideAlertFormComponent; @@ -37,13 +47,13 @@ describe('SystemWideAlertFormComponent', () => { alertId: 1, message: 'Test alert message', active: true, - countdownTo: utcToZonedTime(countDownDate, 'UTC').toISOString() + countdownTo: utcToZonedTime(countDownDate, 'UTC').toISOString(), }); systemWideAlertDataService = jasmine.createSpyObj('systemWideAlertDataService', { findAll: createSuccessfulRemoteDataObject$(createPaginatedList([systemWideAlert])), put: createSuccessfulRemoteDataObject$(systemWideAlert), - create: createSuccessfulRemoteDataObject$(systemWideAlert) + create: createSuccessfulRemoteDataObject$(systemWideAlert), }); requestService = jasmine.createSpyObj('requestService', ['setStaleByHrefSubstring']); @@ -52,14 +62,13 @@ 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}, - {provide: Router, useValue: router}, - {provide: RequestService, useValue: requestService}, - ] + { provide: SystemWideAlertDataService, useValue: systemWideAlertDataService }, + { provide: NotificationsService, useValue: notificationsService }, + { provide: Router, useValue: router }, + { provide: RequestService, useValue: requestService }, + ], }).compileComponents(); })); @@ -90,8 +99,8 @@ describe('SystemWideAlertFormComponent', () => { comp.createForm(); expect(comp.formMessage.value).toEqual(''); expect(comp.formActive.value).toEqual(false); - expect(comp.time).toEqual({hour: now.getHours(), minute: now.getMinutes()}); - expect(comp.date).toEqual({year: now.getFullYear(), month: now.getMonth() + 1, day: now.getDate()}); + expect(comp.time).toEqual({ hour: now.getHours(), minute: now.getMinutes() }); + expect(comp.date).toEqual({ year: now.getFullYear(), month: now.getMonth() + 1, day: now.getDate() }); }); }); @@ -103,11 +112,11 @@ describe('SystemWideAlertFormComponent', () => { expect(comp.formMessage.value).toEqual(systemWideAlert.message); expect(comp.formActive.value).toEqual(true); - expect(comp.time).toEqual({hour: countDownTo.getHours(), minute: countDownTo.getMinutes()}); + expect(comp.time).toEqual({ hour: countDownTo.getHours(), minute: countDownTo.getMinutes() }); expect(comp.date).toEqual({ year: countDownTo.getFullYear(), month: countDownTo.getMonth() + 1, - day: countDownTo.getDate() + day: countDownTo.getDate(), }); }); }); @@ -138,8 +147,8 @@ describe('SystemWideAlertFormComponent', () => { countDownDate.setHours(countDownDate.getHours() + 1); countDownDate.setMinutes(countDownDate.getMinutes() + 1); - comp.time = {hour: countDownDate.getHours(), minute: countDownDate.getMinutes()}; - comp.date = {year: countDownDate.getFullYear(), month: countDownDate.getMonth() + 1, day: countDownDate.getDate()}; + comp.time = { hour: countDownDate.getHours(), minute: countDownDate.getMinutes() }; + comp.date = { year: countDownDate.getFullYear(), month: countDownDate.getMonth() + 1, day: countDownDate.getDate() }; comp.updatePreviewTime(); @@ -161,14 +170,14 @@ 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; comp.formMessage.patchValue('New message'); comp.formActive.patchValue(true); - comp.time = {hour: 4, minute: 26}; - comp.date = {year: 2023, month: 1, day: 25}; + comp.time = { hour: 4, minute: 26 }; + comp.date = { year: 2023, month: 1, day: 25 }; const expectedAlert = new SystemWideAlert(); expectedAlert.alertId = systemWideAlert.alertId; @@ -184,14 +193,14 @@ 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; comp.formMessage.patchValue('New message'); comp.formActive.patchValue(true); - comp.time = {hour: 4, minute: 26}; - comp.date = {year: 2023, month: 1, day: 25}; + comp.time = { hour: 4, minute: 26 }; + comp.date = { year: 2023, month: 1, day: 25 }; const expectedAlert = new SystemWideAlert(); expectedAlert.alertId = systemWideAlert.alertId; @@ -207,14 +216,14 @@ 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; comp.formMessage.patchValue('New message'); comp.formActive.patchValue(true); - comp.time = {hour: 4, minute: 26}; - comp.date = {year: 2023, month: 1, day: 25}; + comp.time = { hour: 4, minute: 26 }; + comp.date = { year: 2023, month: 1, day: 25 }; comp.counterEnabled$.next(false); const expectedAlert = new SystemWideAlert(); @@ -230,15 +239,15 @@ 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; comp.formMessage.patchValue('New message'); comp.formActive.patchValue(true); - comp.time = {hour: 4, minute: 26}; - comp.date = {year: 2023, month: 1, day: 25}; + comp.time = { hour: 4, minute: 26 }; + comp.date = { year: 2023, month: 1, day: 25 }; const expectedAlert = new SystemWideAlert(); expectedAlert.alertId = systemWideAlert.alertId; @@ -260,8 +269,8 @@ describe('SystemWideAlertFormComponent', () => { comp.formMessage.patchValue('New message'); comp.formActive.patchValue(true); - comp.time = {hour: 4, minute: 26}; - comp.date = {year: 2023, month: 1, day: 25}; + comp.time = { hour: 4, minute: 26 }; + comp.date = { year: 2023, month: 1, day: 25 }; const expectedAlert = new SystemWideAlert(); expectedAlert.message = 'New message'; @@ -285,8 +294,8 @@ describe('SystemWideAlertFormComponent', () => { comp.formMessage.patchValue('New message'); comp.formActive.patchValue(true); - comp.time = {hour: 4, minute: 26}; - comp.date = {year: 2023, month: 1, day: 25}; + comp.time = { hour: 4, minute: 26 }; + comp.date = { year: 2023, month: 1, day: 25 }; const expectedAlert = new SystemWideAlert(); expectedAlert.message = 'New message'; @@ -302,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 eaff12c169d..b30e864fa12 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,19 +1,54 @@ -import { Component, OnInit } from '@angular/core'; -import { SystemWideAlertDataService } from '../../core/data/system-wide-alert-data.service'; -import { getFirstCompletedRemoteData } from '../../core/shared/operators'; -import { filter, map } from 'rxjs/operators'; +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 { + 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, +} from 'rxjs'; +import { + filter, + map, +} from 'rxjs/operators'; + import { PaginatedList } from '../../core/data/paginated-list.model'; -import { SystemWideAlert } from '../system-wide-alert.model'; -import { hasValue, isNotEmpty } from '../../shared/empty.util'; -import { BehaviorSubject, Observable } from 'rxjs'; -import { UntypedFormBuilder, UntypedFormControl, UntypedFormGroup, Validators } from '@angular/forms'; -import { NgbDateStruct } from '@ng-bootstrap/ng-bootstrap'; -import { utcToZonedTime, zonedTimeToUtc } from 'date-fns-tz'; import { RemoteData } from '../../core/data/remote-data'; -import { NotificationsService } from '../../shared/notifications/notifications.service'; -import { Router } from '@angular/router'; import { RequestService } from '../../core/data/request.service'; -import { TranslateService } from '@ngx-translate/core'; +import { SystemWideAlertDataService } from '../../core/data/system-wide-alert-data.service'; +import { getFirstCompletedRemoteData } from '../../core/shared/operators'; +import { + hasValue, + isNotEmpty, +} from '../../shared/empty.util'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { SystemWideAlert } from '../system-wide-alert.model'; /** @@ -22,7 +57,9 @@ import { TranslateService } from '@ngx-translate/core'; @Component({ selector: 'ds-system-wide-alert-form', styleUrls: ['./system-wide-alert-form.component.scss'], - templateUrl: './system-wide-alert-form.component.html' + templateUrl: './system-wide-alert-form.component.html', + standalone: true, + imports: [FormsModule, ReactiveFormsModule, UiSwitchModule, NgIf, NgbDatepickerModule, NgbTimepickerModule, AsyncPipe, TranslateModule], }) export class SystemWideAlertFormComponent implements OnInit { @@ -82,7 +119,7 @@ export class SystemWideAlertFormComponent implements OnInit { protected notificationsService: NotificationsService, protected router: Router, protected requestService: RequestService, - protected translateService: TranslateService + protected translateService: TranslateService, ) { } @@ -98,12 +135,12 @@ export class SystemWideAlertFormComponent implements OnInit { }), map((payload: PaginatedList) => payload.page), filter((page) => isNotEmpty(page)), - map((page) => page[0]) + map((page) => page[0]), ); this.createForm(); const currentDate = new Date(); - this.minDate = {year: currentDate.getFullYear(), month: currentDate.getMonth() + 1, day: currentDate.getDate()}; + this.minDate = { year: currentDate.getFullYear(), month: currentDate.getMonth() + 1, day: currentDate.getDate() }; this.systemWideAlert$.subscribe((alert) => { @@ -117,11 +154,11 @@ export class SystemWideAlertFormComponent implements OnInit { */ createForm() { this.alertForm = new UntypedFormBuilder().group({ - formMessage: new UntypedFormControl('', { - validators: [Validators.required], - }), - formActive: new UntypedFormControl(false), - } + formMessage: new UntypedFormControl('', { + validators: [Validators.required], + }), + formActive: new UntypedFormControl(false), + }, ); this.setDateTime(new Date()); } @@ -168,8 +205,8 @@ export class SystemWideAlertFormComponent implements OnInit { private setDateTime(dateToSet) { - this.time = {hour: dateToSet.getHours(), minute: dateToSet.getMinutes()}; - this.date = {year: dateToSet.getFullYear(), month: dateToSet.getMonth() + 1, day: dateToSet.getDate()}; + this.time = { hour: dateToSet.getHours(), minute: dateToSet.getMinutes() }; + this.date = { year: dateToSet.getFullYear(), month: dateToSet.getMonth() + 1, day: dateToSet.getDate() }; this.updatePreviewTime(); } @@ -219,17 +256,19 @@ 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); + } } } private handleResponse(response$: Observable>, messagePrefix, navigateToHomePage: boolean) { response$.pipe( - getFirstCompletedRemoteData() + getFirstCompletedRemoteData(), ).subscribe((response: RemoteData) => { if (response.hasSucceeded) { this.notificationsService.success(this.translateService.get(`${messagePrefix}.success`)); 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 00000000000..a71007b6b3c --- /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 beb1b32187a..00000000000 --- a/src/app/system-wide-alert/system-wide-alert-routing.module.ts +++ /dev/null @@ -1,22 +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 158deb26036..73bd7610a9d 100644 --- a/src/app/system-wide-alert/system-wide-alert.model.ts +++ b/src/app/system-wide-alert/system-wide-alert.model.ts @@ -1,4 +1,8 @@ -import { autoserialize, deserialize } from 'cerialize'; +import { + autoserialize, + deserialize, +} from 'cerialize'; + import { typedObject } from '../core/cache/builders/build-decorators'; import { CacheableObject } from '../core/cache/cacheable-object.model'; import { HALLink } from '../core/shared/hal-link.model'; 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 ca200fa4f1c..00000000000 --- a/src/app/system-wide-alert/system-wide-alert.module.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { NgModule } from '@angular/core'; -import { FormsModule } from '@angular/forms'; -import { SystemWideAlertBannerComponent } from './alert-banner/system-wide-alert-banner.component'; -import { SystemWideAlertFormComponent } from './alert-form/system-wide-alert-form.component'; -import { SharedModule } from '../shared/shared.module'; -import { SystemWideAlertDataService } from '../core/data/system-wide-alert-data.service'; -import { SystemWideAlertRoutingModule } from './system-wide-alert-routing.module'; -import { UiSwitchModule } from 'ngx-ui-switch'; -import { NgbDatepickerModule, NgbTimepickerModule } from '@ng-bootstrap/ng-bootstrap'; - -@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 2a8d809104a..6d14378d3a5 100644 --- a/src/app/thumbnail/themed-thumbnail.component.ts +++ b/src/app/thumbnail/themed-thumbnail.component.ts @@ -1,13 +1,19 @@ +import { + Component, + Input, +} from '@angular/core'; + +import { RemoteData } from '../core/data/remote-data'; +import { Bitstream } from '../core/shared/bitstream.model'; import { ThemedComponent } from '../shared/theme-support/themed.component'; -import { Component, Input } from '@angular/core'; import { ThumbnailComponent } from './thumbnail.component'; -import { Bitstream } from '../core/shared/bitstream.model'; -import { RemoteData } from '../core/data/remote-data'; @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 6a0516b0d47..e151684a01f 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 ebecb5e075b..10e461112bb 100644 --- a/src/app/thumbnail/thumbnail.component.spec.ts +++ b/src/app/thumbnail/thumbnail.component.spec.ts @@ -1,20 +1,37 @@ -import { DebugElement, Pipe, PipeTransform } from '@angular/core'; -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { + DebugElement, + Pipe, + PipeTransform, +} from '@angular/core'; +import { + ComponentFixture, + TestBed, + waitForAsync, +} from '@angular/core/testing'; import { By } from '@angular/platform-browser'; -import { Bitstream } from '../core/shared/bitstream.model'; -import { SafeUrlPipe } from '../shared/utils/safe-url-pipe'; +import { TranslateModule } from '@ngx-translate/core'; import { of as observableOf } from 'rxjs'; -import { ThumbnailComponent } from './thumbnail.component'; -import { RemoteData } from '../core/data/remote-data'; -import { createFailedRemoteDataObject, createSuccessfulRemoteDataObject } from '../shared/remote-data.utils'; import { AuthService } from '../core/auth/auth.service'; +import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service'; +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 { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service'; +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; @@ -40,18 +57,30 @@ describe('ThumbnailComponent', () => { isAuthorized: observableOf(true), }); fileService = jasmine.createSpyObj('FileService', { - retrieveFileDownloadLink: null + retrieveFileDownloadLink: null, }); 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 } - ] - }).compileComponents(); + { provide: FileService, useValue: fileService }, + { provide: ThemeService, useValue: getMockThemeService() }, + ], + }).overrideComponent(ThumbnailComponent, { + add: { + imports: [MockTranslatePipe], + }, + }) + .compileComponents(); })); beforeEach(() => { @@ -67,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(); }); @@ -100,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(); @@ -111,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(); }); @@ -214,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(); @@ -233,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; @@ -290,7 +319,7 @@ describe('ThumbnailComponent', () => { bundle: { href: 'bundle.url' }, format: { href: 'format.url' }, content: { href: CONTENT }, - thumbnail: undefined + thumbnail: undefined, }; }); @@ -328,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 7f6531cc5e3..cc583c3998f 100644 --- a/src/app/thumbnail/thumbnail.component.ts +++ b/src/app/thumbnail/thumbnail.component.ts @@ -1,13 +1,27 @@ -import { Component, Input, OnChanges, SimpleChanges } from '@angular/core'; -import { Bitstream } from '../core/shared/bitstream.model'; -import { hasNoValue, hasValue } from '../shared/empty.util'; -import { RemoteData } from '../core/data/remote-data'; -import { BehaviorSubject, of as observableOf } from 'rxjs'; +import { CommonModule } from '@angular/common'; +import { + Component, + Input, + OnChanges, + SimpleChanges, +} from '@angular/core'; +import { TranslateModule } from '@ngx-translate/core'; +import { of as observableOf } from 'rxjs'; import { switchMap } from 'rxjs/operators'; -import { FeatureID } from '../core/data/feature-authorization/feature-id'; -import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service'; + 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 { RemoteData } from '../core/data/remote-data'; +import { Bitstream } from '../core/shared/bitstream.model'; import { FileService } from '../core/shared/file.service'; +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. @@ -15,9 +29,11 @@ import { FileService } from '../core/shared/file.service'; * 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 { /** @@ -34,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; @@ -57,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, @@ -110,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; @@ -134,7 +150,7 @@ export class ThumbnailComponent implements OnChanges { } else { return observableOf(null); } - }) + }), ).subscribe((url: string) => { if (hasValue(url)) { // If we got a URL, try to load it @@ -162,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; } } @@ -172,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 cbb85b6ad87..d370c83a93e 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 @@ -1,8 +1,13 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { AdvancedWorkflowActionPageComponent } from './advanced-workflow-action-page.component'; +import { + ComponentFixture, + TestBed, +} from '@angular/core/testing'; 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', () => { let component: AdvancedWorkflowActionPageComponent; let fixture: ComponentFixture; @@ -11,8 +16,6 @@ describe('AdvancedWorkflowActionPageComponent', () => { await TestBed.configureTestingModule({ imports: [ TranslateModule.forRoot(), - ], - declarations: [ AdvancedWorkflowActionPageComponent, ], providers: [ @@ -27,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 91dce19a5ea..91796e826fe 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 @@ -1,5 +1,11 @@ -import { Component, OnInit } from '@angular/core'; +import { + Component, + 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} @@ -8,7 +14,12 @@ import { ActivatedRoute } from '@angular/router'; @Component({ selector: 'ds-advanced-workflow-action-page', templateUrl: './advanced-workflow-action-page.component.html', - styleUrls: ['./advanced-workflow-action-page.component.scss'] + 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 d6d6f973c44..fe450237185 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 @@ -1,35 +1,48 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; import { Location } from '@angular/common'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; import { - AdvancedWorkflowActionRatingComponent, - ADVANCED_WORKFLOW_TASK_OPTION_RATING -} from './advanced-workflow-action-rating.component'; -import { ActivatedRoute, Router } from '@angular/router'; + ComponentFixture, + TestBed, +} from '@angular/core/testing'; +import { + FormsModule, + ReactiveFormsModule, +} from '@angular/forms'; +import { + ActivatedRoute, + Router, +} from '@angular/router'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { TranslateModule } from '@ngx-translate/core'; import { of as observableOf } from 'rxjs'; -import { ClaimedTaskDataService } from '../../../core/tasks/claimed-task-data.service'; -import { NotificationsService } from '../../../shared/notifications/notifications.service'; -import { RouteService } from '../../../core/services/route.service'; -import { routeServiceStub } from '../../../shared/testing/route-service.stub'; + +import { RequestService } from '../../../core/data/request.service'; import { WorkflowActionDataService } from '../../../core/data/workflow-action-data.service'; +import { RouteService } from '../../../core/services/route.service'; +import { Item } from '../../../core/shared/item.model'; +import { WorkflowItem } from '../../../core/submission/models/workflowitem.model'; import { WorkflowItemDataService } from '../../../core/submission/workflowitem-data.service'; +import { ClaimedTaskDataService } from '../../../core/tasks/claimed-task-data.service'; +import { ProcessTaskResponse } from '../../../core/tasks/models/process-task-response'; +import { RatingAdvancedWorkflowInfo } from '../../../core/tasks/models/rating-advanced-workflow-info.model'; +import { NotificationsService } from '../../../shared/notifications/notifications.service'; +import { + createSuccessfulRemoteDataObject, + createSuccessfulRemoteDataObject$, +} from '../../../shared/remote-data.utils'; 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 { RouterStub } from '../../../shared/testing/router.stub'; import { WorkflowActionDataServiceStub } from '../../../shared/testing/workflow-action-data-service.stub'; import { WorkflowItemDataServiceStub } from '../../../shared/testing/workflow-item-data-service.stub'; -import { RouterStub } from '../../../shared/testing/router.stub'; -import { TranslateModule } from '@ngx-translate/core'; import { VarDirective } from '../../../shared/utils/var.directive'; -import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; -import { FormsModule, ReactiveFormsModule } from '@angular/forms'; -import { WorkflowItem } from '../../../core/submission/models/workflowitem.model'; -import { createSuccessfulRemoteDataObject$, createSuccessfulRemoteDataObject } from '../../../shared/remote-data.utils'; -import { Item } from '../../../core/shared/item.model'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { ProcessTaskResponse } from '../../../core/tasks/models/process-task-response'; -import { RatingAdvancedWorkflowInfo } from '../../../core/tasks/models/rating-advanced-workflow-info.model'; -import { RequestService } from '../../../core/data/request.service'; -import { RequestServiceStub } from '../../../shared/testing/request-service.stub'; -import { LocationStub } from '../../../shared/testing/location.stub'; +import { + ADVANCED_WORKFLOW_TASK_OPTION_RATING, + AdvancedWorkflowActionRatingComponent, +} from './advanced-workflow-action-rating.component'; const claimedTaskId = '2'; const workflowId = '1'; @@ -57,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 ebf1ead64ab..b8620e7d897 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,11 +1,26 @@ -import { Component, OnInit } from '@angular/core'; import { - rendersAdvancedWorkflowTaskOption -} from '../../../shared/mydspace-actions/claimed-task/switcher/claimed-task-actions-decorator'; -import { AdvancedWorkflowActionComponent } from '../advanced-workflow-action/advanced-workflow-action.component'; -import { UntypedFormGroup, UntypedFormControl, Validators } from '@angular/forms'; -import { WorkflowAction } from '../../../core/tasks/models/workflow-action-object.model'; + 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 { 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'; export const ADVANCED_WORKFLOW_ACTION_RATING = 'scorereviewaction'; @@ -13,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 71952ce9c70..eb79aa390cc 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 @@ -1,32 +1,45 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; import { Location } from '@angular/common'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; import { - AdvancedWorkflowActionSelectReviewerComponent, - ADVANCED_WORKFLOW_TASK_OPTION_SELECT_REVIEWER, -} from './advanced-workflow-action-select-reviewer.component'; -import { ActivatedRoute, Router } from '@angular/router'; -import { WorkflowItemDataService } from '../../../core/submission/workflowitem-data.service'; -import { WorkflowItemDataServiceStub } from '../../../shared/testing/workflow-item-data-service.stub'; -import { WorkflowActionDataServiceStub } from '../../../shared/testing/workflow-action-data-service.stub'; -import { WorkflowActionDataService } from '../../../core/data/workflow-action-data.service'; -import { RouteService } from '../../../core/services/route.service'; -import { routeServiceStub } from '../../../shared/testing/route-service.stub'; -import { NotificationsService } from '../../../shared/notifications/notifications.service'; -import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub'; + ComponentFixture, + TestBed, +} from '@angular/core/testing'; +import { + ActivatedRoute, + Router, +} from '@angular/router'; import { TranslateModule } from '@ngx-translate/core'; -import { ClaimedTaskDataService } from '../../../core/tasks/claimed-task-data.service'; -import { ClaimedTaskDataServiceStub } from '../../../shared/testing/claimed-task-data-service.stub'; import { of as observableOf } from 'rxjs'; -import { WorkflowItem } from '../../../core/submission/models/workflowitem.model'; -import { createSuccessfulRemoteDataObject$, createSuccessfulRemoteDataObject } from '../../../shared/remote-data.utils'; + +import { RequestService } from '../../../core/data/request.service'; +import { WorkflowActionDataService } from '../../../core/data/workflow-action-data.service'; +import { RouteService } from '../../../core/services/route.service'; import { Item } from '../../../core/shared/item.model'; -import { EPersonMock, EPersonMock2 } from '../../../shared/testing/eperson.mock'; +import { WorkflowItem } from '../../../core/submission/models/workflowitem.model'; +import { WorkflowItemDataService } from '../../../core/submission/workflowitem-data.service'; +import { ClaimedTaskDataService } from '../../../core/tasks/claimed-task-data.service'; import { ProcessTaskResponse } from '../../../core/tasks/models/process-task-response'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { RequestService } from '../../../core/data/request.service'; +import { NotificationsService } from '../../../shared/notifications/notifications.service'; +import { + createSuccessfulRemoteDataObject, + createSuccessfulRemoteDataObject$, +} from '../../../shared/remote-data.utils'; +import { ClaimedTaskDataServiceStub } from '../../../shared/testing/claimed-task-data-service.stub'; +import { + EPersonMock, + EPersonMock2, +} from '../../../shared/testing/eperson.mock'; +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 { RouterStub } from '../../../shared/testing/router.stub'; -import { LocationStub } from '../../../shared/testing/location.stub'; +import { WorkflowActionDataServiceStub } from '../../../shared/testing/workflow-action-data-service.stub'; +import { WorkflowItemDataServiceStub } from '../../../shared/testing/workflow-item-data-service.stub'; +import { + ADVANCED_WORKFLOW_TASK_OPTION_SELECT_REVIEWER, + AdvancedWorkflowActionSelectReviewerComponent, +} from './advanced-workflow-action-select-reviewer.component'; const claimedTaskId = '2'; const workflowId = '1'; @@ -55,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 329af733510..72f5623626c 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,27 +1,37 @@ -import { Component, OnInit, OnDestroy } from '@angular/core'; -import { Location } from '@angular/common'; import { - rendersAdvancedWorkflowTaskOption -} from '../../../shared/mydspace-actions/claimed-task/switcher/claimed-task-actions-decorator'; -import { AdvancedWorkflowActionComponent } from '../advanced-workflow-action/advanced-workflow-action.component'; -import { WorkflowAction } from '../../../core/tasks/models/workflow-action-object.model'; + CommonModule, + Location, +} from '@angular/common'; import { - SelectReviewerAdvancedWorkflowInfo -} from '../../../core/tasks/models/select-reviewer-advanced-workflow-info.model'; + Component, + OnDestroy, + OnInit, +} from '@angular/core'; import { - EPersonListActionConfig -} from '../../../access-control/group-registry/group-form/members-list/members-list.component'; + ActivatedRoute, + Params, + Router, +} from '@angular/router'; +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'; +import { RequestService } from '../../../core/data/request.service'; +import { WorkflowActionDataService } from '../../../core/data/workflow-action-data.service'; import { EPerson } from '../../../core/eperson/models/eperson.model'; -import { ActivatedRoute, Params, Router } from '@angular/router'; -import { WorkflowItemDataService } from '../../../core/submission/workflowitem-data.service'; import { RouteService } from '../../../core/services/route.service'; -import { NotificationsService } from '../../../shared/notifications/notifications.service'; -import { TranslateService } from '@ngx-translate/core'; -import { WorkflowActionDataService } from '../../../core/data/workflow-action-data.service'; +import { WorkflowItemDataService } from '../../../core/submission/workflowitem-data.service'; import { ClaimedTaskDataService } from '../../../core/tasks/claimed-task-data.service'; -import { RequestService } from '../../../core/data/request.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 { 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'; @@ -29,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 { @@ -84,7 +100,7 @@ export class AdvancedWorkflowActionSelectReviewerComponent extends AdvancedWorkf remove: { css: 'btn-outline-danger', disabled: false, - icon: 'fas fa-minus' + icon: 'fas fa-minus', }, }; } else { @@ -97,7 +113,7 @@ export class AdvancedWorkflowActionSelectReviewerComponent extends AdvancedWorkf remove: { css: 'btn-primary', disabled: true, - icon: 'fas fa-check' + icon: 'fas fa-check', }, }; } 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 7c8db782ce6..e826de1c0e4 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 @@ -1,38 +1,78 @@ import { CommonModule } from '@angular/common'; -import { NO_ERRORS_SCHEMA, SimpleChange, DebugElement } from '@angular/core'; -import { ComponentFixture, fakeAsync, flush, TestBed, waitForAsync } from '@angular/core/testing'; -import { FormsModule, ReactiveFormsModule } from '@angular/forms'; -import { BrowserModule, By } from '@angular/platform-browser'; -import { Router } from '@angular/router'; +import { + DebugElement, + NO_ERRORS_SCHEMA, + SimpleChange, +} from '@angular/core'; +import { + ComponentFixture, + fakeAsync, + flush, + TestBed, + waitForAsync, +} from '@angular/core/testing'; +import { + FormsModule, + ReactiveFormsModule, +} from '@angular/forms'; +import { + BrowserModule, + By, +} from '@angular/platform-browser'; +import { + ActivatedRoute, + Router, +} from '@angular/router'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; -import { TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core'; -import { Observable, of as observableOf } from 'rxjs'; +import { + TranslateLoader, + TranslateModule, + TranslateService, +} from '@ngx-translate/core'; +import { + Observable, + of as observableOf, +} from 'rxjs'; + import { RestResponse } from '../../../../core/cache/response.models'; -import { buildPaginatedList, 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 { 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 { GroupMock, GroupMock2 } from '../../../../shared/testing/group-mock'; -import { ReviewersListComponent } from './reviewers-list.component'; -import { EPersonMock, EPersonMock2 } from '../../../../shared/testing/eperson.mock'; +import { PaginationComponent } from '../../../../shared/pagination/pagination.component'; import { + createNoContentRemoteDataObject$, createSuccessfulRemoteDataObject$, - createNoContentRemoteDataObject$ } from '../../../../shared/remote-data.utils'; -import { getMockTranslateService } from '../../../../shared/mocks/translate.service.mock'; -import { getMockFormBuilderService } from '../../../../shared/mocks/form-builder-service.mock'; -import { TranslateLoaderMock } from '../../../../shared/testing/translate-loader.mock'; +import { ActivatedRouteStub } from '../../../../shared/testing/active-router.stub'; +import { + EPersonMock, + EPersonMock2, +} from '../../../../shared/testing/eperson.mock'; +import { GroupMock } from '../../../../shared/testing/group-mock'; import { NotificationsServiceStub } from '../../../../shared/testing/notifications-service.stub'; -import { RouterMock } from '../../../../shared/mocks/router.mock'; -import { PaginationService } from '../../../../core/pagination/pagination.service'; import { PaginationServiceStub } from '../../../../shared/testing/pagination-service.stub'; -import { EpersonDtoModel } from '../../../../core/eperson/models/eperson-dto.model'; +import { TranslateLoaderMock } from '../../../../shared/testing/translate-loader.mock'; +import { ReviewersListComponent } from './reviewers-list.component'; +// todo: optimize imports + +// NOTE: Because ReviewersListComponent extends MembersListComponent, the below tests ONLY validate +// features which are *unique* to ReviewersListComponent. All other features are tested in the +// members-list.component.spec.ts file. describe('ReviewersListComponent', () => { let component: ReviewersListComponent; let fixture: ComponentFixture; @@ -40,31 +80,27 @@ describe('ReviewersListComponent', () => { let builderService: FormBuilderService; let ePersonDataServiceStub: any; let groupsDataServiceStub: any; - let activeGroup; - let allEPersons; - let allGroups; - let epersonMembers; - let subgroupMembers; + let activeGroup: Group; + let epersonMembers: EPerson[]; + let epersonNonMembers: EPerson[]; let paginationService; - let ePersonDtoModel1: EpersonDtoModel; - let ePersonDtoModel2: EpersonDtoModel; beforeEach(waitForAsync(() => { activeGroup = GroupMock; epersonMembers = [EPersonMock2]; - subgroupMembers = [GroupMock2]; - allEPersons = [EPersonMock, EPersonMock2]; - allGroups = [GroupMock, GroupMock2]; + epersonNonMembers = [EPersonMock]; ePersonDataServiceStub = { activeGroup: activeGroup, epersonMembers: epersonMembers, - subgroupMembers: subgroupMembers, + epersonNonMembers: epersonNonMembers, + // This method is used to get all the current members findListByHref(_href: string): Observable>> { return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), groupsDataServiceStub.getEPersonMembers())); }, + // This method is used to search across *non-members* searchByScope(scope: string, query: string): Observable>> { if (query === '') { - return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), allEPersons)); + return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), epersonNonMembers)); } return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), [])); }, @@ -74,29 +110,26 @@ describe('ReviewersListComponent', () => { clearLinkRequests() { // empty }, - getEPeoplePageRouterLink(): string { - return '/access-control/epeople'; - } }; groupsDataServiceStub = { activeGroup: activeGroup, epersonMembers: epersonMembers, - subgroupMembers: subgroupMembers, - allGroups: allGroups, + epersonNonMembers: epersonNonMembers, getActiveGroup(): Observable { return observableOf(activeGroup); }, getEPersonMembers() { return this.epersonMembers; }, - searchGroups(query: string): Observable>> { - if (query === '') { - return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), this.allGroups)); - } - return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), [])); - }, - addMemberToGroup(parentGroup, eperson: EPerson): Observable { - this.epersonMembers = [...this.epersonMembers, eperson]; + addMemberToGroup(parentGroup, epersonToAdd: EPerson): Observable { + // Add eperson to list of members + this.epersonMembers = [...this.epersonMembers, epersonToAdd]; + // Remove eperson from list of non-members + this.epersonNonMembers.forEach( (eperson: EPerson, index: number) => { + if (eperson.id === epersonToAdd.id) { + this.epersonNonMembers.splice(index, 1); + } + }); return observableOf(new RestResponse(true, 200, 'Success')); }, clearGroupsRequests() { @@ -109,42 +142,39 @@ describe('ReviewersListComponent', () => { return '/access-control/groups/' + group.id; }, deleteMemberFromGroup(parentGroup, epersonToDelete: EPerson): Observable { - this.epersonMembers = this.epersonMembers.find((eperson: EPerson) => { - if (eperson.id !== epersonToDelete.id) { - return eperson; + // Remove eperson from list of members + this.epersonMembers.forEach( (eperson: EPerson, index: number) => { + if (eperson.id === epersonToDelete.id) { + this.epersonMembers.splice(index, 1); } }); - if (this.epersonMembers === undefined) { - this.epersonMembers = []; - } + // Add eperson to list of non-members + this.epersonNonMembers = [...this.epersonNonMembers, epersonToDelete]; return observableOf(new RestResponse(true, 200, 'Success')); }, + // Used to find the currently active group findById(id: string) { - for (const group of allGroups) { - if (group.id === id) { - return createSuccessfulRemoteDataObject$(group); - } + if (activeGroup.id === id) { + return createSuccessfulRemoteDataObject$(activeGroup); } return createNoContentRemoteDataObject$(); }, editGroup() { // empty - } + }, }; builderService = getMockFormBuilderService(); translateService = getMockTranslateService(); paginationService = new PaginationServiceStub(); - TestBed.configureTestingModule({ + return TestBed.configureTestingModule({ imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule, TranslateModule.forRoot({ loader: { provide: TranslateLoader, - useClass: TranslateLoaderMock - } - }), - ], - declarations: [ReviewersListComponent], + useClass: TranslateLoaderMock, + }, + }), ReviewersListComponent], providers: [ReviewersListComponent, { provide: EPersonDataService, useValue: ePersonDataServiceStub }, { provide: GroupDataService, useValue: groupsDataServiceStub }, @@ -152,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(); + schemas: [NO_ERRORS_SCHEMA], + }) + .overrideComponent(ReviewersListComponent, { + remove: { + imports: [ContextHelpDirective, PaginationComponent], + }, + }) + .compileComponents(); })); beforeEach(() => { @@ -169,17 +206,11 @@ describe('ReviewersListComponent', () => { fixture.debugElement.nativeElement.remove(); })); - beforeEach(() => { - ePersonDtoModel1 = new EpersonDtoModel(); - ePersonDtoModel1.eperson = EPersonMock; - ePersonDtoModel2 = new EpersonDtoModel(); - ePersonDtoModel2.eperson = EPersonMock2; - }); describe('when no group is selected', () => { beforeEach(() => { component.ngOnChanges({ - groupId: new SimpleChange(undefined, null, true) + groupId: new SimpleChange(undefined, null, true), }); fixture.detectChanges(); }); @@ -193,12 +224,44 @@ 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', () => { beforeEach(() => { component.ngOnChanges({ - groupId: new SimpleChange(undefined, GroupMock.id, true) + groupId: new SimpleChange(undefined, GroupMock.id, true), }); fixture.detectChanges(); }); @@ -214,39 +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 = [ePersonDtoModel1]; - - component.addMemberToGroup(ePersonDtoModel2); - - expect(component.selectedReviewers).toEqual([ePersonDtoModel2]); - expect(component.selectedReviewersUpdated.emit).toHaveBeenCalledWith([ePersonDtoModel2.eperson]); - }); - - it('should add the value when a new member is added when multipleReviewers is true', () => { - spyOn(component.selectedReviewersUpdated, 'emit'); - component.multipleReviewers = true; - component.selectedReviewers = [ePersonDtoModel1]; - - component.addMemberToGroup(ePersonDtoModel2); - - expect(component.selectedReviewers).toEqual([ePersonDtoModel1, ePersonDtoModel2]); - expect(component.selectedReviewersUpdated.emit).toHaveBeenCalledWith([ePersonDtoModel1.eperson, ePersonDtoModel2.eperson]); - }); - - it('should delete the member when present', () => { - spyOn(component.selectedReviewersUpdated, 'emit'); - ePersonDtoModel1.memberOfGroup = true; - component.selectedReviewers = [ePersonDtoModel1]; - - component.deleteMemberFromGroup(ePersonDtoModel1); - - expect(component.selectedReviewers).toEqual([]); - expect(ePersonDtoModel1.memberOfGroup).toBeFalse(); - 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 6984a1d86dd..5ae3a13f313 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,31 +1,61 @@ -import { Component, OnDestroy, OnInit, Input, OnChanges, SimpleChanges, EventEmitter, Output } from '@angular/core'; -import { UntypedFormBuilder } from '@angular/forms'; -import { Router } from '@angular/router'; -import { TranslateService } from '@ngx-translate/core'; +import { + AsyncPipe, + NgClass, + NgForOf, + NgIf, +} from '@angular/common'; +import { + Component, + EventEmitter, + Input, + OnChanges, + OnDestroy, + OnInit, + Output, + SimpleChanges, +} from '@angular/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, + MembersListComponent, +} from '../../../../access-control/group-registry/group-form/members-list/members-list.component'; +import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service'; +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 { NotificationsService } from '../../../../shared/notifications/notifications.service'; -import { PaginationService } from '../../../../core/pagination/pagination.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 { EpersonDtoModel } from '../../../../core/eperson/models/eperson-dto.model'; -import { EPerson } from '../../../../core/eperson/models/eperson.model'; -import { Observable, of as observableOf } from 'rxjs'; +import { ContextHelpDirective } from '../../../../shared/context-help.directive'; import { hasValue } from '../../../../shared/empty.util'; -import { PaginatedList } from '../../../../core/data/paginated-list.model'; -import { - MembersListComponent, - EPersonListActionConfig, -} from '../../../../access-control/group-registry/group-form/members-list/members-list.component'; -import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service'; +import { NotificationsService } from '../../../../shared/notifications/notifications.service'; +import { PaginationComponent } from '../../../../shared/pagination/pagination.component'; /** * Keys to keep track of specific subscriptions */ enum SubKey { ActiveGroup, - MembersDTO, - SearchResultsDTO, + Members, + SearchResults, } /** @@ -35,6 +65,18 @@ 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 { @@ -50,7 +92,7 @@ export class ReviewersListComponent extends MembersListComponent implements OnIn @Output() selectedReviewersUpdated: EventEmitter = new EventEmitter(); - selectedReviewers: EpersonDtoModel[] = []; + selectedReviewers: EPerson[] = []; constructor( protected groupService: GroupDataService, @@ -65,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: '', @@ -78,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) => { @@ -100,10 +143,12 @@ export class ReviewersListComponent extends MembersListComponent implements OnIn retrieveMembers(page: number): void { this.config.currentPage = page; if (this.groupId === null) { - this.unsubFrom(SubKey.MembersDTO); - const paginatedListOfDTOs: PaginatedList = new PaginatedList(); - paginatedListOfDTOs.page = this.selectedReviewers; - this.ePeopleMembersOfGroupDtos.next(paginatedListOfDTOs); + 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); } @@ -115,39 +160,36 @@ export class ReviewersListComponent extends MembersListComponent implements OnIn * @param possibleMember The {@link EPerson} that needs to be checked */ isMemberOfGroup(possibleMember: EPerson): Observable { - return observableOf(hasValue(this.selectedReviewers.find((reviewer: EpersonDtoModel) => reviewer.eperson.id === possibleMember.id))); + return observableOf(hasValue(this.selectedReviewers.find((reviewer: EPerson) => reviewer.id === possibleMember.id))); } /** - * Removes the {@link ePerson} from the {@link selectedReviewers} + * Removes the {@link eperson} from the {@link selectedReviewers} * - * @param ePerson The {@link EpersonDtoModel} containg the {@link EPerson} to remove + * @param eperson The {@link EPerson} to remove */ - deleteMemberFromGroup(ePerson: EpersonDtoModel) { - ePerson.memberOfGroup = false; - const index = this.selectedReviewers.indexOf(ePerson); + deleteMemberFromGroup(eperson: EPerson) { + const index = this.selectedReviewers.findIndex((reviewer: EPerson) => reviewer.id === eperson.id); if (index !== -1) { this.selectedReviewers.splice(index, 1); } - this.selectedReviewersUpdated.emit(this.selectedReviewers.map((ePersonDtoModel: EpersonDtoModel) => ePersonDtoModel.eperson)); + this.retrieveMembers(this.config.currentPage); + this.selectedReviewersUpdated.emit(this.selectedReviewers); } /** - * Adds the {@link ePerson} to the {@link selectedReviewers} (or replaces it when {@link multipleReviewers} is + * Adds the {@link eperson} to the {@link selectedReviewers} (or replaces it when {@link multipleReviewers} is * `false`). Afterwards it will emit the list. * - * @param ePerson The {@link EPerson} to add to the list + * @param eperson The {@link EPerson} to add to the list */ - addMemberToGroup(ePerson: EpersonDtoModel) { - ePerson.memberOfGroup = true; + addMemberToGroup(eperson: EPerson) { if (!this.multipleReviewers) { - for (const selectedReviewer of this.selectedReviewers) { - selectedReviewer.memberOfGroup = false; - } this.selectedReviewers = []; } - this.selectedReviewers.push(ePerson); - this.selectedReviewersUpdated.emit(this.selectedReviewers.map((epersonDtoModel: EpersonDtoModel) => epersonDtoModel.eperson)); + 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 f1beb86b983..ab1494341e8 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 @@ -1,27 +1,31 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; import { Location } from '@angular/common'; -import { AdvancedWorkflowActionComponent } from './advanced-workflow-action.component'; import { Component } from '@angular/core'; -import { MockComponent } from 'ng-mocks'; -import { DSOSelectorComponent } from '../../../shared/dso-selector/dso-selector/dso-selector.component'; -import { ClaimedTaskDataService } from '../../../core/tasks/claimed-task-data.service'; -import { ClaimedTaskDataServiceStub } from '../../../shared/testing/claimed-task-data-service.stub'; +import { + ComponentFixture, + TestBed, +} from '@angular/core/testing'; import { ActivatedRoute } from '@angular/router'; -import { of as observableOf } from 'rxjs'; -import { WorkflowItemDataService } from '../../../core/submission/workflowitem-data.service'; import { RouterTestingModule } from '@angular/router/testing'; -import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub'; +import { TranslateModule } from '@ngx-translate/core'; +import { MockComponent } from 'ng-mocks'; +import { of as observableOf } from 'rxjs'; + +import { RequestService } from '../../../core/data/request.service'; import { WorkflowActionDataService } from '../../../core/data/workflow-action-data.service'; -import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { RouteService } from '../../../core/services/route.service'; +import { WorkflowItemDataService } from '../../../core/submission/workflowitem-data.service'; +import { ClaimedTaskDataService } from '../../../core/tasks/claimed-task-data.service'; +import { ProcessTaskResponse } from '../../../core/tasks/models/process-task-response'; +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 { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub'; +import { RequestServiceStub } from '../../../shared/testing/request-service.stub'; import { routeServiceStub } from '../../../shared/testing/route-service.stub'; -import { TranslateModule } from '@ngx-translate/core'; import { WorkflowActionDataServiceStub } from '../../../shared/testing/workflow-action-data-service.stub'; -import { ProcessTaskResponse } from '../../../core/tasks/models/process-task-response'; import { WorkflowItemDataServiceStub } from '../../../shared/testing/workflow-item-data-service.stub'; -import { RequestService } from '../../../core/data/request.service'; -import { RequestServiceStub } from '../../../shared/testing/request-service.stub'; -import { LocationStub } from '../../../shared/testing/location.stub'; +import { WorkflowItemActionPageDirective } from '../../workflow-item-action-page.component'; +import { AdvancedWorkflowActionComponent } from './advanced-workflow-action.component'; const workflowId = '1'; @@ -30,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: [ @@ -66,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(() => { @@ -117,7 +121,9 @@ describe('AdvancedWorkflowActionComponent', () => { @Component({ // eslint-disable-next-line @angular-eslint/component-selector selector: '', - template: '' + 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 73fd6dc63ed..a12a40b52ae 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 @@ -1,19 +1,26 @@ -import { Component, OnInit } from '@angular/core'; -import { WorkflowAction } from '../../../core/tasks/models/workflow-action-object.model'; -import { WorkflowActionDataService } from '../../../core/data/workflow-action-data.service'; -import { ActivatedRoute, Router } from '@angular/router'; +import { Location } from '@angular/common'; +import { + Component, + OnInit, +} from '@angular/core'; +import { + ActivatedRoute, + Router, +} from '@angular/router'; +import { TranslateService } from '@ngx-translate/core'; import { Observable } from 'rxjs'; -import { WorkflowItemActionPageComponent } from '../../workflow-item-action-page.component'; -import { WorkflowItemDataService } from '../../../core/submission/workflowitem-data.service'; +import { map } from 'rxjs/operators'; + +import { RequestService } from '../../../core/data/request.service'; +import { WorkflowActionDataService } from '../../../core/data/workflow-action-data.service'; import { RouteService } from '../../../core/services/route.service'; -import { NotificationsService } from '../../../shared/notifications/notifications.service'; -import { TranslateService } from '@ngx-translate/core'; import { getFirstSucceededRemoteDataPayload } from '../../../core/shared/operators'; +import { WorkflowItemDataService } from '../../../core/submission/workflowitem-data.service'; import { ClaimedTaskDataService } from '../../../core/tasks/claimed-task-data.service'; -import { map } from 'rxjs/operators'; import { ProcessTaskResponse } from '../../../core/tasks/models/process-task-response'; -import { RequestService } from '../../../core/data/request.service'; -import { Location } from '@angular/common'; +import { WorkflowAction } from '../../../core/tasks/models/workflow-action-object.model'; +import { NotificationsService } from '../../../shared/notifications/notifications.service'; +import { WorkflowItemActionPageDirective } from '../../workflow-item-action-page.component'; /** * Abstract component for rendering an advanced claimed task's workflow page @@ -25,7 +32,7 @@ import { Location } from '@angular/common'; 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.html b/src/app/workflowitems-edit-page/advanced-workflow-action/advanced-workflow-actions-loader/advanced-workflow-actions-loader.component.html deleted file mode 100644 index 0904d0fcde5..00000000000 --- a/src/app/workflowitems-edit-page/advanced-workflow-action/advanced-workflow-actions-loader/advanced-workflow-actions-loader.component.html +++ /dev/null @@ -1 +0,0 @@ - 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 2c12b07589f..54aae05f768 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,14 +1,29 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { AdvancedWorkflowActionsLoaderComponent } from './advanced-workflow-actions-loader.component'; -import { Router } from '@angular/router'; -import { RouterStub } from '../../../shared/testing/router.stub'; -import { ChangeDetectionStrategy, Component } from '@angular/core'; -import { AdvancedWorkflowActionsDirective } from './advanced-workflow-actions.directive'; +/* eslint-disable max-classes-per-file */ import { - rendersAdvancedWorkflowTaskOption -} from '../../../shared/mydspace-actions/claimed-task/switcher/claimed-task-actions-decorator'; + ChangeDetectionStrategy, + Component, + ComponentFactoryResolver, + Directive, + Injector, + NO_ERRORS_SCHEMA, + ViewContainerRef, +} from '@angular/core'; +import { + ComponentFixture, + TestBed, +} 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'; + import { PAGE_NOT_FOUND_PATH } from '../../../app-routing-paths'; +import { DynamicComponentLoaderDirective } from '../../../shared/abstract-component-loader/dynamic-component-loader.directive'; +import { rendersAdvancedWorkflowTaskOption } from '../../../shared/mydspace-actions/claimed-task/switcher/claimed-task-actions-decorator'; +import { RouterStub } from '../../../shared/testing/router.stub'; +import { AdvancedWorkflowActionsLoaderComponent } from './advanced-workflow-actions-loader.component'; const ADVANCED_WORKFLOW_ACTION_TEST = 'testaction'; @@ -17,22 +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: [ - AdvancedWorkflowActionsDirective, + 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(); }); @@ -50,24 +80,24 @@ describe('AdvancedWorkflowActionsLoaderComponent', () => { describe('When the component is rendered', () => { it('should display the AdvancedWorkflowActionTestComponent when the type has been defined in a rendersAdvancedWorkflowTaskOption', () => { - spyOn(component, 'getComponentByWorkflowTaskOption').and.returnValue(AdvancedWorkflowActionTestComponent); + spyOn(component, 'getComponent').and.returnValue(AdvancedWorkflowActionTestComponent); component.ngOnInit(); fixture.detectChanges(); - expect(component.getComponentByWorkflowTaskOption).toHaveBeenCalledWith(ADVANCED_WORKFLOW_ACTION_TEST); + expect(component.getComponent).toHaveBeenCalled(); expect(fixture.debugElement.query(By.css('#AdvancedWorkflowActionsLoaderComponent'))).not.toBeNull(); expect(router.navigate).not.toHaveBeenCalled(); }); it('should redirect to page not found when the type has not been defined in a rendersAdvancedWorkflowTaskOption', () => { - spyOn(component, 'getComponentByWorkflowTaskOption').and.returnValue(undefined); + spyOn(component, 'getComponent').and.returnValue(undefined); component.type = 'nonexistingaction'; component.ngOnInit(); fixture.detectChanges(); - expect(component.getComponentByWorkflowTaskOption).toHaveBeenCalledWith('nonexistingaction'); + expect(component.getComponent).toHaveBeenCalled(); expect(router.navigate).toHaveBeenCalledWith([PAGE_NOT_FOUND_PATH]); }); }); @@ -78,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 32f14c015de..799e06d8637 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 @@ -1,21 +1,26 @@ -import { Component, Input, ViewChild, ComponentFactoryResolver, OnInit } from '@angular/core'; -import { hasValue } from '../../../shared/empty.util'; import { - getAdvancedComponentByWorkflowTaskOption -} from '../../../shared/mydspace-actions/claimed-task/switcher/claimed-task-actions-decorator'; -import { AdvancedWorkflowActionsDirective } from './advanced-workflow-actions.directive'; + Component, + Input, + OnInit, +} from '@angular/core'; import { Router } from '@angular/router'; +import { AbstractComponentLoaderComponent } from 'src/app/shared/abstract-component-loader/abstract-component-loader.component'; + import { PAGE_NOT_FOUND_PATH } from '../../../app-routing-paths'; +import { GenericConstructor } from '../../../core/shared/generic-constructor'; +import { hasValue } from '../../../shared/empty.util'; +import { getAdvancedComponentByWorkflowTaskOption } from '../../../shared/mydspace-actions/claimed-task/switcher/claimed-task-actions-decorator'; +import { ThemeService } from '../../../shared/theme-support/theme.service'; /** * Component for loading a {@link AdvancedWorkflowActionComponent} depending on the "{@link type}" input */ @Component({ selector: 'ds-advanced-workflow-actions-loader', - templateUrl: './advanced-workflow-actions-loader.component.html', - styleUrls: ['./advanced-workflow-actions-loader.component.scss'], + templateUrl: '../../../shared/abstract-component-loader/abstract-component-loader.component.html', + standalone: true, }) -export class AdvancedWorkflowActionsLoaderComponent implements OnInit { +export class AdvancedWorkflowActionsLoaderComponent extends AbstractComponentLoaderComponent implements OnInit { /** * The name of the type to render @@ -23,35 +28,28 @@ export class AdvancedWorkflowActionsLoaderComponent implements OnInit { */ @Input() type: string; - /** - * Directive to determine where the dynamic child component is located - */ - @ViewChild(AdvancedWorkflowActionsDirective, { static: true }) claimedTaskActionsDirective: AdvancedWorkflowActionsDirective; + protected inputNames: (keyof this & string)[] = [ + ...this.inputNames, + 'type', + ]; constructor( - private componentFactoryResolver: ComponentFactoryResolver, + protected themeService: ThemeService, private router: Router, ) { + super(themeService); } - /** - * Fetch, create and initialize the relevant component - */ ngOnInit(): void { - const comp = this.getComponentByWorkflowTaskOption(this.type); - if (hasValue(comp)) { - const componentFactory = this.componentFactoryResolver.resolveComponentFactory(comp); - - const viewContainerRef = this.claimedTaskActionsDirective.viewContainerRef; - viewContainerRef.clear(); - viewContainerRef.createComponent(componentFactory); + if (hasValue(this.getComponent())) { + super.ngOnInit(); } else { void this.router.navigate([PAGE_NOT_FOUND_PATH]); } } - getComponentByWorkflowTaskOption(type: string): any { - return getAdvancedComponentByWorkflowTaskOption(type); + public getComponent(): GenericConstructor { + return getAdvancedComponentByWorkflowTaskOption(this.type) as GenericConstructor; } } diff --git a/src/app/workflowitems-edit-page/advanced-workflow-action/advanced-workflow-actions-loader/advanced-workflow-actions.directive.ts b/src/app/workflowitems-edit-page/advanced-workflow-action/advanced-workflow-actions-loader/advanced-workflow-actions.directive.ts deleted file mode 100644 index e569f6cc6f8..00000000000 --- a/src/app/workflowitems-edit-page/advanced-workflow-action/advanced-workflow-actions-loader/advanced-workflow-actions.directive.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Directive, ViewContainerRef } from '@angular/core'; - -@Directive({ - selector: '[dsAdvancedWorkflowActions]', -}) -/** - * Directive used as a hook to know where to inject the dynamic Advanced Claimed Task Actions component - */ -export class AdvancedWorkflowActionsDirective { - - constructor( - public viewContainerRef: ViewContainerRef, - ) { - } - -} 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 00000000000..1c29d6b8612 --- /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 1ef87ad10ff..9ee8eaed061 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 @@ -1,35 +1,36 @@ 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'; const wfi = { id: uuid, - item: createSuccessfulRemoteDataObject$({ id: itemUuid }) + item: createSuccessfulRemoteDataObject$({ id: itemUuid }), }; beforeEach(() => { wfiService = { - findById: (id: string) => createSuccessfulRemoteDataObject$(wfi) + 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) => { expect(resolved.payload.id).toEqual(itemUuid); done(); - } + }, ); }); }); 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 bacf5156569..e76a147f525 100644 --- a/src/app/workflowitems-edit-page/item-from-workflow.resolver.ts +++ b/src/app/workflowitems-edit-page/item-from-workflow.resolver.ts @@ -1,21 +1,21 @@ -import { Injectable } from '@angular/core'; -import { Resolve } from '@angular/router'; +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 { Store } from '@ngrx/store'; -import { WorkflowItemDataService } from '../core/submission/workflowitem-data.service'; 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 c4dea0f30cf..64fb97147ba 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,30 +1,54 @@ -import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing'; +import { + CommonModule, + Location, +} from '@angular/common'; +import { + Component, + NO_ERRORS_SCHEMA, +} from '@angular/core'; +import { + ComponentFixture, + TestBed, + waitForAsync, +} from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { + ActivatedRoute, + Router, +} from '@angular/router'; +import { + TranslateLoader, + TranslateModule, + TranslateService, +} from '@ngx-translate/core'; +import { + Observable, + of as observableOf, +} from 'rxjs'; -import { TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core'; -import { WorkflowItemActionPageComponent } from './workflow-item-action-page.component'; -import { NotificationsService } from '../shared/notifications/notifications.service'; +import { RequestService } from '../core/data/request.service'; import { RouteService } from '../core/services/route.service'; -import { Component, NO_ERRORS_SCHEMA } from '@angular/core'; -import { WorkflowItemDataService } from '../core/submission/workflowitem-data.service'; -import { ActivatedRoute, Router } from '@angular/router'; import { WorkflowItem } from '../core/submission/models/workflowitem.model'; -import { Observable, of as observableOf } from 'rxjs'; -import { VarDirective } from '../shared/utils/var.directive'; -import { By } from '@angular/platform-browser'; -import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils'; +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 { + createSuccessfulRemoteDataObject, + createSuccessfulRemoteDataObject$, +} from '../shared/remote-data.utils'; import { ActivatedRouteStub } from '../shared/testing/active-router.stub'; -import { RouterStub } from '../shared/testing/router.stub'; +import { LocationStub } from '../shared/testing/location.stub'; import { NotificationsServiceStub } from '../shared/testing/notifications-service.stub'; -import { RequestService } from '../core/data/request.service'; import { RequestServiceStub } from '../shared/testing/request-service.stub'; -import { Location } from '@angular/common'; -import { LocationStub } from '../shared/testing/location.stub'; +import { RouterStub } from '../shared/testing/router.stub'; +import { VarDirective } from '../shared/utils/var.directive'; +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$; @@ -32,7 +56,7 @@ describe('WorkflowItemActionPageComponent', () => { function init() { wfiService = jasmine.createSpyObj('workflowItemService', { - sendBack: observableOf(true) + sendBack: observableOf(true), }); itemRD$ = createSuccessfulRemoteDataObject$(itemRD$); wfi = new WorkflowItem(); @@ -46,10 +70,9 @@ describe('WorkflowItemActionPageComponent', () => { imports: [TranslateModule.forRoot({ loader: { provide: TranslateLoader, - useClass: TranslateLoaderMock - } - })], - declarations: [TestComponent, VarDirective], + useClass: TranslateLoaderMock, + }, + }), TestComponent, VarDirective], providers: [ { provide: ActivatedRoute, useValue: new ActivatedRouteStub({}, { wfi: createSuccessfulRemoteDataObject(wfi) }) }, { provide: Router, useClass: RouterStub }, @@ -59,7 +82,7 @@ describe('WorkflowItemActionPageComponent', () => { { provide: WorkflowItemDataService, useValue: wfiService }, { provide: RequestService, useClass: RequestServiceStub }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }) .compileComponents(); })); @@ -106,11 +129,12 @@ describe('WorkflowItemActionPageComponent', () => { }); @Component({ - selector: 'ds-workflow-item-test-action-page', - templateUrl: 'workflow-item-action-page.component.html' - } -) -class TestComponent extends WorkflowItemActionPageComponent { + selector: 'ds-workflow-item-test-action-page', + templateUrl: 'workflow-item-action-page.component.html', + 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 2ed5639c5a7..f187d26b99a 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,28 +1,51 @@ -import { Component, OnInit } from '@angular/core'; import { Location } from '@angular/common'; -import { Observable, combineLatest } from 'rxjs'; -import { map, switchMap, take } from 'rxjs/operators'; +import { + Directive, + Input, + OnInit, +} from '@angular/core'; +import { + ActivatedRoute, + Data, + Params, + Router, +} from '@angular/router'; import { TranslateService } from '@ngx-translate/core'; -import { WorkflowItem } from '../core/submission/models/workflowitem.model'; +import { + combineLatest, + Observable, +} from 'rxjs'; +import { + map, + switchMap, + take, +} from 'rxjs/operators'; + +import { RemoteData } from '../core/data/remote-data'; +import { RequestService } from '../core/data/request.service'; +import { RouteService } from '../core/services/route.service'; import { Item } from '../core/shared/item.model'; -import { ActivatedRoute, Data, Router, Params } from '@angular/router'; +import { + getAllSucceededRemoteData, + getRemoteDataPayload, +} from '../core/shared/operators'; +import { WorkflowItem } from '../core/submission/models/workflowitem.model'; import { WorkflowItemDataService } from '../core/submission/workflowitem-data.service'; -import { RouteService } from '../core/services/route.service'; -import { NotificationsService } from '../shared/notifications/notifications.service'; -import { RemoteData } from '../core/data/remote-data'; -import { getAllSucceededRemoteData, getRemoteDataPayload } from '../core/shared/operators'; import { isEmpty } from '../shared/empty.util'; -import { RequestService } from '../core/data/request.service'; +import { NotificationsService } from '../shared/notifications/notifications.service'; /** * 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; @@ -45,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; } /** @@ -54,7 +77,7 @@ export abstract class WorkflowItemActionPageComponent implements OnInit { performAction() { combineLatest([this.wfi$, this.requestService.removeByHrefSubstring('/discover')]).pipe( take(1), - switchMap(([wfi]) => this.sendRequest(wfi.id)) + switchMap(([wfi]) => this.sendRequest(wfi.id)), ).subscribe((successful: boolean) => { if (successful) { const title = this.translationService.get('workflow-item.' + this.type + '.notification.success.title'); @@ -76,18 +99,18 @@ export abstract class WorkflowItemActionPageComponent implements OnInit { previousPage() { this.routeService.getPreviousUrl().pipe(take(1)) .subscribe((url) => { - let params: Params = {}; - if (isEmpty(url)) { - url = '/mydspace'; - params = this.previousQueryParameters; - } - if (url.split('?').length > 1) { - for (const param of url.split('?')[1].split('&')) { - params[param.split('=')[0]] = decodeURIComponent(param.split('=')[1]); - } + let params: Params = {}; + if (isEmpty(url)) { + url = '/mydspace'; + params = this.previousQueryParameters; + } + if (url.split('?').length > 1) { + for (const param of url.split('?')[1].split('&')) { + params[param.split('=')[0]] = decodeURIComponent(param.split('=')[1]); } - void this.router.navigate([url.split('?')[0]], { queryParams: params }); } + void this.router.navigate([url.split('?')[0]], { queryParams: params }); + }, ); } 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 358d26b4822..39180d1c7b2 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 @@ -1,15 +1,18 @@ -import { WorkflowItemDeleteComponent } from './workflow-item-delete.component'; -import { ThemedComponent } from '../../shared/theme-support/themed.component'; import { Component } from '@angular/core'; +import { ThemedComponent } from '../../shared/theme-support/themed.component'; +import { WorkflowItemDeleteComponent } from './workflow-item-delete.component'; + /** * Themed wrapper for WorkflowItemDeleteComponent */ @Component({ - selector: 'ds-themed-workflow-item-delete', + selector: 'ds-workflow-item-delete', styleUrls: [], - templateUrl: './../../shared/theme-support/themed.component.html' + 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 89a7029b4a1..801113c4ce7 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 @@ -1,23 +1,37 @@ -import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing'; import { Location } from '@angular/common'; -import { WorkflowItemDeleteComponent } from './workflow-item-delete.component'; -import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; -import { ActivatedRoute, Router } from '@angular/router'; -import { RouteService } from '../../core/services/route.service'; -import { NotificationsService } from '../../shared/notifications/notifications.service'; -import { WorkflowItemDataService } from '../../core/submission/workflowitem-data.service'; -import { WorkflowItem } from '../../core/submission/models/workflowitem.model'; import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { VarDirective } from '../../shared/utils/var.directive'; +import { + ComponentFixture, + TestBed, + waitForAsync, +} from '@angular/core/testing'; +import { + ActivatedRoute, + Router, +} from '@angular/router'; +import { + TranslateLoader, + TranslateModule, +} from '@ngx-translate/core'; import { of as observableOf } from 'rxjs'; + import { RequestService } from '../../core/data/request.service'; -import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; +import { RouteService } from '../../core/services/route.service'; +import { WorkflowItem } from '../../core/submission/models/workflowitem.model'; +import { WorkflowItemDataService } from '../../core/submission/workflowitem-data.service'; +import { getMockRequestService } from '../../shared/mocks/request.service.mock'; import { TranslateLoaderMock } from '../../shared/mocks/translate-loader.mock'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { + createSuccessfulRemoteDataObject, + createSuccessfulRemoteDataObject$, +} from '../../shared/remote-data.utils'; import { ActivatedRouteStub } from '../../shared/testing/active-router.stub'; -import { RouterStub } from '../../shared/testing/router.stub'; -import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub'; -import { getMockRequestService } from '../../shared/mocks/request.service.mock'; import { LocationStub } from '../../shared/testing/location.stub'; +import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub'; +import { RouterStub } from '../../shared/testing/router.stub'; +import { VarDirective } from '../../shared/utils/var.directive'; +import { WorkflowItemDeleteComponent } from './workflow-item-delete.component'; describe('WorkflowItemDeleteComponent', () => { let component: WorkflowItemDeleteComponent; @@ -29,7 +43,7 @@ describe('WorkflowItemDeleteComponent', () => { function init() { wfiService = jasmine.createSpyObj('workflowItemService', { - delete: observableOf(true) + delete: observableOf(true), }); itemRD$ = createSuccessfulRemoteDataObject$(itemRD$); wfi = new WorkflowItem(); @@ -43,10 +57,9 @@ describe('WorkflowItemDeleteComponent', () => { imports: [TranslateModule.forRoot({ loader: { provide: TranslateLoader, - useClass: TranslateLoaderMock - } - })], - declarations: [WorkflowItemDeleteComponent, VarDirective], + useClass: TranslateLoaderMock, + }, + }), WorkflowItemDeleteComponent, VarDirective], providers: [ { provide: ActivatedRoute, useValue: new ActivatedRouteStub({}, { wfi: createSuccessfulRemoteDataObject(wfi) }) }, { provide: Router, useClass: RouterStub }, @@ -56,7 +69,7 @@ describe('WorkflowItemDeleteComponent', () => { { provide: WorkflowItemDataService, useValue: wfiService }, { provide: RequestService, useValue: getMockRequestService() }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }) .compileComponents(); })); 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 011398369db..0352eba098b 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,26 +1,40 @@ +import { + CommonModule, + Location, +} from '@angular/common'; import { Component } from '@angular/core'; +import { + ActivatedRoute, + Router, +} from '@angular/router'; +import { + TranslateModule, + TranslateService, +} from '@ngx-translate/core'; import { Observable } from 'rxjs'; -import { WorkflowItemActionPageComponent } from '../workflow-item-action-page.component'; -import { ActivatedRoute, Router } from '@angular/router'; -import { WorkflowItemDataService } from '../../core/submission/workflowitem-data.service'; -import { RouteService } from '../../core/services/route.service'; -import { NotificationsService } from '../../shared/notifications/notifications.service'; -import { TranslateService } from '@ngx-translate/core'; -import { RequestService } from '../../core/data/request.service'; import { map } from 'rxjs/operators'; + import { RemoteData } from '../../core/data/remote-data'; +import { RequestService } from '../../core/data/request.service'; +import { RouteService } from '../../core/services/route.service'; import { NoContent } from '../../core/shared/NoContent.model'; import { getFirstCompletedRemoteData } from '../../core/shared/operators'; -import { Location } from '@angular/common'; +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 { VarDirective } from '../../shared/utils/var.directive'; +import { WorkflowItemActionPageDirective } from '../workflow-item-action-page.component'; @Component({ - selector: 'ds-workflow-item-delete', - templateUrl: '../workflow-item-action-page.component.html' + 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, @@ -47,7 +61,7 @@ export class WorkflowItemDeleteComponent extends WorkflowItemActionPageComponent sendRequest(id: string): Observable { return this.workflowItemService.delete(id).pipe( getFirstCompletedRemoteData(), - map((response: RemoteData) => response.hasSucceeded) + map((response: RemoteData) => response.hasSucceeded), ); } } 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 8f6a1f1de09..02754776a93 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 @@ -1,29 +1,30 @@ import { first } from 'rxjs/operators'; -import { WorkflowItemPageResolver } from './workflow-item-page.resolver'; + import { WorkflowItemDataService } from '../core/submission/workflowitem-data.service'; import { createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils'; +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'; beforeEach(() => { wfiService = { - findById: (id: string) => createSuccessfulRemoteDataObject$({ id }) + 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) => { expect(resolved.payload.id).toEqual(uuid); done(); - } + }, ); }); }); 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 4bb3eac5131..d0f49d5700e 100644 --- a/src/app/workflowitems-edit-page/workflow-item-page.resolver.ts +++ b/src/app/workflowitems-edit-page/workflow-item-page.resolver.ts @@ -1,34 +1,28 @@ -import { Injectable } from '@angular/core'; -import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router'; +import { inject } from '@angular/core'; +import { + ActivatedRouteSnapshot, + ResolveFn, + RouterStateSnapshot, +} from '@angular/router'; import { Observable } from 'rxjs'; + import { RemoteData } from '../core/data/remote-data'; -import { followLink } from '../shared/utils/follow-link-config.model'; -import { WorkflowItemDataService } from '../core/submission/workflowitem-data.service'; -import { WorkflowItem } from '../core/submission/models/workflowitem.model'; import { getFirstCompletedRemoteData } from '../core/shared/operators'; +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 5a3e9ca4762..72edd8147e9 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 @@ -1,5 +1,6 @@ -import { ThemedComponent } from '../../shared/theme-support/themed.component'; import { Component } from '@angular/core'; + +import { ThemedComponent } from '../../shared/theme-support/themed.component'; import { WorkflowItemSendBackComponent } from './workflow-item-send-back.component'; /** @@ -7,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' + 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 1196e05593d..2a25bc0cc6c 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 @@ -1,23 +1,37 @@ -import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing'; import { Location } from '@angular/common'; -import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; -import { ActivatedRoute, Router } from '@angular/router'; -import { RouteService } from '../../core/services/route.service'; -import { NotificationsService } from '../../shared/notifications/notifications.service'; -import { WorkflowItemDataService } from '../../core/submission/workflowitem-data.service'; -import { WorkflowItem } from '../../core/submission/models/workflowitem.model'; import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { VarDirective } from '../../shared/utils/var.directive'; +import { + ComponentFixture, + TestBed, + waitForAsync, +} from '@angular/core/testing'; +import { + ActivatedRoute, + Router, +} from '@angular/router'; +import { + TranslateLoader, + TranslateModule, +} from '@ngx-translate/core'; import { of as observableOf } from 'rxjs'; -import { WorkflowItemSendBackComponent } from './workflow-item-send-back.component'; + import { RequestService } from '../../core/data/request.service'; -import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; -import { ActivatedRouteStub } from '../../shared/testing/active-router.stub'; -import { RouterStub } from '../../shared/testing/router.stub'; -import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub'; -import { TranslateLoaderMock } from '../../shared/mocks/translate-loader.mock'; +import { RouteService } from '../../core/services/route.service'; +import { WorkflowItem } from '../../core/submission/models/workflowitem.model'; +import { WorkflowItemDataService } from '../../core/submission/workflowitem-data.service'; import { getMockRequestService } from '../../shared/mocks/request.service.mock'; +import { TranslateLoaderMock } from '../../shared/mocks/translate-loader.mock'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { + createSuccessfulRemoteDataObject, + createSuccessfulRemoteDataObject$, +} from '../../shared/remote-data.utils'; +import { ActivatedRouteStub } from '../../shared/testing/active-router.stub'; import { LocationStub } from '../../shared/testing/location.stub'; +import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub'; +import { RouterStub } from '../../shared/testing/router.stub'; +import { VarDirective } from '../../shared/utils/var.directive'; +import { WorkflowItemSendBackComponent } from './workflow-item-send-back.component'; describe('WorkflowItemSendBackComponent', () => { let component: WorkflowItemSendBackComponent; @@ -29,7 +43,7 @@ describe('WorkflowItemSendBackComponent', () => { function init() { wfiService = jasmine.createSpyObj('workflowItemService', { - sendBack: observableOf(true) + sendBack: observableOf(true), }); itemRD$ = createSuccessfulRemoteDataObject$(itemRD$); wfi = new WorkflowItem(); @@ -43,10 +57,9 @@ describe('WorkflowItemSendBackComponent', () => { imports: [TranslateModule.forRoot({ loader: { provide: TranslateLoader, - useClass: TranslateLoaderMock - } - })], - declarations: [WorkflowItemSendBackComponent, VarDirective], + useClass: TranslateLoaderMock, + }, + }), WorkflowItemSendBackComponent, VarDirective], providers: [ { provide: ActivatedRoute, useValue: new ActivatedRouteStub({}, { wfi: createSuccessfulRemoteDataObject(wfi) }) }, { provide: Router, useClass: RouterStub }, @@ -56,7 +69,7 @@ describe('WorkflowItemSendBackComponent', () => { { provide: WorkflowItemDataService, useValue: wfiService }, { provide: RequestService, useValue: getMockRequestService() }, ], - schemas: [NO_ERRORS_SCHEMA] + schemas: [NO_ERRORS_SCHEMA], }) .compileComponents(); })); 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 a3c03bcfb1f..ad0b1d91a83 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,22 +1,36 @@ +import { + CommonModule, + Location, +} from '@angular/common'; import { Component } from '@angular/core'; -import { WorkflowItemActionPageComponent } from '../workflow-item-action-page.component'; +import { + ActivatedRoute, + Router, +} from '@angular/router'; +import { + TranslateModule, + TranslateService, +} from '@ngx-translate/core'; import { Observable } from 'rxjs'; -import { ActivatedRoute, Router } from '@angular/router'; -import { WorkflowItemDataService } from '../../core/submission/workflowitem-data.service'; + +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 { TranslateService } from '@ngx-translate/core'; -import { RequestService } from '../../core/data/request.service'; -import { Location } from '@angular/common'; +import { VarDirective } from '../../shared/utils/var.directive'; +import { WorkflowItemActionPageDirective } from '../workflow-item-action-page.component'; @Component({ - selector: 'ds-workflow-item-send-back', - templateUrl: '../workflow-item-action-page.component.html' + 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 00000000000..4bc074c4256 --- /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-paths.ts b/src/app/workflowitems-edit-page/workflowitems-edit-page-routing-paths.ts index 326eebe4a79..1b21174c604 100644 --- a/src/app/workflowitems-edit-page/workflowitems-edit-page-routing-paths.ts +++ b/src/app/workflowitems-edit-page/workflowitems-edit-page-routing-paths.ts @@ -1,5 +1,8 @@ +import { + getWorkflowItemModuleRoute, + getWorkspaceItemModuleRoute, +} from '../app-routing-paths'; import { URLCombiner } from '../core/url-combiner/url-combiner'; -import { getWorkflowItemModuleRoute, getWorkspaceItemModuleRoute } from '../app-routing-paths'; export function getWorkflowItemPageRoute(wfiId: string) { return new URLCombiner(getWorkflowItemModuleRoute(), wfiId).toString(); @@ -28,9 +31,14 @@ export function getWorkspaceItemDeleteRoute(wsiId: string) { return new URLCombiner(getWorkspaceItemModuleRoute(), wsiId, WORKSPACE_ITEM_DELETE_PATH).toString(); } +export function getWorkspaceItemEditRoute(wsiId: string) { + return new URLCombiner(getWorkspaceItemModuleRoute(), wsiId, WORKSPACE_ITEM_EDIT_PATH).toString(); +} + export const WORKFLOW_ITEM_EDIT_PATH = 'edit'; export const WORKFLOW_ITEM_DELETE_PATH = 'delete'; export const WORKFLOW_ITEM_VIEW_PATH = 'view'; export const WORKFLOW_ITEM_SEND_BACK_PATH = 'sendback'; export const ADVANCED_WORKFLOW_PATH = 'advanced'; export const WORKSPACE_ITEM_DELETE_PATH = 'delete'; +export const WORKSPACE_ITEM_EDIT_PATH = '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 b093f205637..00000000000 --- a/src/app/workflowitems-edit-page/workflowitems-edit-page-routing.module.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { NgModule } from '@angular/core'; -import { RouterModule } from '@angular/router'; - -import { AuthenticatedGuard } from '../core/auth/authenticated.guard'; -import { WorkflowItemPageResolver } from './workflow-item-page.resolver'; -import { - WORKFLOW_ITEM_DELETE_PATH, - WORKFLOW_ITEM_EDIT_PATH, - WORKFLOW_ITEM_SEND_BACK_PATH, - WORKFLOW_ITEM_VIEW_PATH, - ADVANCED_WORKFLOW_PATH, -} from './workflowitems-edit-page-routing-paths'; -import { ThemedSubmissionEditComponent } from '../submission/edit/themed-submission-edit.component'; -import { ThemedWorkflowItemDeleteComponent } from './workflow-item-delete/themed-workflow-item-delete.component'; -import { ThemedWorkflowItemSendBackComponent } from './workflow-item-send-back/themed-workflow-item-send-back.component'; -import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver'; -import { ItemFromWorkflowResolver } from './item-from-workflow.resolver'; -import { ThemedFullItemPageComponent } from '../item-page/full/themed-full-item-page.component'; -import { - AdvancedWorkflowActionPageComponent -} from './advanced-workflow-action/advanced-workflow-action-page/advanced-workflow-action-page.component'; - -@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 cf998c52743..00000000000 --- a/src/app/workflowitems-edit-page/workflowitems-edit-page.module.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { CommonModule } from '@angular/common'; -import { NgModule } from '@angular/core'; -import { SharedModule } from '../shared/shared.module'; -import { WorkflowItemsEditPageRoutingModule } from './workflowitems-edit-page-routing.module'; -import { SubmissionModule } from '../submission/submission.module'; -import { WorkflowItemDeleteComponent } from './workflow-item-delete/workflow-item-delete.component'; -import { WorkflowItemSendBackComponent } from './workflow-item-send-back/workflow-item-send-back.component'; -import { ThemedWorkflowItemDeleteComponent } from './workflow-item-delete/themed-workflow-item-delete.component'; -import { - ThemedWorkflowItemSendBackComponent -} from './workflow-item-send-back/themed-workflow-item-send-back.component'; -import { StatisticsModule } from '../statistics/statistics.module'; -import { ItemPageModule } from '../item-page/item-page.module'; -import { - AdvancedWorkflowActionsLoaderComponent -} from './advanced-workflow-action/advanced-workflow-actions-loader/advanced-workflow-actions-loader.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 { - AdvancedWorkflowActionPageComponent -} from './advanced-workflow-action/advanced-workflow-action-page/advanced-workflow-action-page.component'; -import { - AdvancedWorkflowActionsDirective -} from './advanced-workflow-action/advanced-workflow-actions-loader/advanced-workflow-actions.directive'; -import { AccessControlModule } from '../access-control/access-control.module'; -import { - ReviewersListComponent -} from './advanced-workflow-action/advanced-workflow-action-select-reviewer/reviewers-list/reviewers-list.component'; -import { FormModule } from '../shared/form/form.module'; -import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; - -@NgModule({ - imports: [ - WorkflowItemsEditPageRoutingModule, - CommonModule, - SharedModule, - SubmissionModule, - StatisticsModule, - ItemPageModule, - AccessControlModule, - FormModule, - NgbModule, - ], - declarations: [ - WorkflowItemDeleteComponent, - ThemedWorkflowItemDeleteComponent, - WorkflowItemSendBackComponent, - ThemedWorkflowItemSendBackComponent, - AdvancedWorkflowActionsLoaderComponent, - AdvancedWorkflowActionRatingComponent, - AdvancedWorkflowActionSelectReviewerComponent, - AdvancedWorkflowActionPageComponent, - AdvancedWorkflowActionsDirective, - 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 00000000000..912d578b454 --- /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 c14344d70da..0dc9ad343df 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 @@ -1,35 +1,36 @@ 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'; const wfi = { id: uuid, - item: createSuccessfulRemoteDataObject$({ id: itemUuid }) + item: createSuccessfulRemoteDataObject$({ id: itemUuid }), }; beforeEach(() => { wfiService = { - findById: (id: string) => createSuccessfulRemoteDataObject$(wfi) + 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) => { expect(resolved.payload.id).toEqual(itemUuid); done(); - } + }, ); }); }); 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 60e1fe6a87a..6e43fc7bea2 100644 --- a/src/app/workspaceitems-edit-page/item-from-workspace.resolver.ts +++ b/src/app/workspaceitems-edit-page/item-from-workspace.resolver.ts @@ -1,21 +1,23 @@ -import { Injectable } from '@angular/core'; -import { Resolve } from '@angular/router'; +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 { Store } from '@ngrx/store'; import { SubmissionObjectResolver } from '../core/submission/resolver/submission-object.resolver'; 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 bbd3360db48..4bf44de9265 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 @@ -1,29 +1,30 @@ import { first } from 'rxjs/operators'; -import { WorkspaceItemPageResolver } from './workspace-item-page.resolver'; + import { WorkspaceitemDataService } from '../core/submission/workspaceitem-data.service'; import { createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils'; +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'; beforeEach(() => { wsiService = { - findById: (id: string) => createSuccessfulRemoteDataObject$({ id }) + 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) => { expect(resolved.payload.id).toEqual(uuid); done(); - } + }, ); }); }); 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 1b1aa25492e..1d3b8e946d2 100644 --- a/src/app/workspaceitems-edit-page/workspace-item-page.resolver.ts +++ b/src/app/workspaceitems-edit-page/workspace-item-page.resolver.ts @@ -1,34 +1,35 @@ -import { Injectable } from '@angular/core'; -import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router'; +import { inject } from '@angular/core'; +import { + ActivatedRouteSnapshot, + ResolveFn, + RouterStateSnapshot, +} from '@angular/router'; import { Observable } from 'rxjs'; + import { RemoteData } from '../core/data/remote-data'; -import { followLink } from '../shared/utils/follow-link-config.model'; -import { WorkspaceitemDataService } from '../core/submission/workspaceitem-data.service'; -import { WorkflowItem } from '../core/submission/models/workflowitem.model'; import { getFirstCompletedRemoteData } from '../core/shared/operators'; +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 681cba21c86..b7e1858b79a 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 @@ -1,15 +1,17 @@ -import { ThemedComponent } from '../../shared/theme-support/themed.component'; import { Component } from '@angular/core'; + +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' + templateUrl: './../../shared/theme-support/themed.component.html', + standalone: true, + imports: [WorkspaceItemsDeletePageComponent], }) export class ThemedWorkspaceItemsDeletePageComponent extends ThemedComponent { protected getComponentName(): string { @@ -17,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 a0f0a1711ec..75c265b4200 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.scss b/src/themes/dspace/app/header/header.component.scss index 2fc857826f9..5aae8af0171 100644 --- a/src/themes/dspace/app/header/header.component.scss +++ b/src/themes/dspace/app/header/header.component.scss @@ -1,26 +1,28 @@ -@media screen and (min-width: map-get($grid-breakpoints, md)) { - nav.navbar { - display: none; - } - .header { +:host { + #main-site-header { + min-height: var(--ds-header-height); + + @include media-breakpoint-up(md) { + height: var(--ds-header-height); + } + background-color: var(--ds-header-bg); + + &-container { + min-height: var(--ds-header-height); + } } -} -.navbar-brand img { - @media screen and (max-width: map-get($grid-breakpoints, md)) { - height: var(--ds-header-logo-height-xs); + img#header-logo { + height: var(--ds-header-logo-height); } -} -.navbar-toggler .navbar-toggler-icon { - background-image: none !important; - line-height: 1.5; -} -.navbar-toggler { - color: var(--ds-header-icon-color); + button#navbar-toggler { + color: var(--ds-header-icon-color); - &:hover, &:focus { - color: var(--ds-header-icon-color-hover); + &:hover, &:focus { + color: var(--ds-header-icon-color-hover); + } } + } diff --git a/src/themes/dspace/app/header/header.component.ts b/src/themes/dspace/app/header/header.component.ts index 6da89b47d57..19318389231 100644 --- a/src/themes/dspace/app/header/header.component.ts +++ b/src/themes/dspace/app/header/header.component.ts @@ -1,13 +1,39 @@ -import { Component } from '@angular/core'; +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 { +export class HeaderComponent extends BaseComponent implements OnInit { + public isNavBarCollapsed$: Observable; + + ngOnInit() { + super.ngOnInit(); + this.isNavBarCollapsed$ = this.menuService.isMenuCollapsed(this.menuID); + } } 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 ef576ed99cd..ca053fc6623 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 @@ -1,9 +1,9 @@ -
+
-

DSpace 7

+

DSpace 9

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

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

DSpace 7

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 @@

DSpace 7

- 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 93ec1763f34..3c3aa8b4450 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 @@ -1,6 +1,5 @@ :host { display: block; - margin-top: calc(var(--ds-content-spacing) * -1); div.background-image-container { color: white; 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 d4032011dc3..cebea38ee89 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 @@ -1,10 +1,12 @@ 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' + 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 05afac7d7e9..d828206e7ae 100644 --- a/src/themes/dspace/app/navbar/navbar.component.html +++ b/src/themes/dspace/app/navbar/navbar.component.html @@ -1,24 +1,9 @@ - + + + + diff --git a/src/themes/dspace/app/navbar/navbar.component.scss b/src/themes/dspace/app/navbar/navbar.component.scss index d3aea9f0780..32c65c8c978 100644 --- a/src/themes/dspace/app/navbar/navbar.component.scss +++ b/src/themes/dspace/app/navbar/navbar.component.scss @@ -1,56 +1,3 @@ -nav.navbar { - border-top: 1px var(--ds-header-navbar-border-top-color) solid; - border-bottom: 5px var(--ds-header-navbar-border-bottom-color) solid; - align-items: baseline; -} - -/** Mobile menu styling **/ -@media screen and (max-width: map-get($grid-breakpoints, md)-0.02) { - .navbar { - width: 100%; - background-color: var(--bs-white); - position: absolute; - overflow: hidden; - height: 0; - &.open { - height: 100vh; //doesn't matter because wrapper is sticky - } - } -} - -@media screen and (min-width: map-get($grid-breakpoints, md)) { - .reset-padding-md { - margin-left: calc(var(--bs-spacer) / -2); - margin-right: calc(var(--bs-spacer) / -2); - } -} - -/* TODO remove when https://github.com/twbs/bootstrap/issues/24726 is fixed */ -.navbar-expand-md.navbar-container { - @media screen and (max-width: map-get($grid-breakpoints, md)-0.02) { - > .navbar-inner-container { - padding: 0 var(--bs-spacer); - a.navbar-brand { - display: none; - } - .navbar-collapsed { - display: none; - } - } - padding: 0; - } - height: 80px; -} - -a.navbar-brand img { - max-height: var(--ds-header-logo-height); -} +:host { -.navbar-nav { - ::ng-deep a.nav-link { - color: var(--ds-navbar-link-color); - } - ::ng-deep a.nav-link:hover { - color: var(--ds-navbar-link-color-hover); - } } diff --git a/src/themes/dspace/app/navbar/navbar.component.ts b/src/themes/dspace/app/navbar/navbar.component.ts index 321351a933c..5713a161e18 100644 --- a/src/themes/dspace/app/navbar/navbar.component.ts +++ b/src/themes/dspace/app/navbar/navbar.component.ts @@ -1,15 +1,34 @@ +import { + AsyncPipe, + NgClass, + NgComponentOutlet, + NgFor, + NgIf, +} from '@angular/common'; import { Component } from '@angular/core'; +import { RouterLink } from '@angular/router'; +import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'; +import { TranslateModule } from '@ngx-translate/core'; +import { ThemedSearchNavbarComponent } from 'src/app/search-navbar/themed-search-navbar.component'; +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 { NavbarComponent as BaseComponent } from '../../../../app/navbar/navbar.component'; import { slideMobileNav } from '../../../../app/shared/animations/slide'; +import { ThemedAuthNavMenuComponent } from '../../../../app/shared/auth-nav-menu/themed-auth-nav-menu.component'; +import { ThemedUserMenuComponent } from '../../../../app/shared/auth-nav-menu/user-menu/themed-user-menu.component'; +import { ImpersonateNavbarComponent } from '../../../../app/shared/impersonate-navbar/impersonate-navbar.component'; /** * Component representing the public navbar */ @Component({ - selector: 'ds-navbar', + selector: 'ds-themed-navbar', styleUrls: ['./navbar.component.scss'], templateUrl: './navbar.component.html', - animations: [slideMobileNav] + animations: [slideMobileNav], + standalone: true, + imports: [NgbDropdownModule, ThemedLangSwitchComponent, ThemedSearchNavbarComponent, NgClass, RouterLink, NgIf, NgFor, NgComponentOutlet, ContextHelpToggleComponent, ThemedAuthNavMenuComponent, ImpersonateNavbarComponent, AsyncPipe, TranslateModule, ThemedUserMenuComponent], }) export class NavbarComponent extends BaseComponent { } diff --git a/src/themes/dspace/eager-theme.module.ts b/src/themes/dspace/eager-theme.module.ts index 63dff9da317..233b08c62ce 100644 --- a/src/themes/dspace/eager-theme.module.ts +++ b/src/themes/dspace/eager-theme.module.ts @@ -1,14 +1,11 @@ -import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; -import { SharedModule } from '../../app/shared/shared.module'; -import { HomeNewsComponent } from './app/home-page/home-news/home-news.component'; -import { NavbarComponent } from './app/navbar/navbar.component'; +import { NgModule } from '@angular/core'; + +import { RootModule } from '../../app/root.module'; import { HeaderComponent } from './app/header/header.component'; import { HeaderNavbarWrapperComponent } from './app/header-nav-wrapper/header-navbar-wrapper.component'; -import { RootModule } from '../../app/root.module'; -import { NavbarModule } from '../../app/navbar/navbar.module'; -import { SharedBrowseByModule } from '../../app/shared/browse-by/shared-browse-by.module'; -import { ResultsBackButtonModule } from '../../app/shared/results-back-button/results-back-button.module'; +import { HomeNewsComponent } from './app/home-page/home-news/home-news.component'; +import { NavbarComponent } from './app/navbar/navbar.component'; /** * Add components that use a custom decorator to ENTRY_COMPONENTS as well as DECLARATIONS. @@ -27,15 +24,11 @@ const DECLARATIONS = [ @NgModule({ imports: [ CommonModule, - SharedModule, - SharedBrowseByModule, - ResultsBackButtonModule, RootModule, - NavbarModule, + ...DECLARATIONS, ], - declarations: DECLARATIONS, providers: [ - ...ENTRY_COMPONENTS.map((component) => ({provide: component})) + ...ENTRY_COMPONENTS.map((component) => ({ provide: component })), ], }) /** diff --git a/src/themes/dspace/lazy-theme.module.ts b/src/themes/dspace/lazy-theme.module.ts index cdaae815ada..9d26b195362 100644 --- a/src/themes/dspace/lazy-theme.module.ts +++ b/src/themes/dspace/lazy-theme.module.ts @@ -1,122 +1,34 @@ -import { NgModule } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { AdminRegistriesModule } from '../../app/admin/admin-registries/admin-registries.module'; -import { AdminSearchModule } from '../../app/admin/admin-search-page/admin-search.module'; -import { - AdminWorkflowModuleModule -} from '../../app/admin/admin-workflow-page/admin-workflow.module'; -import { - BitstreamFormatsModule -} from '../../app/admin/admin-registries/bitstream-formats/bitstream-formats.module'; -import { BrowseByModule } from '../../app/browse-by/browse-by.module'; -import { - CollectionFormModule -} from '../../app/collection-page/collection-form/collection-form.module'; -import { CommunityFormModule } from '../../app/community-page/community-form/community-form.module'; -import { CoreModule } from '../../app/core/core.module'; import { DragDropModule } from '@angular/cdk/drag-drop'; -import { EditItemPageModule } from '../../app/item-page/edit-item-page/edit-item-page.module'; -import { FormsModule } from '@angular/forms'; +import { CommonModule } from '@angular/common'; import { HttpClientModule } from '@angular/common/http'; -import { IdlePreloadModule } from 'angular-idle-preload'; -import { - JournalEntitiesModule -} from '../../app/entity-groups/journal-entities/journal-entities.module'; -import { MyDspaceSearchModule } from '../../app/my-dspace-page/my-dspace-search.module'; -import { MenuModule } from '../../app/shared/menu/menu.module'; -import { NavbarModule } from '../../app/navbar/navbar.module'; +import { NgModule } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { RouterModule } from '@angular/router'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; -import { ProfilePageModule } from '../../app/profile-page/profile-page.module'; -import { RegisterEmailFormModule } from '../../app/register-email-form/register-email-form.module'; -import { - ResearchEntitiesModule -} from '../../app/entity-groups/research-entities/research-entities.module'; -import { ScrollToModule } from '@nicky-lenaers/ngx-scroll-to'; -import { SearchPageModule } from '../../app/search-page/search-page.module'; -import { SharedModule } from '../../app/shared/shared.module'; -import { StatisticsModule } from '../../app/statistics/statistics.module'; -import { StoreModule } from '@ngrx/store'; import { StoreRouterConnectingModule } from '@ngrx/router-store'; +import { StoreModule } from '@ngrx/store'; import { TranslateModule } from '@ngx-translate/core'; -import { HomePageModule } from '../../app/home-page/home-page.module'; -import { AppModule } from '../../app/app.module'; -import { ItemPageModule } from '../../app/item-page/item-page.module'; -import { RouterModule } from '@angular/router'; -import { CommunityListPageModule } from '../../app/community-list-page/community-list-page.module'; -import { InfoModule } from '../../app/info/info.module'; -import { StatisticsPageModule } from '../../app/statistics-page/statistics-page.module'; -import { CommunityPageModule } from '../../app/community-page/community-page.module'; -import { CollectionPageModule } from '../../app/collection-page/collection-page.module'; -import { SubmissionModule } from '../../app/submission/submission.module'; -import { MyDSpacePageModule } from '../../app/my-dspace-page/my-dspace-page.module'; -import { SearchModule } from '../../app/shared/search/search.module'; -import { - ResourcePoliciesModule -} from '../../app/shared/resource-policies/resource-policies.module'; -import { ComcolModule } from '../../app/shared/comcol/comcol.module'; +import { ScrollToModule } from '@nicky-lenaers/ngx-scroll-to'; + import { RootModule } from '../../app/root.module'; -import { BrowseByPageModule } from '../../app/browse-by/browse-by-page.module'; -import { ResultsBackButtonModule } from '../../app/shared/results-back-button/results-back-button.module'; -import { SharedBrowseByModule } from '../../app/shared/browse-by/shared-browse-by.module'; -import { ItemVersionsModule } from '../../app/item-page/versions/item-versions.module'; -import { ItemSharedModule } from 'src/app/item-page/item-shared.module'; const DECLARATIONS = [ ]; @NgModule({ imports: [ - AdminRegistriesModule, - AdminSearchModule, - AdminWorkflowModuleModule, - AppModule, RootModule, - BitstreamFormatsModule, - BrowseByModule, - BrowseByPageModule, - ResultsBackButtonModule, - CollectionFormModule, - CollectionPageModule, CommonModule, - CommunityFormModule, - CommunityListPageModule, - CommunityPageModule, - CoreModule, DragDropModule, - ItemSharedModule, - ItemPageModule, - EditItemPageModule, - ItemVersionsModule, FormsModule, - HomePageModule, HttpClientModule, - IdlePreloadModule, - InfoModule, - JournalEntitiesModule, - MenuModule, - MyDspaceSearchModule, - NavbarModule, NgbModule, - ProfilePageModule, - RegisterEmailFormModule, - ResearchEntitiesModule, RouterModule, ScrollToModule, - SearchPageModule, - SharedModule, - SharedBrowseByModule, - StatisticsModule, - StatisticsPageModule, StoreModule, StoreRouterConnectingModule, TranslateModule, - SubmissionModule, - MyDSpacePageModule, - MyDspaceSearchModule, - SearchModule, FormsModule, - ResourcePoliciesModule, - ComcolModule, ], declarations: DECLARATIONS, }) diff --git a/src/themes/dspace/styles/_global-styles.scss b/src/themes/dspace/styles/_global-styles.scss index e41dae0e3f2..d2258193269 100644 --- a/src/themes/dspace/styles/_global-styles.scss +++ b/src/themes/dspace/styles/_global-styles.scss @@ -3,7 +3,7 @@ // imports the base global style @import '../../../styles/_global-styles.scss'; -.facet-filter, .setting-option { +.facet-filter, .setting-option, .advanced-search { background-color: var(--bs-light); border-radius: var(--bs-border-radius); @@ -17,7 +17,7 @@ background-color: var(--bs-primary); } - h5 { + h4, .h4 { font-size: 1.1rem } } diff --git a/src/themes/dspace/styles/_theme_css_variable_overrides.scss b/src/themes/dspace/styles/_theme_css_variable_overrides.scss index 516eff9f7e9..63a5535d958 100644 --- a/src/themes/dspace/styles/_theme_css_variable_overrides.scss +++ b/src/themes/dspace/styles/_theme_css_variable_overrides.scss @@ -1,11 +1,28 @@ // Override or add CSS variables for your theme here :root { - --ds-header-logo-height: 40px; + + @include media-breakpoint-up(md) { + --ds-header-logo-height: 40px; + --ds-header-height: 80px; + } + @include media-breakpoint-down(sm) { + --ds-header-logo-height: 50px; + --ds-header-height: 90px; + } + --ds-banner-text-background: rgba(0, 0, 0, 0.45); --ds-banner-background-gradient-width: 300px; - --ds-home-news-link-color: #{$green}; - --ds-home-news-link-hover-color: #{darken($green, 15%)}; - --ds-header-navbar-border-bottom-color: #{$green}; + + --ds-header-navbar-border-bottom-height: 5px; + + /* set the next two properties as `--ds-header-navbar-border-bottom-*` + in order to keep the bottom border of the header when navbar is expanded */ + --ds-expandable-navbar-border-top-color: #{$white}; + --ds-expandable-navbar-border-top-height: 0; + --ds-expandable-navbar-padding-top: 0; + + --ds-item-page-img-field-default-inline-height: 24px; + --ds-item-page-img-field-ror-inline-height: var(--ds-item-page-img-field-default-inline-height); } diff --git a/src/themes/dspace/styles/_theme_sass_variable_overrides.scss b/src/themes/dspace/styles/_theme_sass_variable_overrides.scss index b5799c97496..a9231d37e52 100644 --- a/src/themes/dspace/styles/_theme_sass_variable_overrides.scss +++ b/src/themes/dspace/styles/_theme_sass_variable_overrides.scss @@ -1,41 +1,90 @@ // DSpace works with CSS variables for its own components, and has a mapping of all bootstrap Sass // variables to CSS equivalents (see src/styles/_bootstrap_variables_mapping.scss). However Bootstrap -// still uses Sass variables internally. So if you want to override bootstrap (or other sass -// variables) you can do so here. Their CSS counterparts will include the changes you make here +// still uses Sass variables internally. So if you want to override Bootstrap (or other sass +// variables) you can do so here. Their CSS counterparts will include the changes you make here. + +// When this file is going to be compiled, internal Bootstrap variables won't have been declared yet, +// therefore if you want to use any Bootstrap variable you also need to declare it here. + +// All SASS variables from the base theme are also included here. Do not use the '!default' flag +// here if you want to override them. + + +/*** FONT FAMILIES ***/ @import url('https://fonts.googleapis.com/css2?family=Nunito:ital,wght@0,200;0,300;0,400;0,600;0,700;0,800;1,200;1,300;1,400;1,600;1,700;1,800&display=swap'); $font-family-sans-serif: 'Nunito', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; -$navbar-dark-color: #FFFFFF; -/* Reassign color vars to semantic color scheme */ -$blue: #2b4e72 !default; -$green: #92C642 !default; -$cyan: #207698 !default; -$yellow: #ec9433 !default; -$red: #CF4444 !default; -$dark: #43515f !default; +/*** SEMANTIC COLOR SCHEME ***/ -$gray-800: #343a40 !default; -$gray-700: #495057 !default; -$gray-400: #ced4da !default; -$gray-100: #f8f9fa !default; +// Gray scale (uncomment the variables that you want to override or that you need to use in this file) +//$white: #fff; +//$gray-100: #f8f9fa; +//$gray-200: #e9ecef; +//$gray-300: #dee2e6; +//$gray-400: #ced4da; +//$gray-500: #adb5bd; +//$gray-600: #6c757d; +//$gray-700: #495057; +//$gray-800: #343a40; +//$gray-900: #212529; +//$black: #000; -$body-color: $gray-800 !default; // Bootstrap $gray-800 +// Other colors (uncomment the variables that you want to override or that you need to use in this file) +//$blue: #007bff !default; +//$indigo: #6610f2 !default; +//$purple: #6f42c1 !default; +//$pink: #e83e8c !default; +//$red: #dc3545 !default; +//$orange: #fd7e14 !default; +//$yellow: #ffc107 !default; +//$green: #28a745 !default; +//$teal: #20c997 !default; +//$cyan: #17a2b8 !default; -$table-accent-bg: $gray-100 !default; // Bootstrap $gray-100 -$table-hover-bg: $gray-400 !default; // Bootstrap $gray-400 +// Define or override other colors here +// ... -$yiq-contrasted-threshold: 170 !default; +// Override semantic colors here +$primary: #43515f; // Gray +$secondary: #495057; // As Bootstrap $gray-700 +$success: #92c642; // Lime +$info: #1e6f90; // Light blue +$warning: #ec9433; // Orange +$danger: #cf4444; // Red +$light: #f8f9fa; // As Bootstrap $gray-100 +$dark: #43515f; // Gray +// Add new semantic colors here (you don't need to add existing semantic colors) $theme-colors: ( - primary: $dark, - secondary: $gray-700, - success: $green, - info: $cyan, - warning: $yellow, - danger: $red, - light: $gray-100, - dark: $dark -) !default; + // ... +); + + +/*** OTHER BOOTSTRAP VARIABLES ***/ + +// The yiq lightness value that determines when the lightness of color changes from "dark" to "light". Acceptable values are between 0 and 255. +$yiq-contrasted-threshold: 170; + +$body-color: #343a40; // As Bootstrap $gray-800 + +$link-color: #1e6f90; // Blue green, as DSpace $info +$link-decoration: none; +$link-hover-color: darken($link-color, 15%); +$link-hover-decoration: underline; + +$table-accent-bg: #f8f9fa; // As Bootstrap $gray-100 +$table-hover-bg: #ced4da; // As Bootstrap $gray-400 + +$navbar-dark-color: #fff; + + +/*** CUSTOM DSPACE VARIABLES ***/ + +$ds-home-news-link-color: #92c642; +$ds-header-navbar-border-bottom-color: #92c642; + +$ds-breadcrumb-link-color: #154E66 !default; +$ds-breadcrumb-link-active-color: #040D11 !default; diff --git a/src/themes/eager-themes.module.ts b/src/themes/eager-themes.module.ts index 4a46595f358..c4db7b582a0 100644 --- a/src/themes/eager-themes.module.ts +++ b/src/themes/eager-themes.module.ts @@ -1,4 +1,5 @@ import { NgModule } from '@angular/core'; + import { EagerThemeModule as DSpaceEagerThemeModule } from './dspace/eager-theme.module'; // import { EagerThemeModule as CustomEagerThemeModule } from './custom/eager-theme.module'; diff --git a/src/typings.d.ts b/src/typings.d.ts index c1c86511f88..9615cf5f67b 100644 --- a/src/typings.d.ts +++ b/src/typings.d.ts @@ -9,7 +9,7 @@ declare module "my-module" { export function doesSomething(value: string): string; } * - * If you're prototying and you will fix the types later you can also declare it as type any + * If you're prototyping and you will fix the types later you can also declare it as type any * declare var assert: any; * diff --git a/tsconfig.json b/tsconfig.json index 1eed699114e..afd00f8568c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -40,12 +40,19 @@ "node", "jasmine" ], - "useDefineForClassFields": false + "useDefineForClassFields": false, + "useUnknownInCatchVariables": true }, "angularCompilerOptions": { "fullTemplateTypeCheck": true, "strictInjectionParameters": true, - "strictInputAccessModifiers": true + "strictInputAccessModifiers": true, + "extendedDiagnostics": { + // The categories to use for specific diagnostics. + "checks": { + "missingControlFlowDirective": "error" + }, + } }, "exclude": [ "cypress.config.ts" diff --git a/webpack/helpers.ts b/webpack/helpers.ts index 43855f6c729..f0b42a8a690 100644 --- a/webpack/helpers.ts +++ b/webpack/helpers.ts @@ -1,18 +1,49 @@ -const path = require('path'); +import { readFileSync, readdirSync, statSync, Stats } from 'fs'; +import { join, resolve } from 'path'; + +const md5 = require('md5'); export const projectRoot = (relativePath) => { - return path.resolve(__dirname, '..', relativePath); + return resolve(__dirname, '..', relativePath); }; export const globalCSSImports = () => { return [ - projectRoot(path.join('src', 'styles', '_variables.scss')), - projectRoot(path.join('src', 'styles', '_mixins.scss')), + projectRoot(join('src', 'styles', '_variables.scss')), + projectRoot(join('src', 'styles', '_mixins.scss')), ]; }; +/** + * Calculates the md5 hash of a file + * + * @param filePath The path of the file + */ +export function calculateFileHash(filePath: string): string { + const fileContent: Buffer = readFileSync(filePath); + return md5(fileContent); +} -module.exports = { - projectRoot, - globalCSSImports -}; +/** + * Calculate the hashes of all the files (matching the given regex) in a certain folder + * + * @param folderPath The path of the folder + * @param regExp A regex of the files in the folder for which a hash needs to be generated + */ +export function getFileHashes(folderPath: string, regExp: RegExp): { [fileName: string]: string } { + const files: string[] = readdirSync(folderPath); + let hashes: { [fileName: string]: string } = {}; + + for (const file of files) { + if (file.match(regExp)) { + const filePath: string = join(folderPath, file); + const stats: Stats = statSync(filePath); + + if (stats.isFile()) { + hashes[file] = calculateFileHash(filePath); + } + } + } + + return hashes; +} diff --git a/webpack/webpack.browser.ts b/webpack/webpack.browser.ts index 5a3b4910ae7..168185ef242 100644 --- a/webpack/webpack.browser.ts +++ b/webpack/webpack.browser.ts @@ -35,5 +35,12 @@ module.exports = Object.assign({}, commonExports, { buildAppConfig(join(process.cwd(), 'src/assets/config.json')); return middlewares; } - } + }, + watchOptions: { + // Ignore directories that should not be watched for recompiling angular + ignored: [ + '**/node_modules', '**/_build', '**/.git', '**/docker', + '**/.angular', '**/.idea', '**/.vscode', '**/.history', '**/.vsix' + ] + }, }); diff --git a/webpack/webpack.common.ts b/webpack/webpack.common.ts index 1a1ecfd6efe..8d433edf393 100644 --- a/webpack/webpack.common.ts +++ b/webpack/webpack.common.ts @@ -1,4 +1,5 @@ -import { globalCSSImports, projectRoot } from './helpers'; +import { globalCSSImports, projectRoot, getFileHashes, calculateFileHash } from './helpers'; +import { EnvironmentPlugin } from 'webpack'; const CopyWebpackPlugin = require('copy-webpack-plugin'); const path = require('path'); @@ -18,12 +19,13 @@ export const copyWebpackOptions = { // use [\/|\\] to match both POSIX and Windows separators const matches = absoluteFilename.match(/.*[\/|\\]assets[\/|\\](.+)\.json5$/); if (matches) { + const fileHash: string = process.env.NODE_ENV === 'production' ? `.${calculateFileHash(absoluteFilename)}` : ''; // matches[1] is the relative path from src/assets to the JSON5 file, without the extension - return path.join('assets', matches[1] + '.json'); + return path.join('assets', `${matches[1]}${fileHash}.json`); } }, transform(content) { - return JSON.stringify(JSON5.parse(content.toString())) + return JSON.stringify(JSON5.parse(content.toString())); } }, { @@ -77,6 +79,9 @@ const SCSS_LOADERS = [ export const commonExports = { plugins: [ + new EnvironmentPlugin({ + languageHashes: getFileHashes(path.join(__dirname, '..', 'src', 'assets', 'i18n'), /.*\.json5/g), + }), new CopyWebpackPlugin(copyWebpackOptions), ], module: { diff --git a/webpack/webpack.prod.ts b/webpack/webpack.prod.ts index 559b7f1dc71..e35bc0c9078 100644 --- a/webpack/webpack.prod.ts +++ b/webpack/webpack.prod.ts @@ -1,16 +1,16 @@ -import { commonExports } from './webpack.common'; -import { projectRoot } from './helpers'; +import { EnvironmentPlugin } from 'webpack'; -const webpack = require('webpack'); +import { projectRoot } from './helpers'; +import { commonExports } from './webpack.common'; module.exports = Object.assign({}, commonExports, { plugins: [ ...commonExports.plugins, - new webpack.EnvironmentPlugin({ + new EnvironmentPlugin({ 'process.env': { - NODE_ENV: JSON.stringify('production'), - AOT: true - } + NODE_ENV: 'production', + AOT: true, + }, }), ], mode: 'production', diff --git a/yarn.lock b/yarn.lock deleted file mode 100644 index 730966fcdb5..00000000000 --- a/yarn.lock +++ /dev/null @@ -1,11936 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -"@ampproject/remapping@2.2.0": - version "2.2.0" - resolved "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz" - integrity sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w== - dependencies: - "@jridgewell/gen-mapping" "^0.1.0" - "@jridgewell/trace-mapping" "^0.3.9" - -"@ampproject/remapping@^2.1.0": - version "2.2.1" - resolved "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz" - integrity sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg== - dependencies: - "@jridgewell/gen-mapping" "^0.3.0" - "@jridgewell/trace-mapping" "^0.3.9" - -"@angular-builders/custom-webpack@~15.0.0": - version "15.0.0" - resolved "https://registry.npmjs.org/@angular-builders/custom-webpack/-/custom-webpack-15.0.0.tgz" - integrity sha512-i8ZQ7rJ+RTy73MivvIdNAa8sZpiDy9wHBetAEyYSlGtjaSB3lntEFtBw9edfyRkpbifw53QA/N5haOwox987/Q== - dependencies: - "@angular-devkit/architect" ">=0.1500.0 < 0.1600.0" - "@angular-devkit/build-angular" "^15.0.0" - "@angular-devkit/core" "^15.0.0" - lodash "^4.17.15" - ts-node "^10.0.0" - tsconfig-paths "^4.1.0" - webpack-merge "^5.7.3" - -"@angular-devkit/architect@0.1502.5", "@angular-devkit/architect@>=0.1500.0 < 0.1600.0", "@angular-devkit/architect@~0.1502.0": - version "0.1502.5" - resolved "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1502.5.tgz" - integrity sha512-6KVrXQ/X7W88WSJvYe69ed/2QzQNlObKpj3BWzmcKnA+IvJB37/mvw8VaGFP9y+pDa/b1D1yCDtAJLeP5QY3xg== - dependencies: - "@angular-devkit/core" "15.2.5" - rxjs "6.6.7" - -"@angular-devkit/architect@0.1502.6": - version "0.1502.6" - resolved "https://registry.yarnpkg.com/@angular-devkit/architect/-/architect-0.1502.6.tgz#bf9452610f128ee312ee836510700152993a2447" - integrity sha512-n4oJ9vzFWwabf+AfgqqevVzdJhNKNCav7ytefjD/Y01vkNwlXqWnHcvyyHCLkVibJ6WR8J9lK4t77j/HFlDvWQ== - dependencies: - "@angular-devkit/core" "15.2.6" - rxjs "6.6.7" - -"@angular-devkit/architect@^0.1202.10": - version "0.1202.18" - resolved "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1202.18.tgz" - integrity sha512-C4ASKe+xBjl91MJyHDLt3z7ICPF9FU6B0CeJ1phwrlSHK9lmFG99WGxEj/Tc82+vHyPhajqS5XJ38KyVAPBGzA== - dependencies: - "@angular-devkit/core" "12.2.18" - rxjs "6.6.7" - -"@angular-devkit/build-angular@^15.0.0": - version "15.2.5" - resolved "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-15.2.5.tgz" - integrity sha512-D2LxjBtUlgJnPxybOIN0XsENEGkVkqCGBBii5oK84HvgBHXO/EyP1WXpOdb2lOYSUZyjhOOs0q42LCobJoaxUw== - dependencies: - "@ampproject/remapping" "2.2.0" - "@angular-devkit/architect" "0.1502.5" - "@angular-devkit/build-webpack" "0.1502.5" - "@angular-devkit/core" "15.2.5" - "@babel/core" "7.20.12" - "@babel/generator" "7.20.14" - "@babel/helper-annotate-as-pure" "7.18.6" - "@babel/helper-split-export-declaration" "7.18.6" - "@babel/plugin-proposal-async-generator-functions" "7.20.7" - "@babel/plugin-transform-async-to-generator" "7.20.7" - "@babel/plugin-transform-runtime" "7.19.6" - "@babel/preset-env" "7.20.2" - "@babel/runtime" "7.20.13" - "@babel/template" "7.20.7" - "@discoveryjs/json-ext" "0.5.7" - "@ngtools/webpack" "15.2.5" - ansi-colors "4.1.3" - autoprefixer "10.4.13" - babel-loader "9.1.2" - babel-plugin-istanbul "6.1.1" - browserslist "4.21.5" - cacache "17.0.4" - chokidar "3.5.3" - copy-webpack-plugin "11.0.0" - critters "0.0.16" - css-loader "6.7.3" - esbuild-wasm "0.17.8" - glob "8.1.0" - https-proxy-agent "5.0.1" - inquirer "8.2.4" - jsonc-parser "3.2.0" - karma-source-map-support "1.4.0" - less "4.1.3" - less-loader "11.1.0" - license-webpack-plugin "4.0.2" - loader-utils "3.2.1" - magic-string "0.29.0" - mini-css-extract-plugin "2.7.2" - open "8.4.1" - ora "5.4.1" - parse5-html-rewriting-stream "7.0.0" - piscina "3.2.0" - postcss "8.4.21" - postcss-loader "7.0.2" - resolve-url-loader "5.0.0" - rxjs "6.6.7" - sass "1.58.1" - sass-loader "13.2.0" - semver "7.3.8" - source-map-loader "4.0.1" - source-map-support "0.5.21" - terser "5.16.3" - text-table "0.2.0" - tree-kill "1.2.2" - tslib "2.5.0" - webpack "5.76.1" - webpack-dev-middleware "6.0.1" - webpack-dev-server "4.11.1" - webpack-merge "5.8.0" - webpack-subresource-integrity "5.1.0" - optionalDependencies: - esbuild "0.17.8" - -"@angular-devkit/build-angular@^15.2.6": - version "15.2.6" - resolved "https://registry.yarnpkg.com/@angular-devkit/build-angular/-/build-angular-15.2.6.tgz#15f15c27eed465c8becc7a313d4909a7ee66f959" - integrity sha512-OmMcdXXUrAdZNxwxDE8SUx1FMcq9FyMnrSv1PmP9sHPBoxAdBVc/qNdGA9V7C5yHvWHGgzsx7ZK5TDuvifzS5g== - dependencies: - "@ampproject/remapping" "2.2.0" - "@angular-devkit/architect" "0.1502.6" - "@angular-devkit/build-webpack" "0.1502.6" - "@angular-devkit/core" "15.2.6" - "@babel/core" "7.20.12" - "@babel/generator" "7.20.14" - "@babel/helper-annotate-as-pure" "7.18.6" - "@babel/helper-split-export-declaration" "7.18.6" - "@babel/plugin-proposal-async-generator-functions" "7.20.7" - "@babel/plugin-transform-async-to-generator" "7.20.7" - "@babel/plugin-transform-runtime" "7.19.6" - "@babel/preset-env" "7.20.2" - "@babel/runtime" "7.20.13" - "@babel/template" "7.20.7" - "@discoveryjs/json-ext" "0.5.7" - "@ngtools/webpack" "15.2.6" - ansi-colors "4.1.3" - autoprefixer "10.4.13" - babel-loader "9.1.2" - babel-plugin-istanbul "6.1.1" - browserslist "4.21.5" - cacache "17.0.4" - chokidar "3.5.3" - copy-webpack-plugin "11.0.0" - critters "0.0.16" - css-loader "6.7.3" - esbuild-wasm "0.17.8" - glob "8.1.0" - https-proxy-agent "5.0.1" - inquirer "8.2.4" - jsonc-parser "3.2.0" - karma-source-map-support "1.4.0" - less "4.1.3" - less-loader "11.1.0" - license-webpack-plugin "4.0.2" - loader-utils "3.2.1" - magic-string "0.29.0" - mini-css-extract-plugin "2.7.2" - open "8.4.1" - ora "5.4.1" - parse5-html-rewriting-stream "7.0.0" - piscina "3.2.0" - postcss "8.4.21" - postcss-loader "7.0.2" - resolve-url-loader "5.0.0" - rxjs "6.6.7" - sass "1.58.1" - sass-loader "13.2.0" - semver "7.3.8" - source-map-loader "4.0.1" - source-map-support "0.5.21" - terser "5.16.3" - text-table "0.2.0" - tree-kill "1.2.2" - tslib "2.5.0" - webpack "5.76.1" - webpack-dev-middleware "6.0.1" - webpack-dev-server "4.11.1" - webpack-merge "5.8.0" - webpack-subresource-integrity "5.1.0" - optionalDependencies: - esbuild "0.17.8" - -"@angular-devkit/build-webpack@0.1502.5": - version "0.1502.5" - resolved "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1502.5.tgz" - integrity sha512-gPkAa4AvQ7BxU+jmVJqrAO18kw/6iks+VUQ+2BVPyHCdqhroANHYdGbZ/pFlZdPmZVzSpusjd6VIbLhbHr/Ohw== - dependencies: - "@angular-devkit/architect" "0.1502.5" - rxjs "6.6.7" - -"@angular-devkit/build-webpack@0.1502.6": - version "0.1502.6" - resolved "https://registry.yarnpkg.com/@angular-devkit/build-webpack/-/build-webpack-0.1502.6.tgz#5fb444fc54d816e57458d36f3c280b522e5f5cfc" - integrity sha512-X7XQ11QDz2Bs5qpJ3a5glIytvI+S74ORQxdzvT6a6KB8ayW0SgZEhTwD+GF7pa5My8draIaXBGzzQR1qmpWK5Q== - dependencies: - "@angular-devkit/architect" "0.1502.6" - rxjs "6.6.7" - -"@angular-devkit/core@12.2.18", "@angular-devkit/core@^12.2.17": - version "12.2.18" - resolved "https://registry.npmjs.org/@angular-devkit/core/-/core-12.2.18.tgz" - integrity sha512-GDLHGe9HEY5SRS+NrKr14C8aHsRCiBFkBFSSbeohgLgcgSXzZHFoU84nDWrl3KZNP8oqcUSv5lHu6dLcf2fnww== - 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" - -"@angular-devkit/core@15.2.5", "@angular-devkit/core@^15.0.0", "@angular-devkit/core@~15.2.0": - version "15.2.5" - resolved "https://registry.npmjs.org/@angular-devkit/core/-/core-15.2.5.tgz" - integrity sha512-ZfjEkAe2yYeekc3xjZ/U4pK9nb+w6BFwAEjou6mE8PWZH7iYskm0YCCXkmu+B+zViEcCLhAkJAxu9MwX4efd8g== - dependencies: - ajv "8.12.0" - ajv-formats "2.1.1" - jsonc-parser "3.2.0" - rxjs "6.6.7" - source-map "0.7.4" - -"@angular-devkit/core@15.2.6": - version "15.2.6" - resolved "https://registry.yarnpkg.com/@angular-devkit/core/-/core-15.2.6.tgz#9118faadbc5e3613283da1774826d2b41883330f" - integrity sha512-YVTWZ+M+xNKdFX4EnY9QX49PZraawiaA0iTd2CUW8ZoTUvU7yOGMKZLSdz6aokTMRVfm0449wt6YL994ibOo1g== - dependencies: - ajv "8.12.0" - ajv-formats "2.1.1" - jsonc-parser "3.2.0" - rxjs "6.6.7" - source-map "0.7.4" - -"@angular-devkit/schematics@12.2.18", "@angular-devkit/schematics@^12.2.17": - version "12.2.18" - resolved "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-12.2.18.tgz" - integrity sha512-bZ9NS5PgoVfetRC6WeQBHCY5FqPZ9y2TKHUo12sOB2YSL3tgWgh1oXyP8PtX34gasqsLjNULxEQsAQYEsiX/qQ== - dependencies: - "@angular-devkit/core" "12.2.18" - ora "5.4.1" - rxjs "6.6.7" - -"@angular-devkit/schematics@15.2.6": - version "15.2.6" - resolved "https://registry.yarnpkg.com/@angular-devkit/schematics/-/schematics-15.2.6.tgz#fb5b7530f21586dbdec45cac56f8a418bcfd053d" - integrity sha512-f7VgnAcok7AwR/DhX0ZWskB0rFBo/KsvtIUA2qZSrpKMf8eFiwu03dv/b2mI0vnf+1FBfIQzJvO0ww45zRp6dA== - dependencies: - "@angular-devkit/core" "15.2.6" - jsonc-parser "3.2.0" - magic-string "0.29.0" - ora "5.4.1" - rxjs "6.6.7" - -"@angular-eslint/builder@15.2.1": - version "15.2.1" - resolved "https://registry.npmjs.org/@angular-eslint/builder/-/builder-15.2.1.tgz" - integrity sha512-7x2DANebLRl997Mj4DhZrnz5+vnSjavGGveJ0mBuU7CEsL0ZYLftdRqL0e0HtU3ksseS7xpchD6OM08nkNgySw== - -"@angular-eslint/bundled-angular-compiler@15.2.1": - version "15.2.1" - resolved "https://registry.npmjs.org/@angular-eslint/bundled-angular-compiler/-/bundled-angular-compiler-15.2.1.tgz" - integrity sha512-LO7Am8eVCr7oh6a0VmKSL7K03CnQEQhFO7Wt/YtbfYOxVjrbwmYLwJn+wZPOT7A02t/BttOD/WXuDrOWtSMQ/Q== - -"@angular-eslint/eslint-plugin-template@15.2.1": - version "15.2.1" - resolved "https://registry.npmjs.org/@angular-eslint/eslint-plugin-template/-/eslint-plugin-template-15.2.1.tgz" - integrity sha512-IeiSLk6YxapFdH2z5o/O3R7VwtBd2T6fWmhLFPwDYMDknrwegnOjwswCdBplOccpUp0wqlCeGUx7LTsuzwaz7w== - dependencies: - "@angular-eslint/bundled-angular-compiler" "15.2.1" - "@angular-eslint/utils" "15.2.1" - "@typescript-eslint/type-utils" "5.48.2" - "@typescript-eslint/utils" "5.48.2" - aria-query "5.1.3" - axobject-query "3.1.1" - -"@angular-eslint/eslint-plugin@15.2.1": - version "15.2.1" - resolved "https://registry.npmjs.org/@angular-eslint/eslint-plugin/-/eslint-plugin-15.2.1.tgz" - integrity sha512-OM7b1kS4E4CkXjkaWN+lEzawh4VxY6l7FO1Cuk4s7iv3/YpZG3rJxIZBqnFLTixwrBuqw8y4FNBzF3eDgmFAUw== - dependencies: - "@angular-eslint/utils" "15.2.1" - "@typescript-eslint/utils" "5.48.2" - -"@angular-eslint/schematics@15.2.1": - version "15.2.1" - resolved "https://registry.npmjs.org/@angular-eslint/schematics/-/schematics-15.2.1.tgz" - integrity sha512-0ZfBCejHWIcgy3J5kFs9sS/jqi8i5AptxggOwFySOlCLJ+CzNrktjD4jff1Zy8K/VLzY0Ci0BSZXvgWfP0k9Rg== - dependencies: - "@angular-eslint/eslint-plugin" "15.2.1" - "@angular-eslint/eslint-plugin-template" "15.2.1" - ignore "5.2.4" - strip-json-comments "3.1.1" - tmp "0.2.1" - -"@angular-eslint/template-parser@15.2.1": - version "15.2.1" - resolved "https://registry.npmjs.org/@angular-eslint/template-parser/-/template-parser-15.2.1.tgz" - integrity sha512-ViCi79gC2aKJecmYLkOT+QlT5WMRNXeYz0Dr9Pr8qXzIbY0oAWE7nOT5jkXwQ9oUk+ybtGCWHma5JVJWVJsIog== - dependencies: - "@angular-eslint/bundled-angular-compiler" "15.2.1" - eslint-scope "^7.0.0" - -"@angular-eslint/utils@15.2.1": - version "15.2.1" - resolved "https://registry.npmjs.org/@angular-eslint/utils/-/utils-15.2.1.tgz" - integrity sha512-++FneAJHxJqcSu0igVN6uOkSoHxlzgLoMBswuovYJy3UKwm33/T6WFku8++753Ca/JucIoR1gdUfO7SoSspMDg== - dependencies: - "@angular-eslint/bundled-angular-compiler" "15.2.1" - "@typescript-eslint/utils" "5.48.2" - -"@angular/animations@^15.2.8": - version "15.2.8" - resolved "https://registry.yarnpkg.com/@angular/animations/-/animations-15.2.8.tgz#11de2b0e7fc50918056bdc6d7545ea1864a8592d" - integrity sha512-I3xh8EASQ04s3qXQYpIORI0jFiFmvBQERBqS70TieTCIML7banOf9R3K7sAWB9frG5J0CEUwr+wtF47DCs/7eQ== - dependencies: - tslib "^2.3.0" - -"@angular/cdk@^15.2.8": - version "15.2.8" - resolved "https://registry.yarnpkg.com/@angular/cdk/-/cdk-15.2.8.tgz#f7da13abd6fb6a1355c02f5c02dc32bc679c4b1d" - integrity sha512-jiCoxfBFMH29IZIiPmVUzIWetfUNpMIvC20xYVF8RMM819vPogoObzwK4DN/sXcp/6oVbBzZFaYdijhhIt9soQ== - dependencies: - tslib "^2.3.0" - optionalDependencies: - parse5 "^7.1.2" - -"@angular/cli@^15.2.6": - version "15.2.6" - resolved "https://registry.yarnpkg.com/@angular/cli/-/cli-15.2.6.tgz#d5994fbd4d02e71017d4dd28ba06dc1c3cb7a976" - integrity sha512-wNkQ/qCVbd4pERaGVagKJPifEvjRNY5otwsd4iRVubY/XOcIHcYChUThZwgQdVfNAImfJPMZNrhbGxejuWLA9w== - dependencies: - "@angular-devkit/architect" "0.1502.6" - "@angular-devkit/core" "15.2.6" - "@angular-devkit/schematics" "15.2.6" - "@schematics/angular" "15.2.6" - "@yarnpkg/lockfile" "1.1.0" - ansi-colors "4.1.3" - ini "3.0.1" - inquirer "8.2.4" - jsonc-parser "3.2.0" - npm-package-arg "10.1.0" - npm-pick-manifest "8.0.1" - open "8.4.1" - ora "5.4.1" - pacote "15.1.0" - resolve "1.22.1" - semver "7.3.8" - symbol-observable "4.0.0" - yargs "17.6.2" - -"@angular/common@^15.2.8": - version "15.2.8" - resolved "https://registry.yarnpkg.com/@angular/common/-/common-15.2.8.tgz#7d60a4d906b41a852f31c63116ede1b87c84a7ad" - integrity sha512-yLDQihiRcVl38HrWMPbqgzOaSUw85AQH5BsGdjbS6BpoBQj3EXOpccCMFsuxOKxPG4toatgawNqrEnK0Jpv9Mw== - dependencies: - tslib "^2.3.0" - -"@angular/compiler-cli@^15.2.8": - version "15.2.8" - resolved "https://registry.yarnpkg.com/@angular/compiler-cli/-/compiler-cli-15.2.8.tgz#6535a34b51d143970aa046593d5289883a40fea8" - integrity sha512-fFxaDlbILo0t2t662qA0cjgn+kWItGlc1tFYKU6X7bvYb3t2e0cd9FzrFPLXUQVboGis83ULcJ2zkDxScnuPuQ== - dependencies: - "@babel/core" "7.19.3" - "@jridgewell/sourcemap-codec" "^1.4.14" - chokidar "^3.0.0" - convert-source-map "^1.5.1" - dependency-graph "^0.11.0" - magic-string "^0.27.0" - reflect-metadata "^0.1.2" - semver "^7.0.0" - tslib "^2.3.0" - yargs "^17.2.1" - -"@angular/compiler@^15.2.8": - version "15.2.8" - resolved "https://registry.yarnpkg.com/@angular/compiler/-/compiler-15.2.8.tgz#7d8f3274e9f011b7b972ba616cd536a66d1e52ea" - integrity sha512-+dvspIDvuGoYqdL7r/3o9ojkR3fH1zevgC0ISJivcIrMi+WcJ0FV2JmJdnm8V52oNsHy+sMF9eEZGEbCbACE/A== - dependencies: - tslib "^2.3.0" - -"@angular/core@^15.2.8": - version "15.2.8" - resolved "https://registry.yarnpkg.com/@angular/core/-/core-15.2.8.tgz#6d7f8afa61be44b91e4b8737b39d7fdcb256a6ae" - integrity sha512-NDs+g4uM4EhyCvluf8a0YBCFXsDAEfCMHOD5cS00Bl+liTQ7JwtmepkWXMyjLB92irC9JaR79kdy4BoIKOh8WA== - dependencies: - tslib "^2.3.0" - -"@angular/forms@^15.2.8": - version "15.2.8" - resolved "https://registry.yarnpkg.com/@angular/forms/-/forms-15.2.8.tgz#f62a3ede067ddac24ed1b7dc5cefe9b4b0370be7" - integrity sha512-VyevVj20DdQWjAQUyiFTe+DAzqG9GqfAOWn376Y/lhPcwxAojXePTGNgtQud566/urDrNrP5haaLD6O36/3n+w== - dependencies: - tslib "^2.3.0" - -"@angular/language-service@^15.2.8": - version "15.2.8" - resolved "https://registry.yarnpkg.com/@angular/language-service/-/language-service-15.2.8.tgz#f37fb8b020c9d0fccf1bb5b46db525b67084cc9c" - integrity sha512-3wlaJrpxe8ZLe7ZGIOs/Z++kgJMOHZCUvdXLyp/4t1ejULgUgW9yi7nGsemWnSzsUCjf7595BP1Cp1gA/J/1QQ== - -"@angular/localize@15.2.8": - version "15.2.8" - resolved "https://registry.yarnpkg.com/@angular/localize/-/localize-15.2.8.tgz#fa44efa7d3f84d4767b3b3ca586e7d36929aff54" - integrity sha512-wJLBp0MUnET9kHzBtqIlZ3RQ56JFItXSgmBXagQq+MU+uJZmGvuw6fez0i5wkgv9Rgnr25oCULVtpTF+T5RGYA== - dependencies: - "@babel/core" "7.19.3" - glob "8.1.0" - yargs "^17.2.1" - -"@angular/platform-browser-dynamic@^15.2.8": - version "15.2.8" - resolved "https://registry.yarnpkg.com/@angular/platform-browser-dynamic/-/platform-browser-dynamic-15.2.8.tgz#158d28298a6f38dc8a17c416eb2779afcafc16a7" - integrity sha512-75HyoZNibA3u/FvdK4Aw5KMzUmS/nDk5N8s7gfM09fe1resSPgFiW8JJEkr1xiUdA2WtSRbHs34y5rHLDe7n1Q== - dependencies: - tslib "^2.3.0" - -"@angular/platform-browser@^15.2.8": - version "15.2.8" - resolved "https://registry.yarnpkg.com/@angular/platform-browser/-/platform-browser-15.2.8.tgz#2400f732a020d59f17b9bf8c6c3f1fb065147328" - integrity sha512-8sKFUld54inj0FnQ1ydhFxnDgsbbf43W9FALye/5uEtLgwwE/ZvkNYMaQ7hq1JPuQRMDj3gJkFqaLeFjplpHDA== - dependencies: - tslib "^2.3.0" - -"@angular/platform-server@^15.2.8": - version "15.2.8" - resolved "https://registry.yarnpkg.com/@angular/platform-server/-/platform-server-15.2.8.tgz#b850cd70baabd986e0d94f27bff1dc44f958f3af" - integrity sha512-YfPAoK2zp3mTO6ubDe0C9Vzg9Vi4BxXkxHCPG0k8IM3Ivjedkv5PZ+vC0a+GjV2xR/0YqiVE3FCwXk4X6m6LuQ== - dependencies: - domino "^2.1.2" - tslib "^2.3.0" - xhr2 "^0.2.0" - -"@angular/router@^15.2.8": - version "15.2.8" - resolved "https://registry.yarnpkg.com/@angular/router/-/router-15.2.8.tgz#99ebf83d34dae3ebe61097b43aded3743b49c5db" - integrity sha512-C62QBEeJSBTNTrQHZiklPrxwJwuENoZzWX22MMJ7dxl+7VjRgnmj8J7mcX9fLjHlL+mC3RvesMlX7sGZRQV1cg== - dependencies: - tslib "^2.3.0" - -"@assemblyscript/loader@^0.10.1": - version "0.10.1" - resolved "https://registry.npmjs.org/@assemblyscript/loader/-/loader-0.10.1.tgz" - integrity sha512-H71nDOOL8Y7kWRLqf6Sums+01Q5msqBW2KhDUTemh1tvY04eSkSXrK0uj/4mmY0Xr16/3zyZmsrxN7CKuRbNRg== - -"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.18.6", "@babel/code-frame@^7.21.4": - version "7.21.4" - resolved "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.21.4.tgz" - integrity sha512-LYvhNKfwWSPpocw8GI7gpK2nq3HSDuEPC/uSYaALSJu9xjsalaaYFOq0Pwt5KmVqwEbZlDu81aLXwBOmD/Fv9g== - dependencies: - "@babel/highlight" "^7.18.6" - -"@babel/compat-data@^7.17.7", "@babel/compat-data@^7.20.1", "@babel/compat-data@^7.20.5", "@babel/compat-data@^7.21.4": - version "7.21.4" - resolved "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.21.4.tgz" - integrity sha512-/DYyDpeCfaVinT40FPGdkkb+lYSKvsVuMjDAG7jPOWWiM1ibOaB9CXJAlc4d1QpP/U2q2P9jbrSlClKSErd55g== - -"@babel/core@7.19.3": - version "7.19.3" - resolved "https://registry.npmjs.org/@babel/core/-/core-7.19.3.tgz" - integrity sha512-WneDJxdsjEvyKtXKsaBGbDeiyOjR5vYq4HcShxnIbG0qixpoHjI3MqeZM9NDvsojNCEBItQE4juOo/bU6e72gQ== - dependencies: - "@ampproject/remapping" "^2.1.0" - "@babel/code-frame" "^7.18.6" - "@babel/generator" "^7.19.3" - "@babel/helper-compilation-targets" "^7.19.3" - "@babel/helper-module-transforms" "^7.19.0" - "@babel/helpers" "^7.19.0" - "@babel/parser" "^7.19.3" - "@babel/template" "^7.18.10" - "@babel/traverse" "^7.19.3" - "@babel/types" "^7.19.3" - convert-source-map "^1.7.0" - debug "^4.1.0" - gensync "^1.0.0-beta.2" - json5 "^2.2.1" - semver "^6.3.0" - -"@babel/core@7.20.12": - version "7.20.12" - resolved "https://registry.npmjs.org/@babel/core/-/core-7.20.12.tgz" - integrity sha512-XsMfHovsUYHFMdrIHkZphTN/2Hzzi78R08NuHfDBehym2VsPDL6Zn/JAD/JQdnRvbSsbQc4mVaU1m6JgtTEElg== - dependencies: - "@ampproject/remapping" "^2.1.0" - "@babel/code-frame" "^7.18.6" - "@babel/generator" "^7.20.7" - "@babel/helper-compilation-targets" "^7.20.7" - "@babel/helper-module-transforms" "^7.20.11" - "@babel/helpers" "^7.20.7" - "@babel/parser" "^7.20.7" - "@babel/template" "^7.20.7" - "@babel/traverse" "^7.20.12" - "@babel/types" "^7.20.7" - convert-source-map "^1.7.0" - debug "^4.1.0" - gensync "^1.0.0-beta.2" - json5 "^2.2.2" - semver "^6.3.0" - -"@babel/core@^7.12.3": - version "7.18.9" - resolved "https://registry.npmjs.org/@babel/core/-/core-7.18.9.tgz" - integrity sha512-1LIb1eL8APMy91/IMW+31ckrfBM4yCoLaVzoDhZUKSM4cu1L1nIidyxkCgzPAgrC5WEz36IPEr/eSeSF9pIn+g== - dependencies: - "@ampproject/remapping" "^2.1.0" - "@babel/code-frame" "^7.18.6" - "@babel/generator" "^7.18.9" - "@babel/helper-compilation-targets" "^7.18.9" - "@babel/helper-module-transforms" "^7.18.9" - "@babel/helpers" "^7.18.9" - "@babel/parser" "^7.18.9" - "@babel/template" "^7.18.6" - "@babel/traverse" "^7.18.9" - "@babel/types" "^7.18.9" - convert-source-map "^1.7.0" - debug "^4.1.0" - gensync "^1.0.0-beta.2" - json5 "^2.2.1" - semver "^6.3.0" - -"@babel/generator@7.20.14", "@babel/generator@^7.20.7": - version "7.20.14" - resolved "https://registry.npmjs.org/@babel/generator/-/generator-7.20.14.tgz" - integrity sha512-AEmuXHdcD3A52HHXxaTmYlb8q/xMEhoRP67B3T4Oq7lbmSoqroMZzjnGj3+i1io3pdnF8iBYVu4Ilj+c4hBxYg== - dependencies: - "@babel/types" "^7.20.7" - "@jridgewell/gen-mapping" "^0.3.2" - jsesc "^2.5.1" - -"@babel/generator@^7.18.9", "@babel/generator@^7.19.3", "@babel/generator@^7.21.4": - version "7.21.4" - resolved "https://registry.npmjs.org/@babel/generator/-/generator-7.21.4.tgz" - integrity sha512-NieM3pVIYW2SwGzKoqfPrQsf4xGs9M9AIG3ThppsSRmO+m7eQhmI6amajKMUeIO37wFfsvnvcxQFx6x6iqxDnA== - dependencies: - "@babel/types" "^7.21.4" - "@jridgewell/gen-mapping" "^0.3.2" - "@jridgewell/trace-mapping" "^0.3.17" - jsesc "^2.5.1" - -"@babel/helper-annotate-as-pure@7.18.6", "@babel/helper-annotate-as-pure@^7.18.6": - version "7.18.6" - resolved "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz" - integrity sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA== - dependencies: - "@babel/types" "^7.18.6" - -"@babel/helper-builder-binary-assignment-operator-visitor@^7.18.6": - version "7.18.9" - resolved "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.18.9.tgz" - integrity sha512-yFQ0YCHoIqarl8BCRwBL8ulYUaZpz3bNsA7oFepAzee+8/+ImtADXNOmO5vJvsPff3qi+hvpkY/NYBTrBQgdNw== - dependencies: - "@babel/helper-explode-assignable-expression" "^7.18.6" - "@babel/types" "^7.18.9" - -"@babel/helper-compilation-targets@^7.17.7", "@babel/helper-compilation-targets@^7.18.9", "@babel/helper-compilation-targets@^7.19.3", "@babel/helper-compilation-targets@^7.20.0", "@babel/helper-compilation-targets@^7.20.7": - version "7.21.4" - resolved "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.21.4.tgz" - integrity sha512-Fa0tTuOXZ1iL8IeDFUWCzjZcn+sJGd9RZdH9esYVjEejGmzf+FFYQpMi/kZUk2kPy/q1H3/GPw7np8qar/stfg== - dependencies: - "@babel/compat-data" "^7.21.4" - "@babel/helper-validator-option" "^7.21.0" - browserslist "^4.21.3" - lru-cache "^5.1.1" - semver "^6.3.0" - -"@babel/helper-create-class-features-plugin@^7.18.6", "@babel/helper-create-class-features-plugin@^7.21.0": - version "7.21.4" - resolved "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.21.4.tgz" - integrity sha512-46QrX2CQlaFRF4TkwfTt6nJD7IHq8539cCL7SDpqWSDeJKY1xylKKY5F/33mJhLZ3mFvKv2gGrVS6NkyF6qs+Q== - dependencies: - "@babel/helper-annotate-as-pure" "^7.18.6" - "@babel/helper-environment-visitor" "^7.18.9" - "@babel/helper-function-name" "^7.21.0" - "@babel/helper-member-expression-to-functions" "^7.21.0" - "@babel/helper-optimise-call-expression" "^7.18.6" - "@babel/helper-replace-supers" "^7.20.7" - "@babel/helper-skip-transparent-expression-wrappers" "^7.20.0" - "@babel/helper-split-export-declaration" "^7.18.6" - -"@babel/helper-create-regexp-features-plugin@^7.18.6", "@babel/helper-create-regexp-features-plugin@^7.20.5": - version "7.21.4" - resolved "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.21.4.tgz" - integrity sha512-M00OuhU+0GyZ5iBBN9czjugzWrEq2vDpf/zCYHxxf93ul/Q5rv+a5h+/+0WnI1AebHNVtl5bFV0qsJoH23DbfA== - dependencies: - "@babel/helper-annotate-as-pure" "^7.18.6" - regexpu-core "^5.3.1" - -"@babel/helper-define-polyfill-provider@^0.3.3": - version "0.3.3" - resolved "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.3.3.tgz" - integrity sha512-z5aQKU4IzbqCC1XH0nAqfsFLMVSo22SBKUc0BxGrLkolTdPTructy0ToNnlO2zA4j9Q/7pjMZf0DSY+DSTYzww== - dependencies: - "@babel/helper-compilation-targets" "^7.17.7" - "@babel/helper-plugin-utils" "^7.16.7" - debug "^4.1.1" - lodash.debounce "^4.0.8" - resolve "^1.14.2" - semver "^6.1.2" - -"@babel/helper-environment-visitor@^7.18.9": - version "7.18.9" - resolved "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz" - integrity sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg== - -"@babel/helper-explode-assignable-expression@^7.18.6": - version "7.18.6" - resolved "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.18.6.tgz" - integrity sha512-eyAYAsQmB80jNfg4baAtLeWAQHfHFiR483rzFK+BhETlGZaQC9bsfrugfXDCbRHLQbIA7U5NxhhOxN7p/dWIcg== - dependencies: - "@babel/types" "^7.18.6" - -"@babel/helper-function-name@^7.18.9", "@babel/helper-function-name@^7.19.0", "@babel/helper-function-name@^7.21.0": - version "7.21.0" - resolved "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.21.0.tgz" - integrity sha512-HfK1aMRanKHpxemaY2gqBmL04iAPOPRj7DxtNbiDOrJK+gdwkiNRVpCpUJYbUT+aZyemKN8brqTOxzCaG6ExRg== - dependencies: - "@babel/template" "^7.20.7" - "@babel/types" "^7.21.0" - -"@babel/helper-hoist-variables@^7.18.6": - version "7.18.6" - resolved "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz" - integrity sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q== - dependencies: - "@babel/types" "^7.18.6" - -"@babel/helper-member-expression-to-functions@^7.20.7", "@babel/helper-member-expression-to-functions@^7.21.0": - version "7.21.0" - resolved "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.21.0.tgz" - integrity sha512-Muu8cdZwNN6mRRNG6lAYErJ5X3bRevgYR2O8wN0yn7jJSnGDu6eG59RfT29JHxGUovyfrh6Pj0XzmR7drNVL3Q== - dependencies: - "@babel/types" "^7.21.0" - -"@babel/helper-module-imports@^7.18.6": - version "7.21.4" - resolved "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.21.4.tgz" - integrity sha512-orajc5T2PsRYUN3ZryCEFeMDYwyw09c/pZeaQEZPH0MpKzSvn3e0uXsDBu3k03VI+9DBiRo+l22BfKTpKwa/Wg== - dependencies: - "@babel/types" "^7.21.4" - -"@babel/helper-module-transforms@^7.18.6", "@babel/helper-module-transforms@^7.18.9", "@babel/helper-module-transforms@^7.19.0", "@babel/helper-module-transforms@^7.20.11", "@babel/helper-module-transforms@^7.21.2": - version "7.21.2" - resolved "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.21.2.tgz" - integrity sha512-79yj2AR4U/Oqq/WOV7Lx6hUjau1Zfo4cI+JLAVYeMV5XIlbOhmjEk5ulbTc9fMpmlojzZHkUUxAiK+UKn+hNQQ== - dependencies: - "@babel/helper-environment-visitor" "^7.18.9" - "@babel/helper-module-imports" "^7.18.6" - "@babel/helper-simple-access" "^7.20.2" - "@babel/helper-split-export-declaration" "^7.18.6" - "@babel/helper-validator-identifier" "^7.19.1" - "@babel/template" "^7.20.7" - "@babel/traverse" "^7.21.2" - "@babel/types" "^7.21.2" - -"@babel/helper-optimise-call-expression@^7.18.6": - version "7.18.6" - resolved "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.18.6.tgz" - integrity sha512-HP59oD9/fEHQkdcbgFCnbmgH5vIQTJbxh2yf+CdM89/glUNnuzr87Q8GIjGEnOktTROemO0Pe0iPAYbqZuOUiA== - dependencies: - "@babel/types" "^7.18.6" - -"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.16.7", "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.18.9", "@babel/helper-plugin-utils@^7.19.0", "@babel/helper-plugin-utils@^7.20.2", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3": - version "7.20.2" - resolved "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.20.2.tgz" - integrity sha512-8RvlJG2mj4huQ4pZ+rU9lqKi9ZKiRmuvGuM2HlWmkmgOhbs6zEAw6IEiJ5cQqGbDzGZOhwuOQNtZMi/ENLjZoQ== - -"@babel/helper-remap-async-to-generator@^7.18.9": - version "7.18.9" - resolved "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.18.9.tgz" - integrity sha512-dI7q50YKd8BAv3VEfgg7PS7yD3Rtbi2J1XMXaalXO0W0164hYLnh8zpjRS0mte9MfVp/tltvr/cfdXPvJr1opA== - dependencies: - "@babel/helper-annotate-as-pure" "^7.18.6" - "@babel/helper-environment-visitor" "^7.18.9" - "@babel/helper-wrap-function" "^7.18.9" - "@babel/types" "^7.18.9" - -"@babel/helper-replace-supers@^7.18.6", "@babel/helper-replace-supers@^7.20.7": - version "7.20.7" - resolved "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.20.7.tgz" - integrity sha512-vujDMtB6LVfNW13jhlCrp48QNslK6JXi7lQG736HVbHz/mbf4Dc7tIRh1Xf5C0rF7BP8iiSxGMCmY6Ci1ven3A== - dependencies: - "@babel/helper-environment-visitor" "^7.18.9" - "@babel/helper-member-expression-to-functions" "^7.20.7" - "@babel/helper-optimise-call-expression" "^7.18.6" - "@babel/template" "^7.20.7" - "@babel/traverse" "^7.20.7" - "@babel/types" "^7.20.7" - -"@babel/helper-simple-access@^7.20.2": - version "7.20.2" - resolved "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.20.2.tgz" - integrity sha512-+0woI/WPq59IrqDYbVGfshjT5Dmk/nnbdpcF8SnMhhXObpTq2KNBdLFRFrkVdbDOyUmHBCxzm5FHV1rACIkIbA== - dependencies: - "@babel/types" "^7.20.2" - -"@babel/helper-skip-transparent-expression-wrappers@^7.20.0": - version "7.20.0" - resolved "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.20.0.tgz" - integrity sha512-5y1JYeNKfvnT8sZcK9DVRtpTbGiomYIHviSP3OQWmDPU3DeH4a1ZlT/N2lyQ5P8egjcRaT/Y9aNqUxK0WsnIIg== - dependencies: - "@babel/types" "^7.20.0" - -"@babel/helper-split-export-declaration@7.18.6", "@babel/helper-split-export-declaration@^7.18.6": - version "7.18.6" - resolved "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz" - integrity sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA== - dependencies: - "@babel/types" "^7.18.6" - -"@babel/helper-string-parser@^7.19.4": - version "7.19.4" - resolved "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz" - integrity sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw== - -"@babel/helper-validator-identifier@^7.18.6", "@babel/helper-validator-identifier@^7.19.1": - version "7.19.1" - resolved "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz" - integrity sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w== - -"@babel/helper-validator-option@^7.18.6", "@babel/helper-validator-option@^7.21.0": - version "7.21.0" - resolved "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.21.0.tgz" - integrity sha512-rmL/B8/f0mKS2baE9ZpyTcTavvEuWhTTW8amjzXNvYG4AwBsqTLikfXsEofsJEfKHf+HQVQbFOHy6o+4cnC/fQ== - -"@babel/helper-wrap-function@^7.18.9": - version "7.20.5" - resolved "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.20.5.tgz" - integrity sha512-bYMxIWK5mh+TgXGVqAtnu5Yn1un+v8DDZtqyzKRLUzrh70Eal2O3aZ7aPYiMADO4uKlkzOiRiZ6GX5q3qxvW9Q== - dependencies: - "@babel/helper-function-name" "^7.19.0" - "@babel/template" "^7.18.10" - "@babel/traverse" "^7.20.5" - "@babel/types" "^7.20.5" - -"@babel/helpers@^7.18.9", "@babel/helpers@^7.19.0", "@babel/helpers@^7.20.7": - version "7.21.0" - resolved "https://registry.npmjs.org/@babel/helpers/-/helpers-7.21.0.tgz" - integrity sha512-XXve0CBtOW0pd7MRzzmoyuSj0e3SEzj8pgyFxnTT1NJZL38BD1MK7yYrm8yefRPIDvNNe14xR4FdbHwpInD4rA== - dependencies: - "@babel/template" "^7.20.7" - "@babel/traverse" "^7.21.0" - "@babel/types" "^7.21.0" - -"@babel/highlight@^7.18.6": - version "7.18.6" - resolved "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz" - integrity sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g== - dependencies: - "@babel/helper-validator-identifier" "^7.18.6" - chalk "^2.0.0" - js-tokens "^4.0.0" - -"@babel/parser@^7.10.3", "@babel/parser@^7.14.7", "@babel/parser@^7.18.9", "@babel/parser@^7.19.3", "@babel/parser@^7.20.7", "@babel/parser@^7.21.4": - version "7.21.4" - resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.21.4.tgz" - integrity sha512-alVJj7k7zIxqBZ7BTRhz0IqJFxW1VJbm6N8JbcYhQ186df9ZBPbZBmWSqAMXwHGsCJdYks7z/voa3ibiS5bCIw== - -"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.18.6": - version "7.18.6" - 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.18.6.tgz" - integrity sha512-Dgxsyg54Fx1d4Nge8UnvTrED63vrwOdPmyvPzlNN/boaliRP54pm3pGzZD1SJUwrBA+Cs/xdG8kXX6Mn/RfISQ== - dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.18.9": - version "7.20.7" - resolved "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.20.7.tgz" - integrity sha512-sbr9+wNE5aXMBBFBICk01tt7sBf2Oc9ikRFEcem/ZORup9IMUdNhW7/wVLEbbtlWOsEubJet46mHAL2C8+2jKQ== - dependencies: - "@babel/helper-plugin-utils" "^7.20.2" - "@babel/helper-skip-transparent-expression-wrappers" "^7.20.0" - "@babel/plugin-proposal-optional-chaining" "^7.20.7" - -"@babel/plugin-proposal-async-generator-functions@7.20.7", "@babel/plugin-proposal-async-generator-functions@^7.20.1": - version "7.20.7" - resolved "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.20.7.tgz" - integrity sha512-xMbiLsn/8RK7Wq7VeVytytS2L6qE69bXPB10YCmMdDZbKF4okCqY74pI/jJQ/8U0b/F6NrT2+14b8/P9/3AMGA== - dependencies: - "@babel/helper-environment-visitor" "^7.18.9" - "@babel/helper-plugin-utils" "^7.20.2" - "@babel/helper-remap-async-to-generator" "^7.18.9" - "@babel/plugin-syntax-async-generators" "^7.8.4" - -"@babel/plugin-proposal-class-properties@^7.18.6": - version "7.18.6" - resolved "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz" - integrity sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ== - dependencies: - "@babel/helper-create-class-features-plugin" "^7.18.6" - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/plugin-proposal-class-static-block@^7.18.6": - version "7.21.0" - resolved "https://registry.npmjs.org/@babel/plugin-proposal-class-static-block/-/plugin-proposal-class-static-block-7.21.0.tgz" - integrity sha512-XP5G9MWNUskFuP30IfFSEFB0Z6HzLIUcjYM4bYOPHXl7eiJ9HFv8tWj6TXTN5QODiEhDZAeI4hLok2iHFFV4hw== - dependencies: - "@babel/helper-create-class-features-plugin" "^7.21.0" - "@babel/helper-plugin-utils" "^7.20.2" - "@babel/plugin-syntax-class-static-block" "^7.14.5" - -"@babel/plugin-proposal-dynamic-import@^7.18.6": - version "7.18.6" - resolved "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.18.6.tgz" - integrity sha512-1auuwmK+Rz13SJj36R+jqFPMJWyKEDd7lLSdOj4oJK0UTgGueSAtkrCvz9ewmgyU/P941Rv2fQwZJN8s6QruXw== - dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - "@babel/plugin-syntax-dynamic-import" "^7.8.3" - -"@babel/plugin-proposal-export-namespace-from@^7.18.9": - version "7.18.9" - resolved "https://registry.npmjs.org/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.18.9.tgz" - integrity sha512-k1NtHyOMvlDDFeb9G5PhUXuGj8m/wiwojgQVEhJ/fsVsMCpLyOP4h0uGEjYJKrRI+EVPlb5Jk+Gt9P97lOGwtA== - dependencies: - "@babel/helper-plugin-utils" "^7.18.9" - "@babel/plugin-syntax-export-namespace-from" "^7.8.3" - -"@babel/plugin-proposal-json-strings@^7.18.6": - version "7.18.6" - resolved "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.18.6.tgz" - integrity sha512-lr1peyn9kOdbYc0xr0OdHTZ5FMqS6Di+H0Fz2I/JwMzGmzJETNeOFq2pBySw6X/KFL5EWDjlJuMsUGRFb8fQgQ== - dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - "@babel/plugin-syntax-json-strings" "^7.8.3" - -"@babel/plugin-proposal-logical-assignment-operators@^7.18.9": - version "7.20.7" - resolved "https://registry.npmjs.org/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.20.7.tgz" - integrity sha512-y7C7cZgpMIjWlKE5T7eJwp+tnRYM89HmRvWM5EQuB5BoHEONjmQ8lSNmBUwOyy/GFRsohJED51YBF79hE1djug== - dependencies: - "@babel/helper-plugin-utils" "^7.20.2" - "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" - -"@babel/plugin-proposal-nullish-coalescing-operator@^7.18.6": - version "7.18.6" - resolved "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.18.6.tgz" - integrity sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA== - dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" - -"@babel/plugin-proposal-numeric-separator@^7.18.6": - version "7.18.6" - resolved "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.18.6.tgz" - integrity sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q== - dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - "@babel/plugin-syntax-numeric-separator" "^7.10.4" - -"@babel/plugin-proposal-object-rest-spread@^7.20.2": - version "7.20.7" - resolved "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.20.7.tgz" - integrity sha512-d2S98yCiLxDVmBmE8UjGcfPvNEUbA1U5q5WxaWFUGRzJSVAZqm5W6MbPct0jxnegUZ0niLeNX+IOzEs7wYg9Dg== - dependencies: - "@babel/compat-data" "^7.20.5" - "@babel/helper-compilation-targets" "^7.20.7" - "@babel/helper-plugin-utils" "^7.20.2" - "@babel/plugin-syntax-object-rest-spread" "^7.8.3" - "@babel/plugin-transform-parameters" "^7.20.7" - -"@babel/plugin-proposal-optional-catch-binding@^7.18.6": - version "7.18.6" - resolved "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.18.6.tgz" - integrity sha512-Q40HEhs9DJQyaZfUjjn6vE8Cv4GmMHCYuMGIWUnlxH6400VGxOuwWsPt4FxXxJkC/5eOzgn0z21M9gMT4MOhbw== - dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" - -"@babel/plugin-proposal-optional-chaining@^7.18.9", "@babel/plugin-proposal-optional-chaining@^7.20.7": - version "7.21.0" - resolved "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.21.0.tgz" - integrity sha512-p4zeefM72gpmEe2fkUr/OnOXpWEf8nAgk7ZYVqqfFiyIG7oFfVZcCrU64hWn5xp4tQ9LkV4bTIa5rD0KANpKNA== - dependencies: - "@babel/helper-plugin-utils" "^7.20.2" - "@babel/helper-skip-transparent-expression-wrappers" "^7.20.0" - "@babel/plugin-syntax-optional-chaining" "^7.8.3" - -"@babel/plugin-proposal-private-methods@^7.18.6": - version "7.18.6" - resolved "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.18.6.tgz" - integrity sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA== - dependencies: - "@babel/helper-create-class-features-plugin" "^7.18.6" - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/plugin-proposal-private-property-in-object@^7.18.6": - version "7.21.0" - resolved "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0.tgz" - integrity sha512-ha4zfehbJjc5MmXBlHec1igel5TJXXLDDRbuJ4+XT2TJcyD9/V1919BA8gMvsdHcNMBy4WBUBiRb3nw/EQUtBw== - dependencies: - "@babel/helper-annotate-as-pure" "^7.18.6" - "@babel/helper-create-class-features-plugin" "^7.21.0" - "@babel/helper-plugin-utils" "^7.20.2" - "@babel/plugin-syntax-private-property-in-object" "^7.14.5" - -"@babel/plugin-proposal-unicode-property-regex@^7.18.6", "@babel/plugin-proposal-unicode-property-regex@^7.4.4": - version "7.18.6" - resolved "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.18.6.tgz" - integrity sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.18.6" - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/plugin-syntax-async-generators@^7.8.4": - 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== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-class-properties@^7.12.13": - 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== - dependencies: - "@babel/helper-plugin-utils" "^7.12.13" - -"@babel/plugin-syntax-class-static-block@^7.14.5": - 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== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-syntax-dynamic-import@^7.8.3": - 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== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-export-namespace-from@^7.8.3": - 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== - dependencies: - "@babel/helper-plugin-utils" "^7.8.3" - -"@babel/plugin-syntax-import-assertions@^7.20.0": - version "7.20.0" - resolved "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.20.0.tgz" - integrity sha512-IUh1vakzNoWalR8ch/areW7qFopR2AEw03JlG7BbrDqmQ4X3q9uuipQwSGrUn7oGiemKjtSLDhNtQHzMHr1JdQ== - dependencies: - "@babel/helper-plugin-utils" "^7.19.0" - -"@babel/plugin-syntax-json-strings@^7.8.3": - 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== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-logical-assignment-operators@^7.10.4": - 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== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-syntax-nullish-coalescing-operator@^7.8.3": - 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== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-numeric-separator@^7.10.4": - 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== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-syntax-object-rest-spread@^7.8.3": - 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== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-optional-catch-binding@^7.8.3": - 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== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-optional-chaining@^7.8.3": - 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== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-private-property-in-object@^7.14.5": - 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== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-syntax-top-level-await@^7.14.5": - 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== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-transform-arrow-functions@^7.18.6": - version "7.20.7" - resolved "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.20.7.tgz" - integrity sha512-3poA5E7dzDomxj9WXWwuD6A5F3kc7VXwIJO+E+J8qtDtS+pXPAhrgEyh+9GBwBgPq1Z+bB+/JD60lp5jsN7JPQ== - dependencies: - "@babel/helper-plugin-utils" "^7.20.2" - -"@babel/plugin-transform-async-to-generator@7.20.7", "@babel/plugin-transform-async-to-generator@^7.18.6": - version "7.20.7" - resolved "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.20.7.tgz" - integrity sha512-Uo5gwHPT9vgnSXQxqGtpdufUiWp96gk7yiP4Mp5bm1QMkEmLXBO7PAGYbKoJ6DhAwiNkcHFBol/x5zZZkL/t0Q== - dependencies: - "@babel/helper-module-imports" "^7.18.6" - "@babel/helper-plugin-utils" "^7.20.2" - "@babel/helper-remap-async-to-generator" "^7.18.9" - -"@babel/plugin-transform-block-scoped-functions@^7.18.6": - version "7.18.6" - resolved "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.18.6.tgz" - integrity sha512-ExUcOqpPWnliRcPqves5HJcJOvHvIIWfuS4sroBUenPuMdmW+SMHDakmtS7qOo13sVppmUijqeTv7qqGsvURpQ== - dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/plugin-transform-block-scoping@^7.20.2": - version "7.21.0" - resolved "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.21.0.tgz" - integrity sha512-Mdrbunoh9SxwFZapeHVrwFmri16+oYotcZysSzhNIVDwIAb1UV+kvnxULSYq9J3/q5MDG+4X6w8QVgD1zhBXNQ== - dependencies: - "@babel/helper-plugin-utils" "^7.20.2" - -"@babel/plugin-transform-classes@^7.20.2": - version "7.21.0" - resolved "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.21.0.tgz" - integrity sha512-RZhbYTCEUAe6ntPehC4hlslPWosNHDox+vAs4On/mCLRLfoDVHf6hVEd7kuxr1RnHwJmxFfUM3cZiZRmPxJPXQ== - dependencies: - "@babel/helper-annotate-as-pure" "^7.18.6" - "@babel/helper-compilation-targets" "^7.20.7" - "@babel/helper-environment-visitor" "^7.18.9" - "@babel/helper-function-name" "^7.21.0" - "@babel/helper-optimise-call-expression" "^7.18.6" - "@babel/helper-plugin-utils" "^7.20.2" - "@babel/helper-replace-supers" "^7.20.7" - "@babel/helper-split-export-declaration" "^7.18.6" - globals "^11.1.0" - -"@babel/plugin-transform-computed-properties@^7.18.9": - version "7.20.7" - resolved "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.20.7.tgz" - integrity sha512-Lz7MvBK6DTjElHAmfu6bfANzKcxpyNPeYBGEafyA6E5HtRpjpZwU+u7Qrgz/2OR0z+5TvKYbPdphfSaAcZBrYQ== - dependencies: - "@babel/helper-plugin-utils" "^7.20.2" - "@babel/template" "^7.20.7" - -"@babel/plugin-transform-destructuring@^7.20.2": - version "7.21.3" - resolved "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.21.3.tgz" - integrity sha512-bp6hwMFzuiE4HqYEyoGJ/V2LeIWn+hLVKc4pnj++E5XQptwhtcGmSayM029d/j2X1bPKGTlsyPwAubuU22KhMA== - dependencies: - "@babel/helper-plugin-utils" "^7.20.2" - -"@babel/plugin-transform-dotall-regex@^7.18.6", "@babel/plugin-transform-dotall-regex@^7.4.4": - version "7.18.6" - resolved "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.18.6.tgz" - integrity sha512-6S3jpun1eEbAxq7TdjLotAsl4WpQI9DxfkycRcKrjhQYzU87qpXdknpBg/e+TdcMehqGnLFi7tnFUBR02Vq6wg== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.18.6" - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/plugin-transform-duplicate-keys@^7.18.9": - version "7.18.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.18.9.tgz" - integrity sha512-d2bmXCtZXYc59/0SanQKbiWINadaJXqtvIQIzd4+hNwkWBgyCd5F/2t1kXoUdvPMrxzPvhK6EMQRROxsue+mfw== - dependencies: - "@babel/helper-plugin-utils" "^7.18.9" - -"@babel/plugin-transform-exponentiation-operator@^7.18.6": - version "7.18.6" - resolved "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.18.6.tgz" - integrity sha512-wzEtc0+2c88FVR34aQmiz56dxEkxr2g8DQb/KfaFa1JYXOFVsbhvAonFN6PwVWj++fKmku8NP80plJ5Et4wqHw== - dependencies: - "@babel/helper-builder-binary-assignment-operator-visitor" "^7.18.6" - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/plugin-transform-for-of@^7.18.8": - version "7.21.0" - resolved "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.21.0.tgz" - integrity sha512-LlUYlydgDkKpIY7mcBWvyPPmMcOphEyYA27Ef4xpbh1IiDNLr0kZsos2nf92vz3IccvJI25QUwp86Eo5s6HmBQ== - dependencies: - "@babel/helper-plugin-utils" "^7.20.2" - -"@babel/plugin-transform-function-name@^7.18.9": - version "7.18.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.18.9.tgz" - integrity sha512-WvIBoRPaJQ5yVHzcnJFor7oS5Ls0PYixlTYE63lCj2RtdQEl15M68FXQlxnG6wdraJIXRdR7KI+hQ7q/9QjrCQ== - dependencies: - "@babel/helper-compilation-targets" "^7.18.9" - "@babel/helper-function-name" "^7.18.9" - "@babel/helper-plugin-utils" "^7.18.9" - -"@babel/plugin-transform-literals@^7.18.9": - version "7.18.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.18.9.tgz" - integrity sha512-IFQDSRoTPnrAIrI5zoZv73IFeZu2dhu6irxQjY9rNjTT53VmKg9fenjvoiOWOkJ6mm4jKVPtdMzBY98Fp4Z4cg== - dependencies: - "@babel/helper-plugin-utils" "^7.18.9" - -"@babel/plugin-transform-member-expression-literals@^7.18.6": - version "7.18.6" - resolved "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.18.6.tgz" - integrity sha512-qSF1ihLGO3q+/g48k85tUjD033C29TNTVB2paCwZPVmOsjn9pClvYYrM2VeJpBY2bcNkuny0YUyTNRyRxJ54KA== - dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/plugin-transform-modules-amd@^7.19.6": - version "7.20.11" - resolved "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.20.11.tgz" - integrity sha512-NuzCt5IIYOW0O30UvqktzHYR2ud5bOWbY0yaxWZ6G+aFzOMJvrs5YHNikrbdaT15+KNO31nPOy5Fim3ku6Zb5g== - dependencies: - "@babel/helper-module-transforms" "^7.20.11" - "@babel/helper-plugin-utils" "^7.20.2" - -"@babel/plugin-transform-modules-commonjs@^7.19.6": - version "7.21.2" - resolved "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.21.2.tgz" - integrity sha512-Cln+Yy04Gxua7iPdj6nOV96smLGjpElir5YwzF0LBPKoPlLDNJePNlrGGaybAJkd0zKRnOVXOgizSqPYMNYkzA== - dependencies: - "@babel/helper-module-transforms" "^7.21.2" - "@babel/helper-plugin-utils" "^7.20.2" - "@babel/helper-simple-access" "^7.20.2" - -"@babel/plugin-transform-modules-systemjs@^7.19.6": - version "7.20.11" - resolved "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.20.11.tgz" - integrity sha512-vVu5g9BPQKSFEmvt2TA4Da5N+QVS66EX21d8uoOihC+OCpUoGvzVsXeqFdtAEfVa5BILAeFt+U7yVmLbQnAJmw== - dependencies: - "@babel/helper-hoist-variables" "^7.18.6" - "@babel/helper-module-transforms" "^7.20.11" - "@babel/helper-plugin-utils" "^7.20.2" - "@babel/helper-validator-identifier" "^7.19.1" - -"@babel/plugin-transform-modules-umd@^7.18.6": - version "7.18.6" - resolved "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.18.6.tgz" - integrity sha512-dcegErExVeXcRqNtkRU/z8WlBLnvD4MRnHgNs3MytRO1Mn1sHRyhbcpYbVMGclAqOjdW+9cfkdZno9dFdfKLfQ== - dependencies: - "@babel/helper-module-transforms" "^7.18.6" - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/plugin-transform-named-capturing-groups-regex@^7.19.1": - version "7.20.5" - resolved "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.20.5.tgz" - integrity sha512-mOW4tTzi5iTLnw+78iEq3gr8Aoq4WNRGpmSlrogqaiCBoR1HFhpU4JkpQFOHfeYx3ReVIFWOQJS4aZBRvuZ6mA== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.20.5" - "@babel/helper-plugin-utils" "^7.20.2" - -"@babel/plugin-transform-new-target@^7.18.6": - version "7.18.6" - resolved "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.18.6.tgz" - integrity sha512-DjwFA/9Iu3Z+vrAn+8pBUGcjhxKguSMlsFqeCKbhb9BAV756v0krzVK04CRDi/4aqmk8BsHb4a/gFcaA5joXRw== - dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/plugin-transform-object-super@^7.18.6": - version "7.18.6" - resolved "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.18.6.tgz" - integrity sha512-uvGz6zk+pZoS1aTZrOvrbj6Pp/kK2mp45t2B+bTDre2UgsZZ8EZLSJtUg7m/no0zOJUWgFONpB7Zv9W2tSaFlA== - dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - "@babel/helper-replace-supers" "^7.18.6" - -"@babel/plugin-transform-parameters@^7.20.1", "@babel/plugin-transform-parameters@^7.20.7": - version "7.21.3" - resolved "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.21.3.tgz" - integrity sha512-Wxc+TvppQG9xWFYatvCGPvZ6+SIUxQ2ZdiBP+PHYMIjnPXD+uThCshaz4NZOnODAtBjjcVQQ/3OKs9LW28purQ== - dependencies: - "@babel/helper-plugin-utils" "^7.20.2" - -"@babel/plugin-transform-property-literals@^7.18.6": - version "7.18.6" - resolved "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.18.6.tgz" - integrity sha512-cYcs6qlgafTud3PAzrrRNbQtfpQ8+y/+M5tKmksS9+M1ckbH6kzY8MrexEM9mcA6JDsukE19iIRvAyYl463sMg== - dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/plugin-transform-regenerator@^7.18.6": - version "7.20.5" - resolved "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.20.5.tgz" - integrity sha512-kW/oO7HPBtntbsahzQ0qSE3tFvkFwnbozz3NWFhLGqH75vLEg+sCGngLlhVkePlCs3Jv0dBBHDzCHxNiFAQKCQ== - dependencies: - "@babel/helper-plugin-utils" "^7.20.2" - regenerator-transform "^0.15.1" - -"@babel/plugin-transform-reserved-words@^7.18.6": - version "7.18.6" - resolved "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.18.6.tgz" - integrity sha512-oX/4MyMoypzHjFrT1CdivfKZ+XvIPMFXwwxHp/r0Ddy2Vuomt4HDFGmft1TAY2yiTKiNSsh3kjBAzcM8kSdsjA== - dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/plugin-transform-runtime@7.19.6": - version "7.19.6" - resolved "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.19.6.tgz" - integrity sha512-PRH37lz4JU156lYFW1p8OxE5i7d6Sl/zV58ooyr+q1J1lnQPyg5tIiXlIwNVhJaY4W3TmOtdc8jqdXQcB1v5Yw== - dependencies: - "@babel/helper-module-imports" "^7.18.6" - "@babel/helper-plugin-utils" "^7.19.0" - babel-plugin-polyfill-corejs2 "^0.3.3" - babel-plugin-polyfill-corejs3 "^0.6.0" - babel-plugin-polyfill-regenerator "^0.4.1" - semver "^6.3.0" - -"@babel/plugin-transform-shorthand-properties@^7.18.6": - version "7.18.6" - resolved "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.18.6.tgz" - integrity sha512-eCLXXJqv8okzg86ywZJbRn19YJHU4XUa55oz2wbHhaQVn/MM+XhukiT7SYqp/7o00dg52Rj51Ny+Ecw4oyoygw== - dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/plugin-transform-spread@^7.19.0": - version "7.20.7" - resolved "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.20.7.tgz" - integrity sha512-ewBbHQ+1U/VnH1fxltbJqDeWBU1oNLG8Dj11uIv3xVf7nrQu0bPGe5Rf716r7K5Qz+SqtAOVswoVunoiBtGhxw== - dependencies: - "@babel/helper-plugin-utils" "^7.20.2" - "@babel/helper-skip-transparent-expression-wrappers" "^7.20.0" - -"@babel/plugin-transform-sticky-regex@^7.18.6": - version "7.18.6" - resolved "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.18.6.tgz" - integrity sha512-kfiDrDQ+PBsQDO85yj1icueWMfGfJFKN1KCkndygtu/C9+XUfydLC8Iv5UYJqRwy4zk8EcplRxEOeLyjq1gm6Q== - dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/plugin-transform-template-literals@^7.18.9": - version "7.18.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.18.9.tgz" - integrity sha512-S8cOWfT82gTezpYOiVaGHrCbhlHgKhQt8XH5ES46P2XWmX92yisoZywf5km75wv5sYcXDUCLMmMxOLCtthDgMA== - dependencies: - "@babel/helper-plugin-utils" "^7.18.9" - -"@babel/plugin-transform-typeof-symbol@^7.18.9": - version "7.18.9" - resolved "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.18.9.tgz" - integrity sha512-SRfwTtF11G2aemAZWivL7PD+C9z52v9EvMqH9BuYbabyPuKUvSWks3oCg6041pT925L4zVFqaVBeECwsmlguEw== - dependencies: - "@babel/helper-plugin-utils" "^7.18.9" - -"@babel/plugin-transform-unicode-escapes@^7.18.10": - version "7.18.10" - resolved "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.18.10.tgz" - integrity sha512-kKAdAI+YzPgGY/ftStBFXTI1LZFju38rYThnfMykS+IXy8BVx+res7s2fxf1l8I35DV2T97ezo6+SGrXz6B3iQ== - dependencies: - "@babel/helper-plugin-utils" "^7.18.9" - -"@babel/plugin-transform-unicode-regex@^7.18.6": - version "7.18.6" - resolved "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.18.6.tgz" - integrity sha512-gE7A6Lt7YLnNOL3Pb9BNeZvi+d8l7tcRrG4+pwJjK9hD2xX4mEvjlQW60G9EEmfXVYRPv9VRQcyegIVHCql/AA== - dependencies: - "@babel/helper-create-regexp-features-plugin" "^7.18.6" - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/preset-env@7.20.2": - version "7.20.2" - resolved "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.20.2.tgz" - integrity sha512-1G0efQEWR1EHkKvKHqbG+IN/QdgwfByUpM5V5QroDzGV2t3S/WXNQd693cHiHTlCFMpr9B6FkPFXDA2lQcKoDg== - dependencies: - "@babel/compat-data" "^7.20.1" - "@babel/helper-compilation-targets" "^7.20.0" - "@babel/helper-plugin-utils" "^7.20.2" - "@babel/helper-validator-option" "^7.18.6" - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression" "^7.18.6" - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.18.9" - "@babel/plugin-proposal-async-generator-functions" "^7.20.1" - "@babel/plugin-proposal-class-properties" "^7.18.6" - "@babel/plugin-proposal-class-static-block" "^7.18.6" - "@babel/plugin-proposal-dynamic-import" "^7.18.6" - "@babel/plugin-proposal-export-namespace-from" "^7.18.9" - "@babel/plugin-proposal-json-strings" "^7.18.6" - "@babel/plugin-proposal-logical-assignment-operators" "^7.18.9" - "@babel/plugin-proposal-nullish-coalescing-operator" "^7.18.6" - "@babel/plugin-proposal-numeric-separator" "^7.18.6" - "@babel/plugin-proposal-object-rest-spread" "^7.20.2" - "@babel/plugin-proposal-optional-catch-binding" "^7.18.6" - "@babel/plugin-proposal-optional-chaining" "^7.18.9" - "@babel/plugin-proposal-private-methods" "^7.18.6" - "@babel/plugin-proposal-private-property-in-object" "^7.18.6" - "@babel/plugin-proposal-unicode-property-regex" "^7.18.6" - "@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.20.0" - "@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-transform-arrow-functions" "^7.18.6" - "@babel/plugin-transform-async-to-generator" "^7.18.6" - "@babel/plugin-transform-block-scoped-functions" "^7.18.6" - "@babel/plugin-transform-block-scoping" "^7.20.2" - "@babel/plugin-transform-classes" "^7.20.2" - "@babel/plugin-transform-computed-properties" "^7.18.9" - "@babel/plugin-transform-destructuring" "^7.20.2" - "@babel/plugin-transform-dotall-regex" "^7.18.6" - "@babel/plugin-transform-duplicate-keys" "^7.18.9" - "@babel/plugin-transform-exponentiation-operator" "^7.18.6" - "@babel/plugin-transform-for-of" "^7.18.8" - "@babel/plugin-transform-function-name" "^7.18.9" - "@babel/plugin-transform-literals" "^7.18.9" - "@babel/plugin-transform-member-expression-literals" "^7.18.6" - "@babel/plugin-transform-modules-amd" "^7.19.6" - "@babel/plugin-transform-modules-commonjs" "^7.19.6" - "@babel/plugin-transform-modules-systemjs" "^7.19.6" - "@babel/plugin-transform-modules-umd" "^7.18.6" - "@babel/plugin-transform-named-capturing-groups-regex" "^7.19.1" - "@babel/plugin-transform-new-target" "^7.18.6" - "@babel/plugin-transform-object-super" "^7.18.6" - "@babel/plugin-transform-parameters" "^7.20.1" - "@babel/plugin-transform-property-literals" "^7.18.6" - "@babel/plugin-transform-regenerator" "^7.18.6" - "@babel/plugin-transform-reserved-words" "^7.18.6" - "@babel/plugin-transform-shorthand-properties" "^7.18.6" - "@babel/plugin-transform-spread" "^7.19.0" - "@babel/plugin-transform-sticky-regex" "^7.18.6" - "@babel/plugin-transform-template-literals" "^7.18.9" - "@babel/plugin-transform-typeof-symbol" "^7.18.9" - "@babel/plugin-transform-unicode-escapes" "^7.18.10" - "@babel/plugin-transform-unicode-regex" "^7.18.6" - "@babel/preset-modules" "^0.1.5" - "@babel/types" "^7.20.2" - babel-plugin-polyfill-corejs2 "^0.3.3" - babel-plugin-polyfill-corejs3 "^0.6.0" - babel-plugin-polyfill-regenerator "^0.4.1" - core-js-compat "^3.25.1" - semver "^6.3.0" - -"@babel/preset-modules@^0.1.5": - version "0.1.5" - resolved "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.5.tgz" - integrity sha512-A57th6YRG7oR3cq/yt/Y84MvGgE0eJG2F1JLhKuyG+jFxEgrd/HAMJatiFtmOiZurz+0DkrvbheCLaV5f2JfjA== - dependencies: - "@babel/helper-plugin-utils" "^7.0.0" - "@babel/plugin-proposal-unicode-property-regex" "^7.4.4" - "@babel/plugin-transform-dotall-regex" "^7.4.4" - "@babel/types" "^7.4.4" - esutils "^2.0.2" - -"@babel/regjsgen@^0.8.0": - version "0.8.0" - resolved "https://registry.npmjs.org/@babel/regjsgen/-/regjsgen-0.8.0.tgz" - integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA== - -"@babel/runtime@7.20.13": - version "7.20.13" - resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.13.tgz" - integrity sha512-gt3PKXs0DBoL9xCvOIIZ2NEqAGZqHjAnmVbfQtB620V0uReIQutpel14KcneZuer7UioY8ALKZ7iocavvzTNFA== - dependencies: - regenerator-runtime "^0.13.11" - -"@babel/runtime@7.21.0": - version "7.21.0" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.21.0.tgz#5b55c9d394e5fcf304909a8b00c07dc217b56673" - integrity sha512-xwII0//EObnq89Ji5AKYQaRYiW/nZ3llSv29d49IuxPhKbtJoLP+9QUUZ4nVragQVtaVGeZrpB+ZtG/Pdy/POw== - dependencies: - regenerator-runtime "^0.13.11" - -"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.12.0", "@babel/runtime@^7.14.5", "@babel/runtime@^7.15.4", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.3", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": - version "7.17.2" - resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.17.2.tgz" - integrity sha512-hzeyJyMA1YGdJTuWU0e/j4wKXrU4OMFvY2MSlaI9B7VQb0r5cxTE3EAIS2Q7Tn2RIcDkRvTA/v2JsAEhxe99uw== - dependencies: - regenerator-runtime "^0.13.4" - -"@babel/template@7.20.7", "@babel/template@^7.18.10", "@babel/template@^7.18.6", "@babel/template@^7.20.7": - version "7.20.7" - resolved "https://registry.npmjs.org/@babel/template/-/template-7.20.7.tgz" - integrity sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw== - dependencies: - "@babel/code-frame" "^7.18.6" - "@babel/parser" "^7.20.7" - "@babel/types" "^7.20.7" - -"@babel/traverse@^7.10.3", "@babel/traverse@^7.18.9", "@babel/traverse@^7.19.3", "@babel/traverse@^7.20.12", "@babel/traverse@^7.20.5", "@babel/traverse@^7.20.7", "@babel/traverse@^7.21.0", "@babel/traverse@^7.21.2": - version "7.21.4" - resolved "https://registry.npmjs.org/@babel/traverse/-/traverse-7.21.4.tgz" - integrity sha512-eyKrRHKdyZxqDm+fV1iqL9UAHMoIg0nDaGqfIOd8rKH17m5snv7Gn4qgjBoFfLz9APvjFU/ICT00NVCv1Epp8Q== - dependencies: - "@babel/code-frame" "^7.21.4" - "@babel/generator" "^7.21.4" - "@babel/helper-environment-visitor" "^7.18.9" - "@babel/helper-function-name" "^7.21.0" - "@babel/helper-hoist-variables" "^7.18.6" - "@babel/helper-split-export-declaration" "^7.18.6" - "@babel/parser" "^7.21.4" - "@babel/types" "^7.21.4" - debug "^4.1.0" - globals "^11.1.0" - -"@babel/types@^7.10.3", "@babel/types@^7.18.6", "@babel/types@^7.18.9", "@babel/types@^7.19.3", "@babel/types@^7.20.0", "@babel/types@^7.20.2", "@babel/types@^7.20.5", "@babel/types@^7.20.7", "@babel/types@^7.21.0", "@babel/types@^7.21.2", "@babel/types@^7.21.4", "@babel/types@^7.4.4": - version "7.21.4" - resolved "https://registry.npmjs.org/@babel/types/-/types-7.21.4.tgz" - integrity sha512-rU2oY501qDxE8Pyo7i/Orqma4ziCOrby0/9mvbDUGEfvZjb279Nk9k19e2fiCxHbRRpY2ZyrgW1eq22mvmOIzA== - dependencies: - "@babel/helper-string-parser" "^7.19.4" - "@babel/helper-validator-identifier" "^7.19.1" - to-fast-properties "^2.0.0" - -"@colors/colors@1.5.0": - version "1.5.0" - resolved "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz" - integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ== - -"@cspotcode/source-map-consumer@0.8.0": - version "0.8.0" - resolved "https://registry.yarnpkg.com/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz#33bf4b7b39c178821606f669bbc447a6a629786b" - integrity sha512-41qniHzTU8yAGbCp04ohlmSrZf8bkf/iJsl3V0dRGsQN/5GFfx+LbCSsCpp2gqrqjTVg/K6O8ycoV35JIwAzAg== - -"@cspotcode/source-map-support@0.6.1": - version "0.6.1" - resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.6.1.tgz#118511f316e2e87ee4294761868e254d3da47960" - integrity sha512-DX3Z+T5dt1ockmPdobJS/FAsQPW4V4SrWEhD2iYQT2Cb2tQsiMnYxrcUH9By/Z3B+v0S5LMBkQtV/XOBbpLEOg== - dependencies: - "@cspotcode/source-map-consumer" "0.8.0" - -"@csstools/postcss-cascade-layers@^1.1.1": - 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== - dependencies: - "@csstools/selector-specificity" "^2.0.2" - postcss-selector-parser "^6.0.10" - -"@csstools/postcss-color-function@^1.1.1": - version "1.1.1" - resolved "https://registry.npmjs.org/@csstools/postcss-color-function/-/postcss-color-function-1.1.1.tgz" - integrity sha512-Bc0f62WmHdtRDjf5f3e2STwRAl89N2CLb+9iAwzrv4L2hncrbDwnQD9PCq0gtAt7pOI2leIV08HIBUd4jxD8cw== - dependencies: - "@csstools/postcss-progressive-custom-properties" "^1.1.0" - postcss-value-parser "^4.2.0" - -"@csstools/postcss-font-format-keywords@^1.0.1": - 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== - dependencies: - postcss-value-parser "^4.2.0" - -"@csstools/postcss-hwb-function@^1.0.2": - 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== - dependencies: - postcss-value-parser "^4.2.0" - -"@csstools/postcss-ic-unit@^1.0.1": - 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== - dependencies: - "@csstools/postcss-progressive-custom-properties" "^1.1.0" - postcss-value-parser "^4.2.0" - -"@csstools/postcss-is-pseudo-class@^2.0.7": - 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== - dependencies: - "@csstools/selector-specificity" "^2.0.0" - postcss-selector-parser "^6.0.10" - -"@csstools/postcss-nested-calc@^1.0.0": - 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== - dependencies: - postcss-value-parser "^4.2.0" - -"@csstools/postcss-normalize-display-values@^1.0.1": - 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== - dependencies: - postcss-value-parser "^4.2.0" - -"@csstools/postcss-oklab-function@^1.1.1": - 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== - dependencies: - "@csstools/postcss-progressive-custom-properties" "^1.1.0" - postcss-value-parser "^4.2.0" - -"@csstools/postcss-progressive-custom-properties@^1.1.0", "@csstools/postcss-progressive-custom-properties@^1.3.0": - 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== - dependencies: - postcss-value-parser "^4.2.0" - -"@csstools/postcss-stepped-value-functions@^1.0.1": - 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== - dependencies: - postcss-value-parser "^4.2.0" - -"@csstools/postcss-text-decoration-shorthand@^1.0.0": - 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== - dependencies: - postcss-value-parser "^4.2.0" - -"@csstools/postcss-trigonometric-functions@^1.0.2": - 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== - dependencies: - postcss-value-parser "^4.2.0" - -"@csstools/postcss-unset-value@^1.0.2": - 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== - -"@csstools/selector-specificity@^2.0.0", "@csstools/selector-specificity@^2.0.2": - version "2.2.0" - resolved "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-2.2.0.tgz" - integrity sha512-+OJ9konv95ClSTOJCmMZqpd5+YGsB2S+x6w3E1oaM8UuR5j8nTNHYSz8c9BEPGDOCMQYIEEGlVPj/VY64iTbGw== - -"@cypress/request@^2.88.10": - version "2.88.11" - resolved "https://registry.npmjs.org/@cypress/request/-/request-2.88.11.tgz" - integrity sha512-M83/wfQ1EkspjkE2lNWNV5ui2Cv7UCv1swW1DqljahbzLVWltcsexQh8jYtuS/vzFXP+HySntGM83ZXA9fn17w== - 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 "~2.3.2" - http-signature "~1.3.6" - 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.10.3" - safe-buffer "^5.1.2" - tough-cookie "~2.5.0" - tunnel-agent "^0.6.0" - uuid "^8.3.2" - -"@cypress/schematic@^1.5.0": - version "1.7.0" - resolved "https://registry.npmjs.org/@cypress/schematic/-/schematic-1.7.0.tgz" - integrity sha512-CouQrVlZ+uHVVBQtmNoMYU9LyoSAmQTOLDpVjrdTdMPpJH1mWnHCL5OCMt+FZLR+43KRiWEvDUjNqSza11oGsQ== - 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" - -"@cypress/xvfb@^1.2.4": - version "1.2.4" - resolved "https://registry.npmjs.org/@cypress/xvfb/-/xvfb-1.2.4.tgz" - integrity sha512-skbBzPggOVYCbnGgV+0dmBdW/s77ZkAOXIC1knS8NagwDjBrNC1LuXtQJeiN6l+m7lzmHtaoUw/ctJKdqkG57Q== - dependencies: - debug "^3.1.0" - lodash.once "^4.1.1" - -"@discoveryjs/json-ext@0.5.7", "@discoveryjs/json-ext@^0.5.0": - version "0.5.7" - resolved "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz" - integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw== - -"@edsilv/http-status-codes@^1.0.3": - version "1.0.3" - resolved "https://registry.npmjs.org/@edsilv/http-status-codes/-/http-status-codes-1.0.3.tgz" - integrity sha512-HLK2FS5sZqxPqD53D6hhZxC6C8THTVwlyZDZ7J0iWsrB8JmMA69m/CQuNKZc1kki9WSVeck2fXna26NL0SE7cg== - -"@emotion/hash@^0.8.0": - version "0.8.0" - resolved "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz" - integrity sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow== - -"@es-joy/jsdoccomment@~0.36.1": - version "0.36.1" - resolved "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.36.1.tgz" - integrity sha512-922xqFsTpHs6D0BUiG4toiyPOMc8/jafnWKxz1KWgS4XzKPy2qXf1Pe6UFuNSCQqt6tOuhAWXBNuuyUhJmw9Vg== - dependencies: - comment-parser "1.3.1" - esquery "^1.4.0" - jsdoc-type-pratt-parser "~3.1.0" - -"@esbuild/android-arm64@0.17.8": - version "0.17.8" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.17.8.tgz#b3d5b65a3b2e073a6c7ee36b1f3c30c8f000315b" - integrity sha512-oa/N5j6v1svZQs7EIRPqR8f+Bf8g6HBDjD/xHC02radE/NjKHK7oQmtmLxPs1iVwYyvE+Kolo6lbpfEQ9xnhxQ== - -"@esbuild/android-arm@0.17.8": - version "0.17.8" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.17.8.tgz#c41e496af541e175369d48164d0cf01a5f656cf6" - integrity sha512-0/rb91GYKhrtbeglJXOhAv9RuYimgI8h623TplY2X+vA4EXnk3Zj1fXZreJ0J3OJJu1bwmb0W7g+2cT/d8/l/w== - -"@esbuild/android-x64@0.17.8": - version "0.17.8" - resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.17.8.tgz#080fa67c29be77f5a3ca5ee4cc78d5bf927e3a3b" - integrity sha512-bTliMLqD7pTOoPg4zZkXqCDuzIUguEWLpeqkNfC41ODBHwoUgZ2w5JBeYimv4oP6TDVocoYmEhZrCLQTrH89bg== - -"@esbuild/darwin-arm64@0.17.8": - version "0.17.8" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.17.8.tgz#053622bf9a82f43d5c075b7818e02618f7b4a397" - integrity sha512-ghAbV3ia2zybEefXRRm7+lx8J/rnupZT0gp9CaGy/3iolEXkJ6LYRq4IpQVI9zR97ID80KJVoUlo3LSeA/sMAg== - -"@esbuild/darwin-x64@0.17.8": - version "0.17.8" - resolved "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.8.tgz" - integrity sha512-n5WOpyvZ9TIdv2V1K3/iIkkJeKmUpKaCTdun9buhGRWfH//osmUjlv4Z5mmWdPWind/VGcVxTHtLfLCOohsOXw== - -"@esbuild/freebsd-arm64@0.17.8": - version "0.17.8" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.8.tgz#e6738d0081ba0721a5c6c674e84c6e7fcea61989" - integrity sha512-a/SATTaOhPIPFWvHZDoZYgxaZRVHn0/LX1fHLGfZ6C13JqFUZ3K6SMD6/HCtwOQ8HnsNaEeokdiDSFLuizqv5A== - -"@esbuild/freebsd-x64@0.17.8": - version "0.17.8" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.17.8.tgz#1855e562f2b730f4483f6e94086e9e2597feb4c3" - integrity sha512-xpFJb08dfXr5+rZc4E+ooZmayBW6R3q59daCpKZ/cDU96/kvDM+vkYzNeTJCGd8rtO6fHWMq5Rcv/1cY6p6/0Q== - -"@esbuild/linux-arm64@0.17.8": - version "0.17.8" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.17.8.tgz#481da38952721a3fdb77c17a36ceaacc4270b5c5" - integrity sha512-v3iwDQuDljLTxpsqQDl3fl/yihjPAyOguxuloON9kFHYwopeJEf1BkDXODzYyXEI19gisEsQlG1bM65YqKSIww== - -"@esbuild/linux-arm@0.17.8": - version "0.17.8" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.17.8.tgz#18127072b270bb6321c6d11be20bfd30e0d6ad17" - integrity sha512-6Ij8gfuGszcEwZpi5jQIJCVIACLS8Tz2chnEBfYjlmMzVsfqBP1iGmHQPp7JSnZg5xxK9tjCc+pJ2WtAmPRFVA== - -"@esbuild/linux-ia32@0.17.8": - version "0.17.8" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.17.8.tgz#ee400af7b3bc69e8ca2e593ca35156ffb9abd54f" - integrity sha512-8svILYKhE5XetuFk/B6raFYIyIqydQi+GngEXJgdPdI7OMKUbSd7uzR02wSY4kb53xBrClLkhH4Xs8P61Q2BaA== - -"@esbuild/linux-loong64@0.17.8": - version "0.17.8" - resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.17.8.tgz#8c509d8a454693d39824b83b3f66c400872fce82" - integrity sha512-B6FyMeRJeV0NpyEOYlm5qtQfxbdlgmiGdD+QsipzKfFky0K5HW5Td6dyK3L3ypu1eY4kOmo7wW0o94SBqlqBSA== - -"@esbuild/linux-mips64el@0.17.8": - version "0.17.8" - resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.17.8.tgz#f2b0d36e63fb26bc3f95b203b6a80638292101ca" - integrity sha512-CCb67RKahNobjm/eeEqeD/oJfJlrWyw29fgiyB6vcgyq97YAf3gCOuP6qMShYSPXgnlZe/i4a8WFHBw6N8bYAA== - -"@esbuild/linux-ppc64@0.17.8": - version "0.17.8" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.17.8.tgz#1e628be003e036e90423716028cc884fe5ba25bd" - integrity sha512-bytLJOi55y55+mGSdgwZ5qBm0K9WOCh0rx+vavVPx+gqLLhxtSFU0XbeYy/dsAAD6xECGEv4IQeFILaSS2auXw== - -"@esbuild/linux-riscv64@0.17.8": - version "0.17.8" - resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.17.8.tgz#419a815cb4c3fb9f1b78ef5295f5b48b8bf6427a" - integrity sha512-2YpRyQJmKVBEHSBLa8kBAtbhucaclb6ex4wchfY0Tj3Kg39kpjeJ9vhRU7x4mUpq8ISLXRXH1L0dBYjAeqzZAw== - -"@esbuild/linux-s390x@0.17.8": - version "0.17.8" - resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.17.8.tgz#291c49ae5c3d11d226352755c0835911fe1a9e5c" - integrity sha512-QgbNY/V3IFXvNf11SS6exkpVcX0LJcob+0RWCgV9OiDAmVElnxciHIisoSix9uzYzScPmS6dJFbZULdSAEkQVw== - -"@esbuild/linux-x64@0.17.8": - version "0.17.8" - resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.17.8.tgz#03199d91c76faf80bd54104f5cbf0a489bc39f6a" - integrity sha512-mM/9S0SbAFDBc4OPoyP6SEOo5324LpUxdpeIUUSrSTOfhHU9hEfqRngmKgqILqwx/0DVJBzeNW7HmLEWp9vcOA== - -"@esbuild/netbsd-x64@0.17.8": - version "0.17.8" - resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.17.8.tgz#b436d767e1b21852f9ed212e2bb57f77203b0ae2" - integrity sha512-eKUYcWaWTaYr9zbj8GertdVtlt1DTS1gNBWov+iQfWuWyuu59YN6gSEJvFzC5ESJ4kMcKR0uqWThKUn5o8We6Q== - -"@esbuild/openbsd-x64@0.17.8": - version "0.17.8" - resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.17.8.tgz#d1481d8539e21d4729cd04a0450a26c2c8789e89" - integrity sha512-Vc9J4dXOboDyMXKD0eCeW0SIeEzr8K9oTHJU+Ci1mZc5njPfhKAqkRt3B/fUNU7dP+mRyralPu8QUkiaQn7iIg== - -"@esbuild/sunos-x64@0.17.8": - version "0.17.8" - resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.17.8.tgz#2cfb8126e079b2c00fd1bf095541e9f5c47877e4" - integrity sha512-0xvOTNuPXI7ft1LYUgiaXtpCEjp90RuBBYovdd2lqAFxje4sEucurg30M1WIm03+3jxByd3mfo+VUmPtRSVuOw== - -"@esbuild/win32-arm64@0.17.8": - version "0.17.8" - resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.17.8.tgz#7c6ecfd097ca23b82119753bf7072bbaefe51e3a" - integrity sha512-G0JQwUI5WdEFEnYNKzklxtBheCPkuDdu1YrtRrjuQv30WsYbkkoixKxLLv8qhJmNI+ATEWquZe/N0d0rpr55Mg== - -"@esbuild/win32-ia32@0.17.8": - version "0.17.8" - resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.17.8.tgz#cffec63c3cb0ef8563a04df4e09fa71056171d00" - integrity sha512-Fqy63515xl20OHGFykjJsMnoIWS+38fqfg88ClvPXyDbLtgXal2DTlhb1TfTX34qWi3u4I7Cq563QcHpqgLx8w== - -"@esbuild/win32-x64@0.17.8": - version "0.17.8" - resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.17.8.tgz#200a0965cf654ac28b971358ecdca9cc5b44c335" - integrity sha512-1iuezdyDNngPnz8rLRDO2C/ZZ/emJLb72OsZeqQ6gL6Avko/XCXZw+NuxBSNhBAP13Hie418V7VMt9et1FMvpg== - -"@eslint-community/eslint-utils@^4.2.0": - version "4.4.0" - resolved "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz" - integrity sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA== - dependencies: - eslint-visitor-keys "^3.3.0" - -"@eslint-community/regexpp@^4.4.0": - version "4.5.0" - resolved "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.5.0.tgz" - integrity sha512-vITaYzIcNmjn5tF5uxcZ/ft7/RXGrMUIS9HalWckEOF6ESiwXKoMzAQf2UW0aVd6rnOeExTJVd5hmWXucBKGXQ== - -"@eslint/eslintrc@^2.0.2": - version "2.0.2" - resolved "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.0.2.tgz" - integrity sha512-3W4f5tDUra+pA+FzgugqL2pRimUTDJWKr7BINqOpkZrC0uYI0NIc0/JFgBROCU07HR6GieA5m3/rsPIhDmCXTQ== - dependencies: - ajv "^6.12.4" - debug "^4.3.2" - espree "^9.5.1" - 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" - -"@eslint/js@8.39.0": - version "8.39.0" - resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.39.0.tgz#58b536bcc843f4cd1e02a7e6171da5c040f4d44b" - integrity sha512-kf9RB0Fg7NZfap83B3QOqOGg9QmD9yBudqQXzzOtn3i4y7ZUXe5ONeW34Gwi+TxhH4mvj72R1Zc300KUMa9Bng== - -"@fortawesome/fontawesome-free@^6.4.0": - version "6.4.0" - resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free/-/fontawesome-free-6.4.0.tgz#1ee0c174e472c84b23cb46c995154dc383e3b4fe" - integrity sha512-0NyytTlPJwB/BF5LtRV8rrABDbe3TdTXqNB3PdZ+UUUZAEIrdOJdmABqKjt4AXwIoJNaRVVZEXxpNrqvE1GAYQ== - -"@gar/promisify@^1.0.1", "@gar/promisify@^1.1.3": - version "1.1.3" - resolved "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz" - integrity sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw== - -"@humanwhocodes/config-array@^0.11.8": - version "0.11.8" - resolved "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.8.tgz" - integrity sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g== - dependencies: - "@humanwhocodes/object-schema" "^1.2.1" - debug "^4.1.1" - minimatch "^3.0.5" - -"@humanwhocodes/module-importer@^1.0.1": - version "1.0.1" - resolved "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz" - integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA== - -"@humanwhocodes/object-schema@^1.2.1": - version "1.2.1" - resolved "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz" - integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== - -"@iiif/vocabulary@^1.0.26": - version "1.0.26" - resolved "https://registry.npmjs.org/@iiif/vocabulary/-/vocabulary-1.0.26.tgz" - integrity sha512-yOsMDg5C90iMfD5HSydoTDzmOM/ki5zGiu4DbHpzRueM7D+12IcDHeai2A8QvEroS8HCJl5M1Edbju5rOlPIpg== - -"@istanbuljs/load-nyc-config@^1.0.0": - version "1.1.0" - resolved "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz" - integrity sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ== - 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" - -"@istanbuljs/schema@^0.1.2": - version "0.1.3" - resolved "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz" - integrity sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA== - -"@jridgewell/gen-mapping@^0.1.0": - version "0.1.1" - resolved "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz" - integrity sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w== - dependencies: - "@jridgewell/set-array" "^1.0.0" - "@jridgewell/sourcemap-codec" "^1.4.10" - -"@jridgewell/gen-mapping@^0.3.0", "@jridgewell/gen-mapping@^0.3.2": - version "0.3.3" - resolved "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz" - integrity sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ== - dependencies: - "@jridgewell/set-array" "^1.0.1" - "@jridgewell/sourcemap-codec" "^1.4.10" - "@jridgewell/trace-mapping" "^0.3.9" - -"@jridgewell/resolve-uri@3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz" - integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w== - -"@jridgewell/set-array@^1.0.0", "@jridgewell/set-array@^1.0.1": - version "1.1.2" - resolved "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz" - integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw== - -"@jridgewell/source-map@^0.3.2": - version "0.3.3" - resolved "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.3.tgz" - integrity sha512-b+fsZXeLYi9fEULmfBrhxn4IrPlINf8fiNarzTof004v3lFdntdwa9PF7vFJqm3mg7s+ScJMxXaE3Acp1irZcg== - dependencies: - "@jridgewell/gen-mapping" "^0.3.0" - "@jridgewell/trace-mapping" "^0.3.9" - -"@jridgewell/sourcemap-codec@1.4.14": - version "1.4.14" - resolved "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz" - integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== - -"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.13", "@jridgewell/sourcemap-codec@^1.4.14": - version "1.4.15" - resolved "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz" - integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== - -"@jridgewell/trace-mapping@^0.3.17", "@jridgewell/trace-mapping@^0.3.9": - version "0.3.18" - resolved "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz" - integrity sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA== - dependencies: - "@jridgewell/resolve-uri" "3.1.0" - "@jridgewell/sourcemap-codec" "1.4.14" - -"@kolkov/ngx-gallery@^2.0.1": - 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" - -"@leichtgewicht/ip-codec@^2.0.1": - version "2.0.4" - resolved "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz" - integrity sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A== - -"@material-ui/core@^4.11.0": - version "4.12.4" - resolved "https://registry.npmjs.org/@material-ui/core/-/core-4.12.4.tgz" - integrity sha512-tr7xekNlM9LjA6pagJmL8QCgZXaubWUwkJnoYcMKd4gw/t4XiyvnTkjdGrUVicyB2BsdaAv1tvow45bPM4sSwQ== - 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" - -"@material-ui/icons@^4.11.3", "@material-ui/icons@^4.9.1": - version "4.11.3" - resolved "https://registry.yarnpkg.com/@material-ui/icons/-/icons-4.11.3.tgz#b0693709f9b161ce9ccde276a770d968484ecff1" - integrity sha512-IKHlyx6LDh8n19vzwH5RtHIOHl9Tu90aAAxcbWME6kp4dmvODM3UvOHJeMIDzUbd4muuJKHmlNoBN+mDY4XkBA== - dependencies: - "@babel/runtime" "^7.4.4" - -"@material-ui/lab@^4.0.0-alpha.53": - 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== - 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" - -"@material-ui/styles@^4.11.5": - 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== - 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" - -"@material-ui/system@^4.12.2": - 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" - -"@material-ui/types@5.1.0": - version "5.1.0" - resolved "https://registry.npmjs.org/@material-ui/types/-/types-5.1.0.tgz" - integrity sha512-7cqRjrY50b8QzRSYyhSpx4WRw2YuO0KKIGQEVk5J8uoz2BanawykgZGoWEqKm7pVIbzFDN0SpPcVV4IhOFkl8A== - -"@material-ui/utils@^4.11.3": - 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" - -"@ng-bootstrap/ng-bootstrap@^11.0.0": - 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" - -"@ng-dynamic-forms/core@^15.0.0": - version "15.0.0" - resolved "https://registry.npmjs.org/@ng-dynamic-forms/core/-/core-15.0.0.tgz" - integrity sha512-JJ0w8WdOA+wsHyt/hwitGhv/e1j95/TlRS82vvZetP/Ip3kjvD/Ge8jbg4bEssIAXZjfBqS/Gy00Hxo4h57DgQ== - dependencies: - tslib "^2.0.0" - -"@ng-dynamic-forms/ui-ng-bootstrap@^15.0.0": - version "15.0.0" - resolved "https://registry.npmjs.org/@ng-dynamic-forms/ui-ng-bootstrap/-/ui-ng-bootstrap-15.0.0.tgz" - integrity sha512-b/+tOJxtDRMzoFA7KLA8JRxbAnXd8d8072/P6C+2xOMaG0Ttc1UUiNQOZ5w82y78nr0bZ63oFHSR0xzSVtMXnA== - dependencies: - tslib "^2.0.0" - -"@ngrx/effects@^15.4.0": - version "15.4.0" - resolved "https://registry.npmjs.org/@ngrx/effects/-/effects-15.4.0.tgz" - integrity sha512-/8gHhOM9aeGaw8OG2LLwi4I4p84xzG0EU9TqWrvQcW74wn8sFZONjLvUte5YOzJ5502PPFFrfXSOc+lHnVAJUA== - dependencies: - tslib "^2.0.0" - -"@ngrx/router-store@^15.4.0": - version "15.4.0" - resolved "https://registry.npmjs.org/@ngrx/router-store/-/router-store-15.4.0.tgz" - integrity sha512-uc8gx+SMr2gyly+WSBmGnpHTPsMVhenUG6skH3Qk67hrwdVG3SABjw3qdP4GibbfwwajMQ67sLaLY8PQZ5Sb2g== - dependencies: - tslib "^2.0.0" - -"@ngrx/store-devtools@^15.4.0": - version "15.4.0" - resolved "https://registry.npmjs.org/@ngrx/store-devtools/-/store-devtools-15.4.0.tgz" - integrity sha512-AqWRprSyS3u/vZlpsBOPMW1/0TZS2LE13/KpZedEY+RMCW+mi53esqgrdNGOB3Wr4vrI82Ar9fgaIm4TQO6LPg== - dependencies: - tslib "^2.0.0" - -"@ngrx/store@^15.4.0": - version "15.4.0" - resolved "https://registry.npmjs.org/@ngrx/store/-/store-15.4.0.tgz" - integrity sha512-OvCuNBHL8mAUnRTS6QSFm+IunspsYNu2cCwDovBNn7EGhxRuGihBeNoX47jCqWPHBFtokj4BlatDfpJ/yCh4xQ== - dependencies: - tslib "^2.0.0" - -"@ngtools/webpack@15.2.5": - version "15.2.5" - resolved "https://registry.npmjs.org/@ngtools/webpack/-/webpack-15.2.5.tgz" - integrity sha512-wD6GY4xghVK+SQL0dy/M3saGx5pqi7+1VHEr+BBI7IUNYGSqPNzylKNxLBgQiTzfkzvbrZ6MhfaMNkhvSCYr5w== - -"@ngtools/webpack@15.2.6", "@ngtools/webpack@^15.2.6": - version "15.2.6" - resolved "https://registry.yarnpkg.com/@ngtools/webpack/-/webpack-15.2.6.tgz#9000eafbcb74508ad8300d4f92cc3640f86f0810" - integrity sha512-I+kekKItfsCLdX+ZjjmsWqd0AyoYGTQPjlbQAiPtmdH73/rfPOF4Q/3AU4tzTdn0n0GXqZWv6VOs91w99ydi0A== - -"@nguniversal/builders@^15.2.1": - version "15.2.1" - resolved "https://registry.yarnpkg.com/@nguniversal/builders/-/builders-15.2.1.tgz#0fe86d0f5af92910c728e44aa81620002857531b" - integrity sha512-6UbnJlgFv0KcEmYtw8lJ4B9DVpQcTyQd7vEpuvlOJ7RtcKwY+yUbmsEFnUusxM7y6NgZqiFDziAdJ796agySTQ== - dependencies: - "@angular-devkit/architect" "~0.1502.0" - "@angular-devkit/core" "~15.2.0" - "@nguniversal/common" "15.2.1" - browser-sync "^2.27.10" - express "^4.18.2" - guess-parser "^0.4.22" - http-proxy-middleware "^2.0.6" - ora "^5.1.0" - piscina "~3.2.0" - rxjs "^6.5.5" - tree-kill "^1.2.2" - -"@nguniversal/common@15.2.1": - version "15.2.1" - resolved "https://registry.yarnpkg.com/@nguniversal/common/-/common-15.2.1.tgz#6e3f6b3f46394c02e20a93f7721bcd660810c1a7" - integrity sha512-1m/YFx5FLJAm/ll7YZfUTFGPqsOaHkhwEv8aHIp8oc1CWf6K+agrfLIwzY39n18T3J0lrCL/8/9vR1tnId3A9g== - dependencies: - critters "0.0.16" - jsdom "21.1.0" - tslib "^2.3.0" - -"@nguniversal/express-engine@^15.2.1": - version "15.2.1" - resolved "https://registry.yarnpkg.com/@nguniversal/express-engine/-/express-engine-15.2.1.tgz#4c99b77cc690e44aa6f25ea857838590c71e8237" - integrity sha512-LHQfntApCGvKREJ0MvhioM1TdWnYxqnYRgX3JoWaNVFPdVryhWLPv4RCjqV8QiMe5DQKd6Pc5J+VIhYX5hwjPQ== - dependencies: - "@nguniversal/common" "15.2.1" - tslib "^2.3.0" - -"@ngx-translate/core@^14.0.0": - version "14.0.0" - resolved "https://registry.yarnpkg.com/@ngx-translate/core/-/core-14.0.0.tgz#af421d0e1a28376843f0fed375cd2fae7630a5ff" - integrity sha512-UevdwNCXMRCdJv//0kC8h2eSfmi02r29xeE8E9gJ1Al4D4jEJ7eiLPdjslTMc21oJNGguqqWeEVjf64SFtvw2w== - dependencies: - tslib "^2.3.0" - -"@nicky-lenaers/ngx-scroll-to@^14.0.0": - version "14.0.0" - resolved "https://registry.yarnpkg.com/@nicky-lenaers/ngx-scroll-to/-/ngx-scroll-to-14.0.0.tgz#178cb2c1781dd565841d940e1a678d6eda0796fc" - integrity sha512-nORagPmAQYDjB5jyQ3awGhbnKTKVARZUCK8FJIe46XjPQX2CI9+WBYvsdqq0CsNWVzCjhnC0Om6krzjOTrTHYQ== - dependencies: - tslib "^2.3.0" - -"@nodelib/fs.scandir@2.1.5": - 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" - -"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": - version "2.0.5" - resolved "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz" - integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== - -"@nodelib/fs.walk@^1.2.3", "@nodelib/fs.walk@^1.2.8": - 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" - -"@npmcli/fs@^1.0.0": - version "1.1.1" - resolved "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.1.tgz" - integrity sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ== - dependencies: - "@gar/promisify" "^1.0.1" - semver "^7.3.5" - -"@npmcli/fs@^2.1.0": - version "2.1.2" - resolved "https://registry.npmjs.org/@npmcli/fs/-/fs-2.1.2.tgz" - integrity sha512-yOJKRvohFOaLqipNtwYB9WugyZKhC/DZC4VYPmpaCzDBrA8YpK3qHZ8/HGscMnE4GqbkLNuVcCnxkeQEdGt6LQ== - dependencies: - "@gar/promisify" "^1.1.3" - semver "^7.3.5" - -"@npmcli/fs@^3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@npmcli/fs/-/fs-3.1.0.tgz" - integrity sha512-7kZUAaLscfgbwBQRbvdMYaZOWyMEcPTH/tJjnyAWJ/dvvs9Ef+CERx/qJb9GExJpl1qipaDGn7KqHnFGGixd0w== - dependencies: - semver "^7.3.5" - -"@npmcli/git@^4.0.0": - version "4.0.4" - resolved "https://registry.npmjs.org/@npmcli/git/-/git-4.0.4.tgz" - integrity sha512-5yZghx+u5M47LghaybLCkdSyFzV/w4OuH12d96HO389Ik9CDsLaDZJVynSGGVJOLn6gy/k7Dz5XYcplM3uxXRg== - dependencies: - "@npmcli/promise-spawn" "^6.0.0" - lru-cache "^7.4.4" - npm-pick-manifest "^8.0.0" - proc-log "^3.0.0" - promise-inflight "^1.0.1" - promise-retry "^2.0.1" - semver "^7.3.5" - which "^3.0.0" - -"@npmcli/installed-package-contents@^2.0.1": - version "2.0.2" - resolved "https://registry.npmjs.org/@npmcli/installed-package-contents/-/installed-package-contents-2.0.2.tgz" - integrity sha512-xACzLPhnfD51GKvTOOuNX2/V4G4mz9/1I2MfDoye9kBM3RYe5g2YbscsaGoTlaWqkxeiapBWyseULVKpSVHtKQ== - dependencies: - npm-bundled "^3.0.0" - npm-normalize-package-bin "^3.0.0" - -"@npmcli/move-file@^1.0.1": - 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== - dependencies: - mkdirp "^1.0.4" - rimraf "^3.0.2" - -"@npmcli/move-file@^2.0.0": - version "2.0.1" - resolved "https://registry.npmjs.org/@npmcli/move-file/-/move-file-2.0.1.tgz" - integrity sha512-mJd2Z5TjYWq/ttPLLGqArdtnC74J6bOzg4rMDnN+p1xTacZ2yPRCk2y0oSWQtygLR9YVQXgOcONrwtnk3JupxQ== - dependencies: - mkdirp "^1.0.4" - rimraf "^3.0.2" - -"@npmcli/node-gyp@^3.0.0": - version "3.0.0" - resolved "https://registry.npmjs.org/@npmcli/node-gyp/-/node-gyp-3.0.0.tgz" - integrity sha512-gp8pRXC2oOxu0DUE1/M3bYtb1b3/DbJ5aM113+XJBgfXdussRAsX0YOrOhdd8WvnAR6auDBvJomGAkLKA5ydxA== - -"@npmcli/promise-spawn@^6.0.0", "@npmcli/promise-spawn@^6.0.1": - version "6.0.2" - resolved "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-6.0.2.tgz" - integrity sha512-gGq0NJkIGSwdbUt4yhdF8ZrmkGKVz9vAdVzpOfnom+V8PLSmSOVhZwbNvZZS1EYcJN5hzzKBxmmVVAInM6HQLg== - dependencies: - which "^3.0.0" - -"@npmcli/run-script@^6.0.0": - version "6.0.0" - resolved "https://registry.npmjs.org/@npmcli/run-script/-/run-script-6.0.0.tgz" - integrity sha512-ql+AbRur1TeOdl1FY+RAwGW9fcr4ZwiVKabdvm93mujGREVuVLbdkXRJDrkTXSdCjaxYydr1wlA2v67jxWG5BQ== - dependencies: - "@npmcli/node-gyp" "^3.0.0" - "@npmcli/promise-spawn" "^6.0.0" - node-gyp "^9.0.0" - read-package-json-fast "^3.0.0" - which "^3.0.0" - -"@polka/url@^1.0.0-next.20": - version "1.0.0-next.21" - resolved "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.21.tgz" - integrity sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g== - -"@react-dnd/asap@^4.0.0": - version "4.0.1" - resolved "https://registry.npmjs.org/@react-dnd/asap/-/asap-4.0.1.tgz" - integrity sha512-kLy0PJDDwvwwTXxqTFNAAllPHD73AycE9ypWeln/IguoGBEbvFcPDbCV03G52bEcC5E+YgupBE0VzHGdC8SIXg== - -"@react-dnd/invariant@^2.0.0": - version "2.0.0" - resolved "https://registry.npmjs.org/@react-dnd/invariant/-/invariant-2.0.0.tgz" - integrity sha512-xL4RCQBCBDJ+GRwKTFhGUW8GXa4yoDfJrPbLblc3U09ciS+9ZJXJ3Qrcs/x2IODOdIE5kQxvMmE2UKyqUictUw== - -"@react-dnd/shallowequal@^2.0.0": - version "2.0.0" - resolved "https://registry.npmjs.org/@react-dnd/shallowequal/-/shallowequal-2.0.0.tgz" - integrity sha512-Pc/AFTdwZwEKJxFJvlxrSmGe/di+aAOBn60sremrpLo6VI/6cmiUYNNwlI5KNYttg7uypzA3ILPMPgxB2GYZEg== - -"@redux-saga/core@^1.2.3": - version "1.2.3" - resolved "https://registry.npmjs.org/@redux-saga/core/-/core-1.2.3.tgz" - integrity sha512-U1JO6ncFBAklFTwoQ3mjAeQZ6QGutsJzwNBjgVLSWDpZTRhobUzuVDS1qH3SKGJD8fvqoaYOjp6XJ3gCmeZWgA== - 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" - redux "^4.0.4" - typescript-tuple "^2.2.1" - -"@redux-saga/deferred@^1.2.1": - version "1.2.1" - resolved "https://registry.npmjs.org/@redux-saga/deferred/-/deferred-1.2.1.tgz" - integrity sha512-cmin3IuuzMdfQjA0lG4B+jX+9HdTgHZZ+6u3jRAOwGUxy77GSlTi4Qp2d6PM1PUoTmQUR5aijlA39scWWPF31g== - -"@redux-saga/delay-p@^1.2.1": - 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" - -"@redux-saga/is@^1.1.3": - 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" - -"@redux-saga/symbols@^1.1.3": - version "1.1.3" - resolved "https://registry.npmjs.org/@redux-saga/symbols/-/symbols-1.1.3.tgz" - integrity sha512-hCx6ZvU4QAEUojETnX8EVg4ubNLBFl1Lps4j2tX7o45x/2qg37m3c6v+kSp8xjDJY+2tJw4QB3j8o8dsl1FDXg== - -"@redux-saga/types@^1.2.1": - version "1.2.1" - resolved "https://registry.npmjs.org/@redux-saga/types/-/types-1.2.1.tgz" - integrity sha512-1dgmkh+3so0+LlBWRhGA33ua4MYr7tUOj+a9Si28vUi0IUFNbff1T3sgpeDJI/LaC75bBYnQ0A3wXjn0OrRNBA== - -"@researchgate/react-intersection-observer@^1.0.0": - 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== - -"@schematics/angular@15.2.6": - version "15.2.6" - resolved "https://registry.yarnpkg.com/@schematics/angular/-/angular-15.2.6.tgz#5a0face51806f6b2adf67eef73b08c31744b07bc" - integrity sha512-OcBUvVAxZEMBX+fi0ytybeAdmStra+GwtlvipS70yOxcAgJ84ZrnZGN7a072cCVQcq7AgqUfssnyqCx1wu+yCg== - dependencies: - "@angular-devkit/core" "15.2.6" - "@angular-devkit/schematics" "15.2.6" - jsonc-parser "3.2.0" - -"@schematics/angular@^12.2.17": - version "12.2.18" - resolved "https://registry.npmjs.org/@schematics/angular/-/angular-12.2.18.tgz" - integrity sha512-niRS9Ly9y8uI0YmTSbo8KpdqCCiZ/ATMZWeS2id5M8JZvfXbngwiqJvojdSol0SWU+n1W4iA+lJBdt4gSKlD5w== - dependencies: - "@angular-devkit/core" "12.2.18" - "@angular-devkit/schematics" "12.2.18" - jsonc-parser "3.0.0" - -"@sigstore/protobuf-specs@^0.1.0": - version "0.1.0" - resolved "https://registry.npmjs.org/@sigstore/protobuf-specs/-/protobuf-specs-0.1.0.tgz" - integrity sha512-a31EnjuIDSX8IXBUib3cYLDRlPMU36AWX4xS8ysLaNu4ZzUesDiPt83pgrW2X1YLMe5L2HbDyaKK5BrL4cNKaQ== - -"@socket.io/component-emitter@~3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz" - integrity sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg== - -"@tootallnate/once@1": - version "1.1.2" - resolved "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz" - integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw== - -"@tootallnate/once@2": - version "2.0.0" - resolved "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz" - integrity sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A== - -"@tsconfig/node10@^1.0.7": - version "1.0.9" - resolved "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz" - integrity sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA== - -"@tsconfig/node12@^1.0.7": - version "1.0.11" - resolved "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz" - integrity sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag== - -"@tsconfig/node14@^1.0.0": - version "1.0.3" - resolved "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz" - integrity sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow== - -"@tsconfig/node16@^1.0.2": - version "1.0.3" - resolved "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.3.tgz" - integrity sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ== - -"@tufjs/canonical-json@1.0.0": - version "1.0.0" - resolved "https://registry.npmjs.org/@tufjs/canonical-json/-/canonical-json-1.0.0.tgz" - integrity sha512-QTnf++uxunWvG2z3UFNzAoQPHxnSXOwtaI3iJ+AohhV+5vONuArPjJE7aPXPVXfXJsqrVbZBu9b81AJoSd09IQ== - -"@tufjs/models@1.0.2": - version "1.0.2" - resolved "https://registry.npmjs.org/@tufjs/models/-/models-1.0.2.tgz" - integrity sha512-uxarDtxTIK3f8hJS4yFhW/lvTa3tsiQU5iDCRut+NCnOXvNtEul0Ct58NIIcIx9Rkt7OFEK31Ndpqsd663nsew== - dependencies: - "@tufjs/canonical-json" "1.0.0" - minimatch "^8.0.3" - -"@types/body-parser@*": - version "1.19.2" - resolved "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz" - integrity sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g== - dependencies: - "@types/connect" "*" - "@types/node" "*" - -"@types/bonjour@^3.5.9": - version "3.5.10" - resolved "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.10.tgz" - integrity sha512-p7ienRMiS41Nu2/igbJxxLDWrSZ0WxM8UQgCeO9KhoVF7cOVFkrKsiDr1EsJIla8vV3oEEjGcz11jc5yimhzZw== - dependencies: - "@types/node" "*" - -"@types/circular-json@^0.4.0": - version "0.4.0" - resolved "https://registry.npmjs.org/@types/circular-json/-/circular-json-0.4.0.tgz" - integrity sha512-7+kYB7x5a7nFWW1YPBh3KxhwKfiaI4PbZ1RvzBU91LZy7lWJO822CI+pqzSre/DZ7KsCuMKdHnLHHFu8AyXbQg== - -"@types/connect-history-api-fallback@^1.3.5": - version "1.3.5" - resolved "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.3.5.tgz" - integrity sha512-h8QJa8xSb1WD4fpKBDcATDNGXghFj6/3GRWG6dhmRcu0RX1Ubasur2Uvx5aeEwlf0MwblEC2bMzzMQntxnw/Cw== - dependencies: - "@types/express-serve-static-core" "*" - "@types/node" "*" - -"@types/connect@*": - version "3.4.35" - resolved "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz" - integrity sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ== - dependencies: - "@types/node" "*" - -"@types/cookie@^0.4.1": - 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== - -"@types/cors@^2.8.12": - version "2.8.13" - resolved "https://registry.npmjs.org/@types/cors/-/cors-2.8.13.tgz" - integrity sha512-RG8AStHlUiV5ysZQKq97copd2UmVYw3/pRMLefISZ3S1hK104Cwm7iLQ3fTKx+lsUH2CE8FlLaYeEA2LSeqYUA== - dependencies: - "@types/node" "*" - -"@types/deep-freeze@0.1.2": - version "0.1.2" - resolved "https://registry.npmjs.org/@types/deep-freeze/-/deep-freeze-0.1.2.tgz" - integrity sha512-M6x29Vk4681dght4IMnPIcF1SNmeEm0c4uatlTFhp+++H1oDK1THEIzuCC2WeCBVhX+gU0NndsseDS3zaCtlcQ== - -"@types/ejs@^3.1.2": - version "3.1.2" - resolved "https://registry.yarnpkg.com/@types/ejs/-/ejs-3.1.2.tgz#75d277b030bc11b3be38c807e10071f45ebc78d9" - integrity sha512-ZmiaE3wglXVWBM9fyVC17aGPkLo/UgaOjEiI2FXQfyczrCefORPxIe+2dVmnmk3zkVIbizjrlQzmPGhSYGXG5g== - -"@types/eslint-scope@^3.7.3": - version "3.7.4" - resolved "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.4.tgz" - integrity sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA== - dependencies: - "@types/eslint" "*" - "@types/estree" "*" - -"@types/eslint@*": - version "8.37.0" - resolved "https://registry.npmjs.org/@types/eslint/-/eslint-8.37.0.tgz" - integrity sha512-Piet7dG2JBuDIfohBngQ3rCt7MgO9xCO4xIMKxBThCq5PNRB91IjlJ10eJVwfoNtvTErmxLzwBZ7rHZtbOMmFQ== - dependencies: - "@types/estree" "*" - "@types/json-schema" "*" - -"@types/estree@*", "@types/estree@^0.0.51": - version "0.0.51" - resolved "https://registry.npmjs.org/@types/estree/-/estree-0.0.51.tgz" - integrity sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ== - -"@types/express-serve-static-core@*", "@types/express-serve-static-core@^4.17.33": - version "4.17.33" - resolved "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.33.tgz" - integrity sha512-TPBqmR/HRYI3eC2E5hmiivIzv+bidAfXofM+sbonAGvyDhySGw9/PQZFt2BLOrjUUR++4eJVpx6KnLQK1Fk9tA== - dependencies: - "@types/node" "*" - "@types/qs" "*" - "@types/range-parser" "*" - -"@types/express@*", "@types/express@^4.17.13", "@types/express@^4.17.17": - version "4.17.17" - resolved "https://registry.npmjs.org/@types/express/-/express-4.17.17.tgz" - integrity sha512-Q4FmmuLGBG58btUnfS1c1r/NQdlp3DMfGDGig8WhfpA2YRUtEkxAjkZb0yvplJGYdF1fsQ81iMDcH24sSCNC/Q== - dependencies: - "@types/body-parser" "*" - "@types/express-serve-static-core" "^4.17.33" - "@types/qs" "*" - "@types/serve-static" "*" - -"@types/grecaptcha@^3.0.4": - version "3.0.4" - resolved "https://registry.npmjs.org/@types/grecaptcha/-/grecaptcha-3.0.4.tgz" - integrity sha512-7l1Y8DTGXkx/r4pwU1nMVAR+yD/QC+MCHKXAyEX/7JZhwcN1IED09aZ9vCjjkcGdhSQiu/eJqcXInpl6eEEEwg== - -"@types/hoist-non-react-statics@^3.3.0", "@types/hoist-non-react-statics@^3.3.1": - version "3.3.1" - resolved "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz" - integrity sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA== - dependencies: - "@types/react" "*" - hoist-non-react-statics "^3.3.0" - -"@types/http-proxy@^1.17.5", "@types/http-proxy@^1.17.8": - version "1.17.10" - resolved "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.10.tgz" - integrity sha512-Qs5aULi+zV1bwKAg5z1PWnDXWmsn+LxIvUGv6E2+OOMYhclZMO+OXd9pYVf2gLykf2I7IV2u7oTHwChPNsvJ7g== - dependencies: - "@types/node" "*" - -"@types/jasmine@~3.6.0": - version "3.6.11" - resolved "https://registry.npmjs.org/@types/jasmine/-/jasmine-3.6.11.tgz" - integrity sha512-S6pvzQDvMZHrkBz2Mcn/8Du7cpr76PlRJBAoHnSDNbulULsH5dp0Gns+WRyNX5LHejz/ljxK4/vIHK/caHt6SQ== - -"@types/js-cookie@2.2.6": - version "2.2.6" - resolved "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-2.2.6.tgz" - integrity sha512-+oY0FDTO2GYKEV0YPvSshGq9t7YozVkgvXLty7zogQNuCxBhT9/3INX9Q7H1aRZ4SUDRXAKlJuA4EA5nTt7SNw== - -"@types/json-schema@*", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": - version "7.0.11" - resolved "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz" - integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ== - -"@types/json5@^0.0.29": - version "0.0.29" - resolved "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz" - integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== - -"@types/lodash@^4.14.194": - version "4.14.194" - resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.194.tgz#b71eb6f7a0ff11bff59fc987134a093029258a76" - integrity sha512-r22s9tAS7imvBt2lyHC9B8AGwWnXaYb1tY09oyLkXDs4vArpYJzw09nj8MLx5VfciBPGIb+ZwG0ssYnEPJxn/g== - -"@types/mime@*": - version "3.0.1" - resolved "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz" - integrity sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA== - -"@types/node@*", "@types/node@>=10.0.0", "@types/node@^14.14.31", "@types/node@^14.14.9": - version "14.18.42" - resolved "https://registry.npmjs.org/@types/node/-/node-14.18.42.tgz" - integrity sha512-xefu+RBie4xWlK8hwAzGh3npDz/4VhF6icY/shU+zv/1fNn+ZVG7T7CRwe9LId9sAYRPxI+59QBPuKL3WpyGRg== - -"@types/parse-json@^4.0.0": - version "4.0.0" - resolved "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz" - integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA== - -"@types/prop-types@*": - version "15.7.5" - resolved "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz" - integrity sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w== - -"@types/qs@*": - version "6.9.7" - resolved "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz" - integrity sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw== - -"@types/range-parser@*": - version "1.2.4" - resolved "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz" - integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw== - -"@types/react-redux@^7.1.20": - version "7.1.25" - resolved "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.25.tgz" - integrity sha512-bAGh4e+w5D8dajd6InASVIyCo4pZLJ66oLb80F9OBLO1gKESbZcRCJpTT6uLXX+HAB57zw1WTdwJdAsewuTweg== - dependencies: - "@types/hoist-non-react-statics" "^3.3.0" - "@types/react" "*" - hoist-non-react-statics "^3.3.0" - redux "^4.0.0" - -"@types/react-transition-group@^4.2.0": - version "4.4.5" - resolved "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.5.tgz" - integrity sha512-juKD/eiSM3/xZYzjuzH6ZwpP+/lejltmiS3QEzV/vmb/Q8+HfDmxu+Baga8UEMGBqV88Nbg4l2hY/K2DkyaLLA== - dependencies: - "@types/react" "*" - -"@types/react@*": - version "17.0.57" - resolved "https://registry.npmjs.org/@types/react/-/react-17.0.57.tgz" - integrity sha512-e4msYpu5QDxzNrXDHunU/VPyv2M1XemGG/p7kfCjUiPtlLDCWLGQfgAMng6YyisWYxZ09mYdQlmMnyS0NfZdEg== - dependencies: - "@types/prop-types" "*" - "@types/scheduler" "*" - csstype "^3.0.2" - -"@types/retry@0.12.0": - version "0.12.0" - resolved "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz" - integrity sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA== - -"@types/sanitize-html@^2.9.0": - version "2.9.0" - resolved "https://registry.yarnpkg.com/@types/sanitize-html/-/sanitize-html-2.9.0.tgz#5b609f7592de22ef80a0930c39670329753dca1b" - integrity sha512-4fP/kEcKNj2u39IzrxWYuf/FnCCwwQCpif6wwY6ROUS1EPRIfWJjGkY3HIowY1EX/VbX5e86yq8AAE7UPMgATg== - dependencies: - htmlparser2 "^8.0.0" - -"@types/scheduler@*": - version "0.16.3" - resolved "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.3.tgz" - integrity sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ== - -"@types/semver@^7.3.12": - version "7.3.13" - resolved "https://registry.npmjs.org/@types/semver/-/semver-7.3.13.tgz" - integrity sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw== - -"@types/serve-index@^1.9.1": - version "1.9.1" - resolved "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.1.tgz" - integrity sha512-d/Hs3nWDxNL2xAczmOVZNj92YZCS6RGxfBPjKzuu/XirCgXdpKEb88dYNbrYGint6IVWLNP+yonwVAuRC0T2Dg== - dependencies: - "@types/express" "*" - -"@types/serve-static@*", "@types/serve-static@^1.13.10": - version "1.15.1" - resolved "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.1.tgz" - integrity sha512-NUo5XNiAdULrJENtJXZZ3fHtfMolzZwczzBbnAeBbqBwG+LaG6YaJtuwzwGSQZ2wsCrxjEhNNjAkKigy3n8teQ== - dependencies: - "@types/mime" "*" - "@types/node" "*" - -"@types/sinonjs__fake-timers@8.1.1": - 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== - -"@types/sizzle@^2.3.2": - version "2.3.3" - resolved "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.3.tgz" - integrity sha512-JYM8x9EGF163bEyhdJBpR2QX1R5naCJHC8ucJylJ3w9/CVBaskdQ8WqBf8MmQrd1kRvp/a4TS8HJ+bxzR7ZJYQ== - -"@types/sockjs@^0.3.33": - version "0.3.33" - resolved "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.33.tgz" - integrity sha512-f0KEEe05NvUnat+boPTZ0dgaLZ4SfSouXUgv5noUiefG2ajgKjmETo9ZJyuqsl7dfl2aHlLJUiki6B4ZYldiiw== - dependencies: - "@types/node" "*" - -"@types/stacktrace-js@^0.0.33": - version "0.0.33" - resolved "https://registry.npmjs.org/@types/stacktrace-js/-/stacktrace-js-0.0.33.tgz" - integrity sha512-aqJ6QM9QThNL4dHBhwl1f9B0oDqiREkYLn9RldghUKsGeFWWGlCsqsRWxbh+hDvvmptMFqc4aIfFIGz9BBu8Qg== - -"@types/ws@^8.5.1": - version "8.5.4" - resolved "https://registry.npmjs.org/@types/ws/-/ws-8.5.4.tgz" - integrity sha512-zdQDHKUgcX/zBc4GrwsE/7dVdAD8JR4EuiAXiiUhhfyIJXXb2+PrGshFyeXWQPMmmZ2XxgaqclgpIC7eTXc1mg== - dependencies: - "@types/node" "*" - -"@types/yauzl@^2.9.1": - version "2.10.0" - resolved "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.0.tgz" - integrity sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw== - dependencies: - "@types/node" "*" - -"@typescript-eslint/eslint-plugin@^5.59.1": - version "5.59.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.59.1.tgz#9b09ee1541bff1d2cebdcb87e7ce4a4003acde08" - integrity sha512-AVi0uazY5quFB9hlp2Xv+ogpfpk77xzsgsIEWyVS7uK/c7MZ5tw7ZPbapa0SbfkqE0fsAMkz5UwtgMLVk2BQAg== - dependencies: - "@eslint-community/regexpp" "^4.4.0" - "@typescript-eslint/scope-manager" "5.59.1" - "@typescript-eslint/type-utils" "5.59.1" - "@typescript-eslint/utils" "5.59.1" - debug "^4.3.4" - grapheme-splitter "^1.0.4" - ignore "^5.2.0" - natural-compare-lite "^1.4.0" - semver "^7.3.7" - tsutils "^3.21.0" - -"@typescript-eslint/parser@^5.59.1": - version "5.59.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.59.1.tgz#73c2c12127c5c1182d2e5b71a8fa2a85d215cbb4" - integrity sha512-nzjFAN8WEu6yPRDizIFyzAfgK7nybPodMNFGNH0M9tei2gYnYszRDqVA0xlnRjkl7Hkx2vYrEdb6fP2a21cG1g== - dependencies: - "@typescript-eslint/scope-manager" "5.59.1" - "@typescript-eslint/types" "5.59.1" - "@typescript-eslint/typescript-estree" "5.59.1" - debug "^4.3.4" - -"@typescript-eslint/scope-manager@5.48.2": - version "5.48.2" - resolved "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.48.2.tgz" - integrity sha512-zEUFfonQid5KRDKoI3O+uP1GnrFd4tIHlvs+sTJXiWuypUWMuDaottkJuR612wQfOkjYbsaskSIURV9xo4f+Fw== - dependencies: - "@typescript-eslint/types" "5.48.2" - "@typescript-eslint/visitor-keys" "5.48.2" - -"@typescript-eslint/scope-manager@5.58.0": - version "5.58.0" - resolved "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.58.0.tgz" - integrity sha512-b+w8ypN5CFvrXWQb9Ow9T4/6LC2MikNf1viLkYTiTbkQl46CnR69w7lajz1icW0TBsYmlpg+mRzFJ4LEJ8X9NA== - dependencies: - "@typescript-eslint/types" "5.58.0" - "@typescript-eslint/visitor-keys" "5.58.0" - -"@typescript-eslint/scope-manager@5.59.1": - version "5.59.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.59.1.tgz#8a20222719cebc5198618a5d44113705b51fd7fe" - integrity sha512-mau0waO5frJctPuAzcxiNWqJR5Z8V0190FTSqRw1Q4Euop6+zTwHAf8YIXNwDOT29tyUDrQ65jSg9aTU/H0omA== - dependencies: - "@typescript-eslint/types" "5.59.1" - "@typescript-eslint/visitor-keys" "5.59.1" - -"@typescript-eslint/type-utils@5.48.2": - version "5.48.2" - resolved "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.48.2.tgz" - integrity sha512-QVWx7J5sPMRiOMJp5dYshPxABRoZV1xbRirqSk8yuIIsu0nvMTZesKErEA3Oix1k+uvsk8Cs8TGJ6kQ0ndAcew== - dependencies: - "@typescript-eslint/typescript-estree" "5.48.2" - "@typescript-eslint/utils" "5.48.2" - debug "^4.3.4" - tsutils "^3.21.0" - -"@typescript-eslint/type-utils@5.59.1": - version "5.59.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.59.1.tgz#63981d61684fd24eda2f9f08c0a47ecb000a2111" - integrity sha512-ZMWQ+Oh82jWqWzvM3xU+9y5U7MEMVv6GLioM3R5NJk6uvP47kZ7YvlgSHJ7ERD6bOY7Q4uxWm25c76HKEwIjZw== - dependencies: - "@typescript-eslint/typescript-estree" "5.59.1" - "@typescript-eslint/utils" "5.59.1" - debug "^4.3.4" - tsutils "^3.21.0" - -"@typescript-eslint/types@5.48.2": - version "5.48.2" - resolved "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.48.2.tgz" - integrity sha512-hE7dA77xxu7ByBc6KCzikgfRyBCTst6dZQpwaTy25iMYOnbNljDT4hjhrGEJJ0QoMjrfqrx+j1l1B9/LtKeuqA== - -"@typescript-eslint/types@5.58.0": - version "5.58.0" - resolved "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.58.0.tgz" - integrity sha512-JYV4eITHPzVQMnHZcYJXl2ZloC7thuUHrcUmxtzvItyKPvQ50kb9QXBkgNAt90OYMqwaodQh2kHutWZl1fc+1g== - -"@typescript-eslint/types@5.59.1": - version "5.59.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.59.1.tgz#03f3fedd1c044cb336ebc34cc7855f121991f41d" - integrity sha512-dg0ICB+RZwHlysIy/Dh1SP+gnXNzwd/KS0JprD3Lmgmdq+dJAJnUPe1gNG34p0U19HvRlGX733d/KqscrGC1Pg== - -"@typescript-eslint/typescript-estree@5.48.2": - version "5.48.2" - resolved "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.48.2.tgz" - integrity sha512-bibvD3z6ilnoVxUBFEgkO0k0aFvUc4Cttt0dAreEr+nrAHhWzkO83PEVVuieK3DqcgL6VAK5dkzK8XUVja5Zcg== - dependencies: - "@typescript-eslint/types" "5.48.2" - "@typescript-eslint/visitor-keys" "5.48.2" - debug "^4.3.4" - globby "^11.1.0" - is-glob "^4.0.3" - semver "^7.3.7" - tsutils "^3.21.0" - -"@typescript-eslint/typescript-estree@5.58.0": - version "5.58.0" - resolved "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.58.0.tgz" - integrity sha512-cRACvGTodA+UxnYM2uwA2KCwRL7VAzo45syNysqlMyNyjw0Z35Icc9ihPJZjIYuA5bXJYiJ2YGUB59BqlOZT1Q== - dependencies: - "@typescript-eslint/types" "5.58.0" - "@typescript-eslint/visitor-keys" "5.58.0" - debug "^4.3.4" - globby "^11.1.0" - is-glob "^4.0.3" - semver "^7.3.7" - tsutils "^3.21.0" - -"@typescript-eslint/typescript-estree@5.59.1": - version "5.59.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.59.1.tgz#4aa546d27fd0d477c618f0ca00b483f0ec84c43c" - integrity sha512-lYLBBOCsFltFy7XVqzX0Ju+Lh3WPIAWxYpmH/Q7ZoqzbscLiCW00LeYCdsUnnfnj29/s1WovXKh2gwCoinHNGA== - dependencies: - "@typescript-eslint/types" "5.59.1" - "@typescript-eslint/visitor-keys" "5.59.1" - debug "^4.3.4" - globby "^11.1.0" - is-glob "^4.0.3" - semver "^7.3.7" - tsutils "^3.21.0" - -"@typescript-eslint/utils@5.48.2": - version "5.48.2" - resolved "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.48.2.tgz" - integrity sha512-2h18c0d7jgkw6tdKTlNaM7wyopbLRBiit8oAxoP89YnuBOzCZ8g8aBCaCqq7h208qUTroL7Whgzam7UY3HVLow== - dependencies: - "@types/json-schema" "^7.0.9" - "@types/semver" "^7.3.12" - "@typescript-eslint/scope-manager" "5.48.2" - "@typescript-eslint/types" "5.48.2" - "@typescript-eslint/typescript-estree" "5.48.2" - eslint-scope "^5.1.1" - eslint-utils "^3.0.0" - semver "^7.3.7" - -"@typescript-eslint/utils@5.59.1": - version "5.59.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.59.1.tgz#d89fc758ad23d2157cfae53f0b429bdf15db9473" - integrity sha512-MkTe7FE+K1/GxZkP5gRj3rCztg45bEhsd8HYjczBuYm+qFHP5vtZmjx3B0yUCDotceQ4sHgTyz60Ycl225njmA== - dependencies: - "@eslint-community/eslint-utils" "^4.2.0" - "@types/json-schema" "^7.0.9" - "@types/semver" "^7.3.12" - "@typescript-eslint/scope-manager" "5.59.1" - "@typescript-eslint/types" "5.59.1" - "@typescript-eslint/typescript-estree" "5.59.1" - eslint-scope "^5.1.1" - semver "^7.3.7" - -"@typescript-eslint/utils@^5.57.0": - version "5.58.0" - resolved "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.58.0.tgz" - integrity sha512-gAmLOTFXMXOC+zP1fsqm3VceKSBQJNzV385Ok3+yzlavNHZoedajjS4UyS21gabJYcobuigQPs/z71A9MdJFqQ== - dependencies: - "@eslint-community/eslint-utils" "^4.2.0" - "@types/json-schema" "^7.0.9" - "@types/semver" "^7.3.12" - "@typescript-eslint/scope-manager" "5.58.0" - "@typescript-eslint/types" "5.58.0" - "@typescript-eslint/typescript-estree" "5.58.0" - eslint-scope "^5.1.1" - semver "^7.3.7" - -"@typescript-eslint/visitor-keys@5.48.2": - version "5.48.2" - resolved "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.48.2.tgz" - integrity sha512-z9njZLSkwmjFWUelGEwEbdf4NwKvfHxvGC0OcGN1Hp/XNDIcJ7D5DpPNPv6x6/mFvc1tQHsaWmpD/a4gOvvCJQ== - dependencies: - "@typescript-eslint/types" "5.48.2" - eslint-visitor-keys "^3.3.0" - -"@typescript-eslint/visitor-keys@5.58.0": - version "5.58.0" - resolved "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.58.0.tgz" - integrity sha512-/fBraTlPj0jwdyTwLyrRTxv/3lnU2H96pNTVM6z3esTWLtA5MZ9ghSMJ7Rb+TtUAdtEw9EyJzJ0EydIMKxQ9gA== - dependencies: - "@typescript-eslint/types" "5.58.0" - eslint-visitor-keys "^3.3.0" - -"@typescript-eslint/visitor-keys@5.59.1": - version "5.59.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.59.1.tgz#0d96c36efb6560d7fb8eb85de10442c10d8f6058" - integrity sha512-6waEYwBTCWryx0VJmP7JaM4FpipLsFl9CvYf2foAE8Qh/Y0s+bxWysciwOs0LTBED4JCaNxTZ5rGadB14M6dwA== - dependencies: - "@typescript-eslint/types" "5.59.1" - eslint-visitor-keys "^3.3.0" - -"@webassemblyjs/ast@1.11.1": - version "1.11.1" - resolved "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz" - integrity sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw== - dependencies: - "@webassemblyjs/helper-numbers" "1.11.1" - "@webassemblyjs/helper-wasm-bytecode" "1.11.1" - -"@webassemblyjs/floating-point-hex-parser@1.11.1": - version "1.11.1" - resolved "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.1.tgz" - integrity sha512-iGRfyc5Bq+NnNuX8b5hwBrRjzf0ocrJPI6GWFodBFzmFnyvrQ83SHKhmilCU/8Jv67i4GJZBMhEzltxzcNagtQ== - -"@webassemblyjs/helper-api-error@1.11.1": - version "1.11.1" - resolved "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.1.tgz" - integrity sha512-RlhS8CBCXfRUR/cwo2ho9bkheSXG0+NwooXcc3PAILALf2QLdFyj7KGsKRbVc95hZnhnERon4kW/D3SZpp6Tcg== - -"@webassemblyjs/helper-buffer@1.11.1": - version "1.11.1" - resolved "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.1.tgz" - integrity sha512-gwikF65aDNeeXa8JxXa2BAk+REjSyhrNC9ZwdT0f8jc4dQQeDQ7G4m0f2QCLPJiMTTO6wfDmRmj/pW0PsUvIcA== - -"@webassemblyjs/helper-numbers@1.11.1": - version "1.11.1" - resolved "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.1.tgz" - integrity sha512-vDkbxiB8zfnPdNK9Rajcey5C0w+QJugEglN0of+kmO8l7lDb77AnlKYQF7aarZuCrv+l0UvqL+68gSDr3k9LPQ== - dependencies: - "@webassemblyjs/floating-point-hex-parser" "1.11.1" - "@webassemblyjs/helper-api-error" "1.11.1" - "@xtuc/long" "4.2.2" - -"@webassemblyjs/helper-wasm-bytecode@1.11.1": - version "1.11.1" - resolved "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.1.tgz" - integrity sha512-PvpoOGiJwXeTrSf/qfudJhwlvDQxFgelbMqtq52WWiXC6Xgg1IREdngmPN3bs4RoO83PnL/nFrxucXj1+BX62Q== - -"@webassemblyjs/helper-wasm-section@1.11.1": - version "1.11.1" - resolved "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.1.tgz" - integrity sha512-10P9No29rYX1j7F3EVPX3JvGPQPae+AomuSTPiF9eBQeChHI6iqjMIwR9JmOJXwpnn/oVGDk7I5IlskuMwU/pg== - dependencies: - "@webassemblyjs/ast" "1.11.1" - "@webassemblyjs/helper-buffer" "1.11.1" - "@webassemblyjs/helper-wasm-bytecode" "1.11.1" - "@webassemblyjs/wasm-gen" "1.11.1" - -"@webassemblyjs/ieee754@1.11.1": - version "1.11.1" - resolved "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.1.tgz" - integrity sha512-hJ87QIPtAMKbFq6CGTkZYJivEwZDbQUgYd3qKSadTNOhVY7p+gfP6Sr0lLRVTaG1JjFj+r3YchoqRYxNH3M0GQ== - dependencies: - "@xtuc/ieee754" "^1.2.0" - -"@webassemblyjs/leb128@1.11.1": - version "1.11.1" - resolved "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.1.tgz" - integrity sha512-BJ2P0hNZ0u+Th1YZXJpzW6miwqQUGcIHT1G/sf72gLVD9DZ5AdYTqPNbHZh6K1M5VmKvFXwGSWZADz+qBWxeRw== - dependencies: - "@xtuc/long" "4.2.2" - -"@webassemblyjs/utf8@1.11.1": - version "1.11.1" - resolved "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.1.tgz" - integrity sha512-9kqcxAEdMhiwQkHpkNiorZzqpGrodQQ2IGrHHxCy+Ozng0ofyMA0lTqiLkVs1uzTRejX+/O0EOT7KxqVPuXosQ== - -"@webassemblyjs/wasm-edit@1.11.1": - version "1.11.1" - resolved "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.1.tgz" - integrity sha512-g+RsupUC1aTHfR8CDgnsVRVZFJqdkFHpsHMfJuWQzWU3tvnLC07UqHICfP+4XyL2tnr1amvl1Sdp06TnYCmVkA== - dependencies: - "@webassemblyjs/ast" "1.11.1" - "@webassemblyjs/helper-buffer" "1.11.1" - "@webassemblyjs/helper-wasm-bytecode" "1.11.1" - "@webassemblyjs/helper-wasm-section" "1.11.1" - "@webassemblyjs/wasm-gen" "1.11.1" - "@webassemblyjs/wasm-opt" "1.11.1" - "@webassemblyjs/wasm-parser" "1.11.1" - "@webassemblyjs/wast-printer" "1.11.1" - -"@webassemblyjs/wasm-gen@1.11.1": - version "1.11.1" - resolved "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.1.tgz" - integrity sha512-F7QqKXwwNlMmsulj6+O7r4mmtAlCWfO/0HdgOxSklZfQcDu0TpLiD1mRt/zF25Bk59FIjEuGAIyn5ei4yMfLhA== - dependencies: - "@webassemblyjs/ast" "1.11.1" - "@webassemblyjs/helper-wasm-bytecode" "1.11.1" - "@webassemblyjs/ieee754" "1.11.1" - "@webassemblyjs/leb128" "1.11.1" - "@webassemblyjs/utf8" "1.11.1" - -"@webassemblyjs/wasm-opt@1.11.1": - version "1.11.1" - resolved "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.1.tgz" - integrity sha512-VqnkNqnZlU5EB64pp1l7hdm3hmQw7Vgqa0KF/KCNO9sIpI6Fk6brDEiX+iCOYrvMuBWDws0NkTOxYEb85XQHHw== - dependencies: - "@webassemblyjs/ast" "1.11.1" - "@webassemblyjs/helper-buffer" "1.11.1" - "@webassemblyjs/wasm-gen" "1.11.1" - "@webassemblyjs/wasm-parser" "1.11.1" - -"@webassemblyjs/wasm-parser@1.11.1": - version "1.11.1" - resolved "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.1.tgz" - integrity sha512-rrBujw+dJu32gYB7/Lup6UhdkPx9S9SnobZzRVL7VcBH9Bt9bCBLEuX/YXOOtBsOZ4NQrRykKhffRWHvigQvOA== - dependencies: - "@webassemblyjs/ast" "1.11.1" - "@webassemblyjs/helper-api-error" "1.11.1" - "@webassemblyjs/helper-wasm-bytecode" "1.11.1" - "@webassemblyjs/ieee754" "1.11.1" - "@webassemblyjs/leb128" "1.11.1" - "@webassemblyjs/utf8" "1.11.1" - -"@webassemblyjs/wast-printer@1.11.1": - version "1.11.1" - resolved "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.1.tgz" - integrity sha512-IQboUWM4eKzWW+N/jij2sRatKMh99QEelo3Eb2q0qXkvPRISAj8Qxtmw5itwqK+TTkBuUIE45AxYPToqPtL5gg== - dependencies: - "@webassemblyjs/ast" "1.11.1" - "@xtuc/long" "4.2.2" - -"@webpack-cli/configtest@^1.2.0": - version "1.2.0" - resolved "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-1.2.0.tgz" - integrity sha512-4FB8Tj6xyVkyqjj1OaTqCjXYULB9FMkqQ8yGrZjRDrYh0nOE+7Lhs45WioWQQMV+ceFlE368Ukhe6xdvJM9Egg== - -"@webpack-cli/info@^1.5.0": - version "1.5.0" - resolved "https://registry.npmjs.org/@webpack-cli/info/-/info-1.5.0.tgz" - integrity sha512-e8tSXZpw2hPl2uMJY6fsMswaok5FdlGNRTktvFk2sD8RjH0hE2+XistawJx1vmKteh4NmGmNUrp+Tb2w+udPcQ== - dependencies: - envinfo "^7.7.3" - -"@webpack-cli/serve@^1.7.0": - version "1.7.0" - resolved "https://registry.npmjs.org/@webpack-cli/serve/-/serve-1.7.0.tgz" - integrity sha512-oxnCNGj88fL+xzV+dacXs44HcDwf1ovs3AuEzvP7mqXw7fQntqIhQ1BRmynh4qEKQSSSRSWVyXRjmTbZIX9V2Q== - -"@wessberg/ts-evaluator@0.0.27": - version "0.0.27" - resolved "https://registry.npmjs.org/@wessberg/ts-evaluator/-/ts-evaluator-0.0.27.tgz" - integrity sha512-7gOpVm3yYojUp/Yn7F4ZybJRxyqfMNf0LXK5KJiawbPfL0XTsJV+0mgrEDjOIR6Bi0OYk2Cyg4tjFu1r8MCZaA== - dependencies: - chalk "^4.1.0" - jsdom "^16.4.0" - object-path "^0.11.5" - tslib "^2.0.3" - -"@xtuc/ieee754@^1.2.0": - version "1.2.0" - resolved "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz" - integrity sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA== - -"@xtuc/long@4.2.2": - version "4.2.2" - resolved "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz" - integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== - -"@yarnpkg/lockfile@1.1.0": - version "1.1.0" - resolved "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz" - integrity sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ== - -abab@^2.0.3, abab@^2.0.5, abab@^2.0.6: - version "2.0.6" - resolved "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz" - integrity sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA== - -abbrev@1, abbrev@^1.0.0: - version "1.1.1" - resolved "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz" - integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== - -accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.8: - 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" - -acorn-globals@^6.0.0: - version "6.0.0" - resolved "https://registry.npmjs.org/acorn-globals/-/acorn-globals-6.0.0.tgz" - integrity sha512-ZQl7LOWaF5ePqqcX4hLuv/bLXYQNfNWw2c0/yX/TsPRKamzHcTGQnlCjHT3TsmkOUVEPS3crCxiPfdzE/Trlhg== - dependencies: - acorn "^7.1.1" - acorn-walk "^7.1.1" - -acorn-globals@^7.0.0: - version "7.0.1" - resolved "https://registry.npmjs.org/acorn-globals/-/acorn-globals-7.0.1.tgz" - integrity sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q== - dependencies: - acorn "^8.1.0" - acorn-walk "^8.0.2" - -acorn-import-assertions@^1.7.6: - version "1.8.0" - resolved "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz" - integrity sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw== - -acorn-jsx@^5.3.2: - version "5.3.2" - resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz" - integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== - -acorn-walk@^7.1.1: - version "7.2.0" - resolved "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz" - integrity sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA== - -acorn-walk@^8.0.0, acorn-walk@^8.0.2, acorn-walk@^8.1.1: - version "8.2.0" - resolved "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz" - integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== - -acorn@^7.1.1: - version "7.4.1" - resolved "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz" - integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== - -acorn@^8.0.4, acorn@^8.1.0, acorn@^8.2.4, acorn@^8.4.1, acorn@^8.5.0, acorn@^8.7.1, acorn@^8.8.0, acorn@^8.8.1: - version "8.8.2" - resolved "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz" - integrity sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw== - -adjust-sourcemap-loader@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/adjust-sourcemap-loader/-/adjust-sourcemap-loader-4.0.0.tgz" - integrity sha512-OXwN5b9pCUXNQHJpwwD2qP40byEmSgzj8B4ydSN0uMNYWiFmJ6x6KwUllMmfk8Rwu/HJDFR7U8ubsWBoN0Xp0A== - dependencies: - loader-utils "^2.0.0" - regex-parser "^2.2.11" - -adm-zip@^0.5.2: - version "0.5.10" - resolved "https://registry.yarnpkg.com/adm-zip/-/adm-zip-0.5.10.tgz#4a51d5ab544b1f5ce51e1b9043139b639afff45b" - integrity sha512-x0HvcHqVJNTPk/Bw8JbLWlWoo6Wwnsug0fnYYro1HBrjxZ3G7/AZk7Ahv8JwDe1uIcz8eBqvu86FuF1POiG7vQ== - -agent-base@6, agent-base@^6.0.2: - version "6.0.2" - resolved "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz" - integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== - dependencies: - debug "4" - -agentkeepalive@^4.2.1: - version "4.3.0" - resolved "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.3.0.tgz" - integrity sha512-7Epl1Blf4Sy37j4v9f9FjICCh4+KAQOyXgHEwlyBiAQLbhKdq/i2QQU3amQalS/wPhdPzDXPL5DMR5bkn+YeWg== - dependencies: - debug "^4.1.0" - depd "^2.0.0" - humanize-ms "^1.2.1" - -aggregate-error@^3.0.0: - version "3.1.0" - resolved "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz" - integrity sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA== - dependencies: - clean-stack "^2.0.0" - indent-string "^4.0.0" - -ajv-formats@2.1.0: - version "2.1.0" - resolved "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.0.tgz" - integrity sha512-USH2jBb+C/hIpwD2iRjp0pe0k+MvzG0mlSn/FIdCgQhUb9ALPRjt2KIQdfZDS9r0ZIeUAg7gOu9KL0PFqGqr5Q== - dependencies: - ajv "^8.0.0" - -ajv-formats@2.1.1, ajv-formats@^2.1.1: - version "2.1.1" - resolved "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz" - integrity sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA== - dependencies: - ajv "^8.0.0" - -ajv-keywords@^3.5.2: - version "3.5.2" - resolved "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz" - integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== - -ajv-keywords@^5.0.0: - version "5.1.0" - resolved "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz" - integrity sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw== - dependencies: - fast-deep-equal "^3.1.3" - -ajv@8.12.0, ajv@^8.8.0: - version "8.12.0" - resolved "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz" - integrity sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA== - dependencies: - fast-deep-equal "^3.1.1" - json-schema-traverse "^1.0.0" - require-from-string "^2.0.2" - uri-js "^4.2.2" - -ajv@8.6.2, ajv@^8.0.0: - version "8.6.2" - resolved "https://registry.npmjs.org/ajv/-/ajv-8.6.2.tgz" - integrity sha512-9807RlWAgT564wT+DjeyU5OFMPjmzxVobvDFmNAhY+5zD6A2ly3jDp6sgnfyDtlIQ+7H97oc/DGCzzfu9rjw9w== - dependencies: - fast-deep-equal "^3.1.1" - json-schema-traverse "^1.0.0" - require-from-string "^2.0.2" - uri-js "^4.2.2" - -ajv@^6.10.0, ajv@^6.12.3, ajv@^6.12.4, ajv@^6.12.5: - version "6.12.6" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" - integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== - 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" - -angular-idle-preload@3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/angular-idle-preload/-/angular-idle-preload-3.0.0.tgz" - integrity sha512-W3P2m2B6MHdt1DVunH6H3VWkAZrG3ZwxGcPjedVvIyRhg/LmMtILoizHSxTXw3fsKIEdAPwGObXGpML9WD1jJA== - -angulartics2@^12.2.0: - version "12.2.0" - resolved "https://registry.yarnpkg.com/angulartics2/-/angulartics2-12.2.0.tgz#79159693b4436905391e3b1e7d549c900408e175" - integrity sha512-1wl/cPA4PGYj42z80VIR+A0Hy3+rt13POzfTfZ83NUDx2KKbHjtTKS0O7u3umi10cqvi5tn4NTSYIFFJ1fI2Tw== - dependencies: - tslib "^2.3.0" - -ansi-colors@4.1.3, ansi-colors@^4.1.1: - 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== - -ansi-escapes@^4.2.1, ansi-escapes@^4.3.0: - version "4.3.2" - resolved "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz" - integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ== - dependencies: - type-fest "^0.21.3" - -ansi-html-community@^0.0.8: - 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== - -ansi-regex@^2.0.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" - integrity sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA== - -ansi-regex@^3.0.0: - version "3.0.1" - resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.1.tgz" - integrity sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw== - -ansi-regex@^5.0.1: - version "5.0.1" - resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz" - integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== - -ansi-styles@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" - integrity sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA== - -ansi-styles@^3.2.1: - 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" - -ansi-styles@^4.0.0, ansi-styles@^4.1.0: - 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" - -anymatch@~3.1.2: - 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" - -"aproba@^1.0.3 || ^2.0.0": - version "2.0.0" - resolved "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz" - integrity sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ== - -arch@^2.2.0: - version "2.2.0" - resolved "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz" - integrity sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ== - -are-we-there-yet@^3.0.0: - version "3.0.1" - resolved "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz" - integrity sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg== - dependencies: - delegates "^1.0.0" - readable-stream "^3.6.0" - -arg@^4.1.0: - version "4.1.3" - resolved "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz" - integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== - -argparse@^1.0.7: - version "1.0.10" - resolved "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz" - integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== - dependencies: - sprintf-js "~1.0.2" - -argparse@^2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz" - integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== - -aria-query@5.1.3: - version "5.1.3" - resolved "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz" - integrity sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ== - dependencies: - deep-equal "^2.0.5" - -array-buffer-byte-length@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz" - integrity sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A== - dependencies: - call-bind "^1.0.2" - is-array-buffer "^3.0.1" - -array-flatten@1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz" - integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== - -array-flatten@^2.1.2: - version "2.1.2" - resolved "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz" - integrity sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ== - -array-includes@^3.1.6: - version "3.1.6" - resolved "https://registry.npmjs.org/array-includes/-/array-includes-3.1.6.tgz" - integrity sha512-sgTbLvL6cNnw24FnbaDyjmvddQ2ML8arZsgaJhoABMoplz/4QRhtrYS+alr1BUM1Bwp6dhx8vVCBSLG+StwOFw== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.4" - es-abstract "^1.20.4" - get-intrinsic "^1.1.3" - is-string "^1.0.7" - -array-union@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39" - integrity sha512-Dxr6QJj/RdU/hCaBjOfxW+q6lyuVE6JFWIrAUpuOOhoJJoQ99cUn3igRaHVB5P9WrgFVN0FfArM3x0cueOU8ng== - dependencies: - array-uniq "^1.0.1" - -array-union@^2.1.0: - version "2.1.0" - resolved "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz" - integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== - -array-uniq@^1.0.1: - version "1.0.3" - resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6" - integrity sha512-MNha4BWQ6JbwhFhj03YK552f7cb3AzoE8SzeljgChvL1dl3IcvggXVz1DilzySZkCja+CXuZbdW7yATchWn8/Q== - -array.prototype.flat@^1.3.1: - version "1.3.1" - resolved "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.1.tgz" - integrity sha512-roTU0KWIOmJ4DRLmwKd19Otg0/mT3qPNt0Qb3GWW8iObuZXxrjB/pzn0R3hqpRSWg4HCwqx+0vwOnWnvlOyeIA== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.4" - es-abstract "^1.20.4" - es-shim-unscopables "^1.0.0" - -array.prototype.flatmap@^1.3.1: - version "1.3.1" - resolved "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.1.tgz" - integrity sha512-8UGn9O1FDVvMNB0UlLv4voxRMze7+FpHyF5mSMRjWHUMlpoDViniy05870VlxhfgTnLbpuwTzvD76MTtWxB/mQ== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.4" - es-abstract "^1.20.4" - es-shim-unscopables "^1.0.0" - -arrify@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" - integrity sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA== - -asn1@~0.2.3: - version "0.2.6" - resolved "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz" - integrity sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ== - dependencies: - safer-buffer "~2.1.0" - -assert-plus@1.0.0, assert-plus@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz" - integrity sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw== - -astral-regex@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz" - integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== - -async-each-series@0.1.1: - 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== - -async@^2.6.0: - version "2.6.4" - resolved "https://registry.npmjs.org/async/-/async-2.6.4.tgz" - integrity sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA== - dependencies: - lodash "^4.17.14" - -async@^3.2.0, async@^3.2.3: - version "3.2.4" - resolved "https://registry.npmjs.org/async/-/async-3.2.4.tgz" - integrity sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ== - -asynckit@^0.4.0: - version "0.4.0" - resolved "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz" - integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== - -at-least-node@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz" - integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== - -autoprefixer@10.4.13: - version "10.4.13" - resolved "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.13.tgz" - integrity sha512-49vKpMqcZYsJjwotvt4+h/BCjJVnhGwcLpDt5xkcaOG3eLrG/HUYLagrihYsQ+qrIBgIzX1Rw7a6L8I/ZA1Atg== - dependencies: - browserslist "^4.21.4" - caniuse-lite "^1.0.30001426" - fraction.js "^4.2.0" - normalize-range "^0.1.2" - picocolors "^1.0.0" - postcss-value-parser "^4.2.0" - -autoprefixer@^10.4.13: - version "10.4.14" - resolved "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.14.tgz" - integrity sha512-FQzyfOsTlwVzjHxKEqRIAdJx9niO6VCBCoEwax/VLSoQF29ggECcPuBqUMZ+u8jCZOPSy8b8/8KnuFbp0SaFZQ== - dependencies: - browserslist "^4.21.5" - caniuse-lite "^1.0.30001464" - fraction.js "^4.2.0" - normalize-range "^0.1.2" - picocolors "^1.0.0" - postcss-value-parser "^4.2.0" - -available-typed-arrays@^1.0.5: - version "1.0.5" - resolved "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz" - integrity sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw== - -aws-sign2@~0.7.0: - 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== - -aws4@^1.8.0: - version "1.12.0" - resolved "https://registry.npmjs.org/aws4/-/aws4-1.12.0.tgz" - integrity sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg== - -axe-core@^4.7.0: - version "4.7.0" - resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.7.0.tgz#34ba5a48a8b564f67e103f0aa5768d76e15bbbbf" - integrity sha512-M0JtH+hlOL5pLQwHOLNYZaXuhqmvS8oExsqB1SBYgA4Dk7u/xx+YdGHXaK5pyUfed5mYXdlYiphWq3G8cRi5JQ== - -axios@0.21.4: - version "0.21.4" - resolved "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz" - integrity sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg== - dependencies: - follow-redirects "^1.14.0" - -axios@^0.27.2: - version "0.27.2" - resolved "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz" - integrity sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ== - dependencies: - follow-redirects "^1.14.9" - form-data "^4.0.0" - -axobject-query@3.1.1: - version "3.1.1" - resolved "https://registry.npmjs.org/axobject-query/-/axobject-query-3.1.1.tgz" - integrity sha512-goKlv8DZrK9hUh975fnHzhNIO4jUnFCfv/dszV5VwUGDFjI6vQ2VwoyjYjYNEbBE8AH87TduWP5uyDR1D+Iteg== - dependencies: - deep-equal "^2.0.5" - -babel-loader@9.1.2: - version "9.1.2" - resolved "https://registry.npmjs.org/babel-loader/-/babel-loader-9.1.2.tgz" - integrity sha512-mN14niXW43tddohGl8HPu5yfQq70iUThvFL/4QzESA7GcZoC0eVOhvWdQ8+3UlSjaDE9MVtsW9mxDY07W7VpVA== - dependencies: - find-cache-dir "^3.3.2" - schema-utils "^4.0.0" - -babel-plugin-istanbul@6.1.1: - 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== - 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" - -babel-plugin-polyfill-corejs2@^0.3.3: - version "0.3.3" - resolved "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.3.3.tgz" - integrity sha512-8hOdmFYFSZhqg2C/JgLUQ+t52o5nirNwaWM2B9LWteozwIvM14VSwdsCAUET10qT+kmySAlseadmfeeSWFCy+Q== - dependencies: - "@babel/compat-data" "^7.17.7" - "@babel/helper-define-polyfill-provider" "^0.3.3" - semver "^6.1.1" - -babel-plugin-polyfill-corejs3@^0.6.0: - version "0.6.0" - resolved "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.6.0.tgz" - integrity sha512-+eHqR6OPcBhJOGgsIar7xoAB1GcSwVUA3XjAd7HJNzOXT4wv6/H7KIdA/Nc60cvUlDbKApmqNvD1B1bzOt4nyA== - dependencies: - "@babel/helper-define-polyfill-provider" "^0.3.3" - core-js-compat "^3.25.1" - -babel-plugin-polyfill-regenerator@^0.4.1: - version "0.4.1" - resolved "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.4.1.tgz" - integrity sha512-NtQGmyQDXjQqQ+IzRkBVwEOz9lQ4zxAQZgoAYEtU9dJjnl1Oc98qnN7jcp+bE7O7aYzVpavXE3/VKXNzUbh7aw== - dependencies: - "@babel/helper-define-polyfill-provider" "^0.3.3" - -balanced-match@^1.0.0: - version "1.0.2" - resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz" - integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== - -base64-js@^1.2.0, base64-js@^1.3.1: - version "1.5.1" - resolved "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz" - integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== - -base64id@2.0.0, base64id@~2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz" - integrity sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog== - -basic-auth@~2.0.1: - 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" - -batch-processor@1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/batch-processor/-/batch-processor-1.0.0.tgz" - integrity sha512-xoLQD8gmmR32MeuBHgH0Tzd5PuSZx71ZsbhVxOCRbgktZEPe4SQy7s9Z50uPp0F/f7iw2XmkHN2xkgbMfckMDA== - -batch@0.6.1: - 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== - -bcrypt-pbkdf@^1.0.0: - version "1.0.2" - resolved "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz" - integrity sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w== - dependencies: - tweetnacl "^0.14.3" - -bent@~7.3.6: - version "7.3.12" - resolved "https://registry.npmjs.org/bent/-/bent-7.3.12.tgz" - integrity sha512-T3yrKnVGB63zRuoco/7Ybl7BwwGZR0lceoVG5XmQyMIH9s19SV5m+a8qam4if0zQuAmOQTyPTPmsQBdAorGK3w== - dependencies: - bytesish "^0.4.1" - caseless "~0.12.0" - is-stream "^2.0.0" - -big.js@^5.2.2: - version "5.2.2" - resolved "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz" - integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ== - -binary-extensions@^2.0.0: - version "2.2.0" - resolved "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz" - integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== - -bl@^4.1.0: - version "4.1.0" - resolved "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz" - integrity sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w== - dependencies: - buffer "^5.5.0" - inherits "^2.0.4" - readable-stream "^3.4.0" - -blob-util@^2.0.2: - version "2.0.2" - resolved "https://registry.npmjs.org/blob-util/-/blob-util-2.0.2.tgz" - integrity sha512-T7JQa+zsXXEa6/8ZhHcQEW1UFfVM49Ts65uBkFL6fz2QmrElqmbajIDJvuA0tEhRe5eIjpV9ZF+0RfZR9voJFQ== - -bluebird@^3.7.2: - version "3.7.2" - resolved "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz" - integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== - -body-parser@1.20.1, body-parser@^1.19.0: - version "1.20.1" - resolved "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz" - integrity sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw== - dependencies: - bytes "3.1.2" - content-type "~1.0.4" - 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.11.0" - raw-body "2.5.1" - type-is "~1.6.18" - unpipe "1.0.0" - -bonjour-service@^1.0.11: - version "1.1.1" - resolved "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.1.1.tgz" - integrity sha512-Z/5lQRMOG9k7W+FkeGTNjh7htqn/2LMnfOvBZ8pynNZCM9MwkQkI3zeI4oz09uWdcgmgHugVvBqxGg4VQJ5PCg== - dependencies: - array-flatten "^2.1.2" - dns-equal "^1.0.0" - fast-deep-equal "^3.1.3" - multicast-dns "^7.2.5" - -boolbase@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz" - integrity sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww== - -bootstrap@^4.6.1: - version "4.6.2" - resolved "https://registry.npmjs.org/bootstrap/-/bootstrap-4.6.2.tgz" - integrity sha512-51Bbp/Uxr9aTuy6ca/8FbFloBUJZLHwnhTcnjIeRn2suQWsWzcuJhGjKDB5eppVte/8oCdOL3VuwxvZDUggwGQ== - -brace-expansion@^1.1.7: - 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" - -brace-expansion@^2.0.1: - 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" - -braces@^3.0.2, braces@~3.0.2: - version "3.0.2" - resolved "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz" - integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== - dependencies: - fill-range "^7.0.1" - -browser-process-hrtime@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz" - integrity sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow== - -browser-sync-client@^2.29.1: - version "2.29.1" - resolved "https://registry.npmjs.org/browser-sync-client/-/browser-sync-client-2.29.1.tgz" - integrity sha512-aESnjt3rU7CZpzjyqzhIC2UJ3MVhzRis7cPKkGbyYWDf/wnbxyRa3fFenF3Qx9061/guY3HHhD67uiTVV26DVg== - dependencies: - etag "1.8.1" - fresh "0.5.2" - mitt "^1.1.3" - -browser-sync-ui@^2.29.1: - version "2.29.1" - resolved "https://registry.npmjs.org/browser-sync-ui/-/browser-sync-ui-2.29.1.tgz" - integrity sha512-MB7SAiUgVUrhipO2xyO1sheC9H0+LKXPQ3L1tQWcZ3AgizBnUNKAqDZPSwe4grNSa8o8ImSAwJp7lMS6XYy1Dw== - 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" - -browser-sync@^2.27.10: - version "2.29.1" - resolved "https://registry.npmjs.org/browser-sync/-/browser-sync-2.29.1.tgz" - integrity sha512-WXy9HMJVQaNUTPjmai330E2fnDA6W84l/vBILGkYu9yHXIpWw1gJYjdQWDfEhLFljYUHNTN9jM3GCej2T55m+g== - dependencies: - browser-sync-client "^2.29.1" - browser-sync-ui "^2.29.1" - bs-recipes "1.3.4" - bs-snippet-injector "^2.0.1" - 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" - localtunnel "^2.0.1" - micromatch "^4.0.2" - opn "5.3.0" - portscanner "2.2.0" - qs "^6.11.0" - raw-body "^2.3.2" - resp-modifier "6.0.2" - rx "4.1.0" - send "0.16.2" - serve-index "1.9.1" - serve-static "1.13.2" - server-destroy "1.0.1" - socket.io "^4.4.1" - ua-parser-js "^1.0.33" - yargs "^17.3.1" - -browserslist@4.21.5, browserslist@^4.14.5, browserslist@^4.21.3, browserslist@^4.21.4, browserslist@^4.21.5: - version "4.21.5" - resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.21.5.tgz" - integrity sha512-tUkiguQGW7S3IhB7N+c2MV/HZPSCPAAiYBZXLsBhFB/PCy6ZKKsZrmBayHV9fdGV/ARIfJ14NkxKzRDjvp7L6w== - dependencies: - caniuse-lite "^1.0.30001449" - electron-to-chromium "^1.4.284" - node-releases "^2.0.8" - update-browserslist-db "^1.0.10" - -bs-recipes@1.3.4: - version "1.3.4" - resolved "https://registry.npmjs.org/bs-recipes/-/bs-recipes-1.3.4.tgz" - integrity sha512-BXvDkqhDNxXEjeGM8LFkSbR+jzmP/CYpCiVKYn+soB1dDldeU15EBNDkwVXndKuX35wnNUaPd0qSoQEAkmQtMw== - -bs-snippet-injector@^2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/bs-snippet-injector/-/bs-snippet-injector-2.0.1.tgz" - integrity sha512-4u8IgB+L9L+S5hknOj3ddNSb42436gsnGm1AuM15B7CdbkpQTyVWgIM5/JUBiKiRwGOR86uo0Lu/OsX+SAlJmw== - -buffer-crc32@~0.2.3: - version "0.2.13" - resolved "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz" - integrity sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ== - -buffer-from@^1.0.0: - version "1.1.2" - resolved "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz" - integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== - -buffer@^5.5.0, buffer@^5.6.0: - version "5.7.1" - resolved "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz" - integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== - dependencies: - base64-js "^1.3.1" - ieee754 "^1.1.13" - -builtins@^5.0.0: - version "5.0.1" - resolved "https://registry.npmjs.org/builtins/-/builtins-5.0.1.tgz" - integrity sha512-qwVpFEHNfhYJIzNRBvd2C1kyo6jz3ZSMPyyuR47OPdiKWlbYnZNyDWuyR175qDnAJLiCo5fBBqPb3RiXgWlkOQ== - dependencies: - semver "^7.0.0" - -bytes@3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz" - integrity sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw== - -bytes@3.1.2: - version "3.1.2" - resolved "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz" - integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== - -bytesish@^0.4.1: - version "0.4.4" - resolved "https://registry.npmjs.org/bytesish/-/bytesish-0.4.4.tgz" - integrity sha512-i4uu6M4zuMUiyfZN4RU2+i9+peJh//pXhd9x1oSe1LBkZ3LEbCoygu8W0bXTukU1Jme2txKuotpCZRaC3FLxcQ== - -cacache@17.0.4: - version "17.0.4" - resolved "https://registry.npmjs.org/cacache/-/cacache-17.0.4.tgz" - integrity sha512-Z/nL3gU+zTUjz5pCA5vVjYM8pmaw2kxM7JEiE0fv3w77Wj+sFbi70CrBruUWH0uNcEdvLDixFpgA2JM4F4DBjA== - dependencies: - "@npmcli/fs" "^3.1.0" - fs-minipass "^3.0.0" - glob "^8.0.1" - lru-cache "^7.7.1" - minipass "^4.0.0" - minipass-collect "^1.0.2" - minipass-flush "^1.0.5" - minipass-pipeline "^1.2.4" - p-map "^4.0.0" - promise-inflight "^1.0.1" - ssri "^10.0.0" - tar "^6.1.11" - unique-filename "^3.0.0" - -cacache@^15.0.5: - version "15.3.0" - resolved "https://registry.npmjs.org/cacache/-/cacache-15.3.0.tgz" - integrity sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ== - 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" - -cacache@^16.1.0: - version "16.1.3" - resolved "https://registry.npmjs.org/cacache/-/cacache-16.1.3.tgz" - integrity sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ== - dependencies: - "@npmcli/fs" "^2.1.0" - "@npmcli/move-file" "^2.0.0" - chownr "^2.0.0" - fs-minipass "^2.1.0" - glob "^8.0.1" - infer-owner "^1.0.4" - lru-cache "^7.7.1" - minipass "^3.1.6" - minipass-collect "^1.0.2" - minipass-flush "^1.0.5" - minipass-pipeline "^1.2.4" - mkdirp "^1.0.4" - p-map "^4.0.0" - promise-inflight "^1.0.1" - rimraf "^3.0.2" - ssri "^9.0.0" - tar "^6.1.11" - unique-filename "^2.0.0" - -cacache@^17.0.0: - version "17.0.5" - resolved "https://registry.npmjs.org/cacache/-/cacache-17.0.5.tgz" - integrity sha512-Y/PRQevNSsjAPWykl9aeGz8Pr+OI6BYM9fYDNMvOkuUiG9IhG4LEmaYrZZZvioMUEQ+cBCxT0v8wrnCURccyKA== - dependencies: - "@npmcli/fs" "^3.1.0" - fs-minipass "^3.0.0" - glob "^9.3.1" - lru-cache "^7.7.1" - minipass "^4.0.0" - minipass-collect "^1.0.2" - minipass-flush "^1.0.5" - minipass-pipeline "^1.2.4" - p-map "^4.0.0" - promise-inflight "^1.0.1" - ssri "^10.0.0" - tar "^6.1.11" - unique-filename "^3.0.0" - -cachedir@^2.3.0: - version "2.3.0" - resolved "https://registry.npmjs.org/cachedir/-/cachedir-2.3.0.tgz" - integrity sha512-A+Fezp4zxnit6FanDmv9EqXNAi3vt9DWp51/71UEhXukb7QUuvtv9344h91dyAxuTLoSYJFU299qzR3tzwPAhw== - -call-bind@^1.0.0, call-bind@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz" - integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA== - dependencies: - function-bind "^1.1.1" - get-intrinsic "^1.0.2" - -callsites@^3.0.0: - version "3.1.0" - resolved "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz" - integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== - -camelcase@^5.3.1: - version "5.3.1" - resolved "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz" - integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== - -caniuse-lite@^1.0.30001426, caniuse-lite@^1.0.30001449, caniuse-lite@^1.0.30001464: - version "1.0.30001477" - resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001477.tgz" - integrity sha512-lZim4iUHhGcy5p+Ri/G7m84hJwncj+Kz7S5aD4hoQfslKZJgt0tHc/hafVbqHC5bbhHb+mrW2JOUHkI5KH7toQ== - -caseless@~0.12.0: - version "0.12.0" - resolved "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz" - integrity sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw== - -cerialize@0.1.18: - 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" - -chalk@4.1.2, chalk@^4.0.0, chalk@^4.0.2, chalk@^4.1.0, chalk@^4.1.1, chalk@~4.1.0: - 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" - -chalk@^1.1.1: - version "1.1.3" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" - integrity sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A== - dependencies: - ansi-styles "^2.2.1" - escape-string-regexp "^1.0.2" - has-ansi "^2.0.0" - strip-ansi "^3.0.0" - supports-color "^2.0.0" - -chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.4.1: - 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" - -chardet@^0.7.0: - version "0.7.0" - resolved "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz" - integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== - -charenc@0.0.2: - version "0.0.2" - resolved "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz" - integrity sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA== - -check-more-types@^2.24.0: - version "2.24.0" - resolved "https://registry.npmjs.org/check-more-types/-/check-more-types-2.24.0.tgz" - integrity sha512-Pj779qHxV2tuapviy1bSZNEL1maXr13bPYpsvSDB68HlYcYuhlDrmGd63i0JHMCLKzc7rUSNIrpdJlhVlNwrxA== - -cheerio-select@^1.5.0: - version "1.6.0" - resolved "https://registry.npmjs.org/cheerio-select/-/cheerio-select-1.6.0.tgz" - integrity sha512-eq0GdBvxVFbqWgmCm7M3XGs1I8oLy/nExUnh6oLqmBditPO9AqQJrkslDpMun/hZ0yyTs8L0m85OHp4ho6Qm9g== - dependencies: - css-select "^4.3.0" - css-what "^6.0.1" - domelementtype "^2.2.0" - domhandler "^4.3.1" - domutils "^2.8.0" - -cheerio@1.0.0-rc.10: - version "1.0.0-rc.10" - resolved "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.10.tgz" - integrity sha512-g0J0q/O6mW8z5zxQ3A8E8J1hUgp4SMOvEoW/x84OwyHKe/Zccz83PVT4y5Crcr530FV6NgmKI1qvGTKVl9XXVw== - dependencies: - cheerio-select "^1.5.0" - dom-serializer "^1.3.2" - domhandler "^4.2.0" - htmlparser2 "^6.1.0" - parse5 "^6.0.1" - parse5-htmlparser2-tree-adapter "^6.0.1" - tslib "^2.2.0" - -chokidar@3.5.3, "chokidar@>=3.0.0 <4.0.0", chokidar@^3.0.0, chokidar@^3.5.1, chokidar@^3.5.2, chokidar@^3.5.3: - version "3.5.3" - resolved "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz" - integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== - 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" - optionalDependencies: - fsevents "~2.3.2" - -chownr@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz" - integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ== - -chrome-trace-event@^1.0.2: - version "1.0.3" - resolved "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz" - integrity sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg== - -ci-info@^3.2.0: - version "3.8.0" - resolved "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz" - integrity sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw== - -circular-json@^0.5.0: - version "0.5.9" - resolved "https://registry.npmjs.org/circular-json/-/circular-json-0.5.9.tgz" - integrity sha512-4ivwqHpIFJZBuhN3g/pEcdbnGUywkBblloGbkglyloVjjR3uT6tieI89MVOfbP2tHX5sgb01FuLgAOzebNlJNQ== - -classnames@^2.2.6: - version "2.3.2" - resolved "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz" - integrity sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw== - -clean-stack@^2.0.0: - version "2.2.0" - resolved "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz" - integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== - -cli-cursor@^3.1.0: - version "3.1.0" - resolved "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz" - integrity sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw== - dependencies: - restore-cursor "^3.1.0" - -cli-progress@^3.12.0: - version "3.12.0" - resolved "https://registry.yarnpkg.com/cli-progress/-/cli-progress-3.12.0.tgz#807ee14b66bcc086258e444ad0f19e7d42577942" - integrity sha512-tRkV3HJ1ASwm19THiiLIXLO7Im7wlTuKnvkYaTkyoAPefqjNg7W7DHKUlGRxy9vxDvbyCYQkQozvptuMkGCg8A== - dependencies: - string-width "^4.2.3" - -cli-spinners@^2.5.0: - version "2.8.0" - resolved "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.8.0.tgz" - integrity sha512-/eG5sJcvEIwxcdYM86k5tPwn0MUzkX5YY3eImTGpJOZgVe4SdTMY14vQpcxgBzJ0wXwAYrS8E+c3uHeK4JNyzQ== - -cli-table3@~0.6.1: - version "0.6.3" - resolved "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.3.tgz" - integrity sha512-w5Jac5SykAeZJKntOxJCrm63Eg5/4dhMWIcuTbo9rpE+brgaSZo0RuNJZeOyMgsUdhDeojvgyQLmjI+K50ZGyg== - dependencies: - string-width "^4.2.0" - optionalDependencies: - "@colors/colors" "1.5.0" - -cli-truncate@^2.1.0: - version "2.1.0" - resolved "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz" - integrity sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg== - dependencies: - slice-ansi "^3.0.0" - string-width "^4.2.0" - -cli-width@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz" - integrity sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw== - -cliui@^7.0.2: - version "7.0.4" - resolved "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz" - integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ== - dependencies: - string-width "^4.2.0" - strip-ansi "^6.0.0" - wrap-ansi "^7.0.0" - -cliui@^8.0.1: - 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" - -clone-deep@^4.0.1: - version "4.0.1" - resolved "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz" - integrity sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ== - dependencies: - is-plain-object "^2.0.4" - kind-of "^6.0.2" - shallow-clone "^3.0.0" - -clone@^1.0.2: - version "1.0.4" - resolved "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz" - integrity sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg== - -clsx@^1.0.4, clsx@^1.1.1: - version "1.2.1" - resolved "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz" - integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg== - -color-convert@^1.9.0: - 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" - -color-convert@^2.0.1: - 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" - -color-name@1.1.3: - version "1.1.3" - resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz" - integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== - -color-name@~1.1.4: - version "1.1.4" - resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz" - integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== - -color-support@^1.1.3: - version "1.1.3" - resolved "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz" - integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg== - -colorette@^2.0.10, colorette@^2.0.14, colorette@^2.0.16: - version "2.0.19" - resolved "https://registry.npmjs.org/colorette/-/colorette-2.0.19.tgz" - integrity sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ== - -colors@^1.4.0: - version "1.4.0" - resolved "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz" - integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA== - -combined-stream@^1.0.6, combined-stream@^1.0.8, combined-stream@~1.0.6: - 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" - -commander@9.2.0: - version "9.2.0" - resolved "https://registry.npmjs.org/commander/-/commander-9.2.0.tgz" - integrity sha512-e2i4wANQiSXgnrBlIatyHtP1odfUp0BbV5Y5nEGbxtIrStkEOAAzCUirvLBNXHLr7kwLvJl6V+4V3XV9x7Wd9w== - -commander@^2.2.0, commander@^2.20.0: - 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== - -commander@^6.1.0, commander@^6.2.1: - version "6.2.1" - resolved "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz" - integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA== - -commander@^7.0.0, commander@^7.2.0: - version "7.2.0" - resolved "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz" - integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw== - -comment-parser@1.3.1: - version "1.3.1" - resolved "https://registry.npmjs.org/comment-parser/-/comment-parser-1.3.1.tgz" - integrity sha512-B52sN2VNghyq5ofvUsqZjmk6YkihBX5vMSChmSK9v4ShjKf3Vk5Xcmgpw4o+iIgtrnM/u5FiMpz9VKb8lpBveA== - -common-tags@^1.8.0: - 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== - -commondir@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz" - integrity sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg== - -compressible@~2.0.16: - 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" - -compression-webpack-plugin@^9.2.0: - 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== - dependencies: - schema-utils "^4.0.0" - serialize-javascript "^6.0.0" - -compression@^1.7.4: - version "1.7.4" - resolved "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz" - integrity sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ== - dependencies: - accepts "~1.3.5" - bytes "3.0.0" - compressible "~2.0.16" - debug "2.6.9" - on-headers "~1.0.2" - safe-buffer "5.1.2" - vary "~1.1.2" - -concat-map@0.0.1: - version "0.0.1" - resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" - integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== - -connect-history-api-fallback@^1: - 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== - -connect-history-api-fallback@^2.0.0: - 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== - -connect@3.6.6: - version "3.6.6" - resolved "https://registry.npmjs.org/connect/-/connect-3.6.6.tgz" - integrity sha512-OO7axMmPpu/2XuX1+2Yrg0ddju31B6xLZMWkJ5rYBu4YRmRVlOjvlY6kw2FJKiAzyxGwnrDUAG4s1Pf0sbBMCQ== - dependencies: - debug "2.6.9" - finalhandler "1.1.0" - parseurl "~1.3.2" - utils-merge "1.0.1" - -connect@^3.7.0: - version "3.7.0" - resolved "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz" - integrity sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ== - dependencies: - debug "2.6.9" - finalhandler "1.1.2" - parseurl "~1.3.3" - utils-merge "1.0.1" - -console-control-strings@^1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz" - integrity sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ== - -content-disposition@0.5.4: - 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" - -content-type@~1.0.4: - version "1.0.5" - resolved "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz" - integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== - -convert-source-map@^1.5.1, convert-source-map@^1.7.0: - 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== - -cookie-parser@1.4.6: - version "1.4.6" - resolved "https://registry.yarnpkg.com/cookie-parser/-/cookie-parser-1.4.6.tgz#3ac3a7d35a7a03bbc7e365073a26074824214594" - integrity sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA== - dependencies: - cookie "0.4.1" - cookie-signature "1.0.6" - -cookie-signature@1.0.6: - version "1.0.6" - resolved "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz" - integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== - -cookie@0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.1.tgz#afd713fe26ebd21ba95ceb61f9a8116e50a537d1" - integrity sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA== - -cookie@0.5.0: - version "0.5.0" - resolved "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz" - integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== - -cookie@~0.4.1: - version "0.4.2" - resolved "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz" - integrity sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA== - -copy-anything@^2.0.1: - version "2.0.6" - resolved "https://registry.npmjs.org/copy-anything/-/copy-anything-2.0.6.tgz" - integrity sha512-1j20GZTsvKNkc4BY3NpMOM8tt///wY3FpIzozTOFO2ffuZcV61nojHXVKIy3WM+7ADCy5FVhdZYHYDdgTU0yJw== - dependencies: - is-what "^3.14.1" - -copy-to-clipboard@^3.3.1: - 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" - -copy-webpack-plugin@11.0.0: - 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== - 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" - -copy-webpack-plugin@^6.4.1: - version "6.4.1" - resolved "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-6.4.1.tgz" - integrity sha512-MXyPCjdPVx5iiWyl40Va3JGh27bKzOTNY3NjUTrosD2q7dR/cLD0013uqJ3BpFbUjyONINjb6qI7nDIJujrMbA== - 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" - -core-js-compat@^3.25.1: - version "3.30.0" - resolved "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.30.0.tgz" - integrity sha512-P5A2h/9mRYZFIAP+5Ab8ns6083IyVpSclU74UNvbGVQ8VM7n3n3/g2yF3AkKQ9NXz2O+ioxLbEWKnDtgsFamhg== - dependencies: - browserslist "^4.21.5" - -core-js@^3.30.1: - version "3.30.1" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.30.1.tgz#fc9c5adcc541d8e9fa3e381179433cbf795628ba" - integrity sha512-ZNS5nbiSwDTq4hFosEDqm65izl2CWmLz0hARJMyNQBgkUZMIF51cQiMvIQKA6hvuaeWxQDP3hEedM1JZIgTldQ== - -core-util-is@1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz" - integrity sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ== - -core-util-is@~1.0.0: - version "1.0.3" - resolved "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz" - integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== - -cors@~2.8.5: - version "2.8.5" - resolved "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz" - integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g== - dependencies: - object-assign "^4" - vary "^1" - -cosmiconfig@^7.0.0: - version "7.1.0" - resolved "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz" - integrity sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA== - 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" - -create-require@^1.1.0: - version "1.1.1" - resolved "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz" - integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== - -critters@0.0.16: - version "0.0.16" - resolved "https://registry.npmjs.org/critters/-/critters-0.0.16.tgz" - integrity sha512-JwjgmO6i3y6RWtLYmXwO5jMd+maZt8Tnfu7VVISmEWyQqfLpB8soBswf8/2bu6SBXxtKA68Al3c+qIG1ApT68A== - dependencies: - chalk "^4.1.0" - css-select "^4.2.0" - parse5 "^6.0.1" - parse5-htmlparser2-tree-adapter "^6.0.1" - postcss "^8.3.7" - pretty-bytes "^5.3.0" - -cross-env@^7.0.3: - version "7.0.3" - resolved "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz" - integrity sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw== - dependencies: - cross-spawn "^7.0.1" - -cross-spawn@^7.0.0, cross-spawn@^7.0.1, cross-spawn@^7.0.2, cross-spawn@^7.0.3: - version "7.0.3" - resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz" - integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== - dependencies: - path-key "^3.1.0" - shebang-command "^2.0.0" - which "^2.0.1" - -crypt@0.0.2: - version "0.0.2" - resolved "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz" - integrity sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow== - -css-blank-pseudo@^3.0.3: - 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== - dependencies: - postcss-selector-parser "^6.0.9" - -css-box-model@^1.2.0: - 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" - -css-has-pseudo@^3.0.4: - version "3.0.4" - resolved "https://registry.npmjs.org/css-has-pseudo/-/css-has-pseudo-3.0.4.tgz" - integrity sha512-Vse0xpR1K9MNlp2j5w1pgWIJtm1a8qS0JwS9goFYcImjlHEmywP9VUF05aGBXzGpDJF86QXk4L0ypBmwPhGArw== - dependencies: - postcss-selector-parser "^6.0.9" - -css-loader@6.7.3: - version "6.7.3" - resolved "https://registry.npmjs.org/css-loader/-/css-loader-6.7.3.tgz" - integrity sha512-qhOH1KlBMnZP8FzRO6YCH9UHXQhVMcEGLyNdb7Hv2cpcmJbW0YrddO+tG1ab5nT41KpHIYGsbeHqxB9xPu1pKQ== - dependencies: - icss-utils "^5.1.0" - postcss "^8.4.19" - postcss-modules-extract-imports "^3.0.0" - postcss-modules-local-by-default "^4.0.0" - postcss-modules-scope "^3.0.0" - postcss-modules-values "^4.0.0" - postcss-value-parser "^4.2.0" - semver "^7.3.8" - -css-prefers-color-scheme@^6.0.3: - version "6.0.3" - resolved "https://registry.npmjs.org/css-prefers-color-scheme/-/css-prefers-color-scheme-6.0.3.tgz" - integrity sha512-4BqMbZksRkJQx2zAjrokiGMd07RqOa2IxIrrN10lyBe9xhn9DEvjUK79J6jkeiv9D9hQFXKb6g1jwU62jziJZA== - -css-select@^4.2.0, css-select@^4.3.0: - version "4.3.0" - resolved "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz" - integrity sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ== - dependencies: - boolbase "^1.0.0" - css-what "^6.0.1" - domhandler "^4.3.1" - domutils "^2.8.0" - nth-check "^2.0.1" - -css-vendor@^2.0.8: - 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" - -css-what@^6.0.1: - version "6.1.0" - resolved "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz" - integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw== - -cssdb@^7.1.0: - version "7.5.4" - resolved "https://registry.npmjs.org/cssdb/-/cssdb-7.5.4.tgz" - integrity sha512-fGD+J6Jlq+aurfE1VDXlLS4Pt0VtNlu2+YgfGOdMxRyl/HQ9bDiHTwSck1Yz8A97Dt/82izSK6Bp/4nVqacOsg== - -cssesc@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz" - integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== - -cssom@^0.4.4: - version "0.4.4" - resolved "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz" - integrity sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw== - -cssom@^0.5.0: - version "0.5.0" - resolved "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz" - integrity sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw== - -cssom@~0.3.6: - version "0.3.8" - resolved "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz" - integrity sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg== - -cssstyle@^2.3.0: - version "2.3.0" - resolved "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz" - integrity sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A== - dependencies: - cssom "~0.3.6" - -csstype@^2.5.2: - version "2.6.21" - resolved "https://registry.npmjs.org/csstype/-/csstype-2.6.21.tgz" - integrity sha512-Z1PhmomIfypOpoMjRQB70jfvy/wxT50qW08YXO5lMIJkrdq4yOTR+AW7FqutScmB9NkLwxo+jU+kZLbofZZq/w== - -csstype@^3.0.2: - version "3.1.2" - resolved "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz" - integrity sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ== - -custom-event@~1.0.0: - version "1.0.1" - resolved "https://registry.npmjs.org/custom-event/-/custom-event-1.0.1.tgz" - integrity sha512-GAj5FOq0Hd+RsCGVJxZuKaIDXDf3h6GQoNEjFgbLLI/trgtavwUbSnZ5pVfg27DVCaWjIohryS0JFwIJyT2cMg== - -cypress-axe@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/cypress-axe/-/cypress-axe-1.4.0.tgz#e67482bfe9e740796bf77c7823f19781a8a2faff" - integrity sha512-Ut7NKfzjyKm0BEbt2WxuKtLkIXmx6FD2j0RwdvO/Ykl7GmB/qRQkwbKLk3VP35+83hiIr8GKD04PDdrTK5BnyA== - -cypress@12.10.0: - version "12.10.0" - resolved "https://registry.yarnpkg.com/cypress/-/cypress-12.10.0.tgz#b6264f77c214d63530ebac2b33c4d099bd40b715" - integrity sha512-Y0wPc221xKKW1/4iAFCphkrG2jNR4MjOne3iGn4mcuCaE7Y5EtXL83N8BzRsAht7GYfWVjJ/UeTqEdDKHz39HQ== - dependencies: - "@cypress/request" "^2.88.10" - "@cypress/xvfb" "^1.2.4" - "@types/node" "^14.14.31" - "@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.6.0" - cachedir "^2.3.0" - chalk "^4.1.0" - check-more-types "^2.24.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-ci "^3.0.0" - 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" - proxy-from-env "1.0.0" - request-progress "^3.0.0" - semver "^7.3.2" - supports-color "^8.1.1" - tmp "~0.2.1" - untildify "^4.0.0" - yauzl "^2.10.0" - -dashdash@^1.12.0: - version "1.14.1" - resolved "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz" - integrity sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g== - dependencies: - assert-plus "^1.0.0" - -data-urls@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/data-urls/-/data-urls-2.0.0.tgz" - integrity sha512-X5eWTSXO/BJmpdIKCRuKUgSCgAN0OwliVK3yPKbwIWU1Tdw5BRajxlzMidvh+gwko9AfQ9zIj52pzF91Q3YAvQ== - dependencies: - abab "^2.0.3" - whatwg-mimetype "^2.3.0" - whatwg-url "^8.0.0" - -data-urls@^3.0.2: - version "3.0.2" - resolved "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz" - integrity sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ== - dependencies: - abab "^2.0.6" - whatwg-mimetype "^3.0.0" - whatwg-url "^11.0.0" - -date-fns-tz@^1.3.7: - version "1.3.8" - resolved "https://registry.npmjs.org/date-fns-tz/-/date-fns-tz-1.3.8.tgz" - integrity sha512-qwNXUFtMHTTU6CFSFjoJ80W8Fzzp24LntbjFFBgL/faqds4e5mo9mftoRLgr3Vi1trISsg4awSpYVsOQCRnapQ== - -date-fns@^2.29.3: - version "2.29.3" - resolved "https://registry.npmjs.org/date-fns/-/date-fns-2.29.3.tgz" - integrity sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA== - -date-format@^4.0.14: - version "4.0.14" - resolved "https://registry.npmjs.org/date-format/-/date-format-4.0.14.tgz" - integrity sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg== - -dayjs@^1.10.4: - version "1.11.7" - resolved "https://registry.npmjs.org/dayjs/-/dayjs-1.11.7.tgz" - integrity sha512-+Yw9U6YO5TQohxLcIkrXBeY73WP3ejHWVvx8XCk3gxvQDCTEmS48ZrSZCKciI7Bhl/uCMyxYtE9UqRILmFphkQ== - -debug@2.6.9, debug@^2.2.0: - 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" - -debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4, debug@~4.3.1, debug@~4.3.2: - version "4.3.4" - resolved "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz" - integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== - dependencies: - ms "2.1.2" - -debug@4.3.2: - version "4.3.2" - resolved "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz" - integrity sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw== - dependencies: - ms "2.1.2" - -debug@^3.1.0, debug@^3.2.6, debug@^3.2.7: - version "3.2.7" - resolved "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz" - integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== - dependencies: - ms "^2.1.1" - -decimal.js@^10.2.1, decimal.js@^10.4.2: - version "10.4.3" - resolved "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz" - integrity sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA== - -deep-equal@^2.0.5: - version "2.2.0" - resolved "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.0.tgz" - integrity sha512-RdpzE0Hv4lhowpIUKKMJfeH6C1pXdtT1/it80ubgWqwI3qpuxUBpC1S4hnHg+zjnuOoDkzUtUCEEkG+XG5l3Mw== - dependencies: - call-bind "^1.0.2" - es-get-iterator "^1.1.2" - get-intrinsic "^1.1.3" - is-arguments "^1.1.1" - is-array-buffer "^3.0.1" - is-date-object "^1.0.5" - is-regex "^1.1.4" - is-shared-array-buffer "^1.0.2" - isarray "^2.0.5" - object-is "^1.1.5" - object-keys "^1.1.1" - object.assign "^4.1.4" - regexp.prototype.flags "^1.4.3" - side-channel "^1.0.4" - which-boxed-primitive "^1.0.2" - which-collection "^1.0.1" - which-typed-array "^1.1.9" - -deep-freeze@0.0.1: - version "0.0.1" - resolved "https://registry.npmjs.org/deep-freeze/-/deep-freeze-0.0.1.tgz" - integrity sha512-Z+z8HiAvsGwmjqlphnHW5oz6yWlOwu6EQfFTjmeTWlDeda3FS2yv3jhq35TX/ewmsnqB+RX2IdsIOyjJCQN5tg== - -deep-is@^0.1.3, deep-is@~0.1.3: - 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== - -deepmerge@^4.2.2, deepmerge@^4.3.1: - version "4.3.1" - resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a" - integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A== - -default-gateway@^6.0.3: - 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== - dependencies: - execa "^5.0.0" - -defaults@^1.0.3: - version "1.0.4" - resolved "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz" - integrity sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A== - dependencies: - clone "^1.0.2" - -define-lazy-prop@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz" - integrity sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og== - -define-properties@^1.1.3, define-properties@^1.1.4: - version "1.2.0" - resolved "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz" - integrity sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA== - dependencies: - has-property-descriptors "^1.0.0" - object-keys "^1.1.1" - -del@^2.2.0: - version "2.2.2" - resolved "https://registry.yarnpkg.com/del/-/del-2.2.2.tgz#c12c981d067846c84bcaf862cff930d907ffd1a8" - integrity sha512-Z4fzpbIRjOu7lO5jCETSWoqUDVe0IPOlfugBsF6suen2LKDlVb4QZpKEM9P+buNJ4KI1eN7I083w/pbKUpsrWQ== - dependencies: - globby "^5.0.0" - is-path-cwd "^1.0.0" - is-path-in-cwd "^1.0.0" - object-assign "^4.0.1" - pify "^2.0.0" - pinkie-promise "^2.0.0" - rimraf "^2.2.8" - -delayed-stream@~1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz" - integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== - -delegates@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz" - integrity sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ== - -depd@2.0.0, depd@^2.0.0, depd@~2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz" - integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== - -depd@~1.1.2: - version "1.1.2" - resolved "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz" - integrity sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ== - -dependency-graph@^0.11.0: - version "0.11.0" - resolved "https://registry.npmjs.org/dependency-graph/-/dependency-graph-0.11.0.tgz" - integrity sha512-JeMq7fEshyepOWDfcfHK06N3MhyPhz++vtqWhMT5O9A3K42rdsEDpfdVqjaqaAhsw6a+ZqeDvQVtD0hFHQWrzg== - -destroy@1.2.0: - version "1.2.0" - resolved "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz" - integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== - -destroy@~1.0.4: - version "1.0.4" - resolved "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz" - integrity sha512-3NdhDuEXnfun/z7x9GOElY49LoqVHoGScmOKwmxhsS8N5Y+Z8KyPPDnaSzqWgYt/ji4mqwfTS34Htrk0zPIXVg== - -detect-node@^2.0.4: - version "2.1.0" - resolved "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz" - integrity sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g== - -dev-ip@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/dev-ip/-/dev-ip-1.0.1.tgz" - integrity sha512-LmVkry/oDShEgSZPNgqCIp2/TlqtExeGmymru3uCELnfyjY11IzpAproLYs+1X88fXO6DBoYP3ul2Xo2yz2j6A== - -di@^0.0.1: - version "0.0.1" - resolved "https://registry.npmjs.org/di/-/di-0.0.1.tgz" - integrity sha512-uJaamHkagcZtHPqCIHZxnFrXlunQXgBOsZSUOWwFw31QJCAbyTBoHMW75YOTur5ZNx8pIeAKgf6GWIgaqqiLhA== - -diff@^4.0.1: - version "4.0.2" - resolved "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz" - integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== - -dir-glob@^3.0.1: - version "3.0.1" - resolved "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz" - integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== - dependencies: - path-type "^4.0.0" - -dnd-core@^10.0.2: - 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" - -dnd-multi-backend@^5.0.1: - version "5.0.1" - resolved "https://registry.npmjs.org/dnd-multi-backend/-/dnd-multi-backend-5.0.1.tgz" - integrity sha512-BAOj6fIeGfZPA1LreehaV8mUD7Ww0EpaKqSDRrZ5mZXLWLIxFPPT3bIvpJhHUCt0+lTrOqkXu11Hudh+gHXNUA== - -dns-equal@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz" - integrity sha512-z+paD6YUQsk+AbGCEM4PrOXSss5gd66QfcVBFTKR/HpFL9jCqikS94HYwKww6fQyO7IxrIIyUu+g0Ka9tUS2Cg== - -dns-packet@^5.2.2: - version "5.5.0" - resolved "https://registry.npmjs.org/dns-packet/-/dns-packet-5.5.0.tgz" - integrity sha512-USawdAUzRkV6xrqTjiAEp6M9YagZEzWcSUaZTcIFAiyQWW1SoI6KyId8y2+/71wbgHKQAKd+iupLv4YvEwYWvA== - dependencies: - "@leichtgewicht/ip-codec" "^2.0.1" - -doctrine@^2.1.0: - version "2.1.0" - resolved "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz" - integrity sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw== - dependencies: - esutils "^2.0.2" - -doctrine@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz" - integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== - dependencies: - esutils "^2.0.2" - -dom-helpers@^5.0.1: - 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" - -dom-serialize@^2.2.1: - version "2.2.1" - resolved "https://registry.npmjs.org/dom-serialize/-/dom-serialize-2.2.1.tgz" - integrity sha512-Yra4DbvoW7/Z6LBN560ZwXMjoNOSAN2wRsKFGc4iBeso+mpIA6qj1vfdf9HpMaKAqG6wXTy+1SYEzmNpKXOSsQ== - dependencies: - custom-event "~1.0.0" - ent "~2.2.0" - extend "^3.0.0" - void-elements "^2.0.0" - -dom-serializer@^1.0.1, dom-serializer@^1.3.2: - version "1.4.1" - resolved "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz" - integrity sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag== - dependencies: - domelementtype "^2.0.1" - domhandler "^4.2.0" - entities "^2.0.0" - -dom-serializer@^2.0.0: - 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" - -domelementtype@^2.0.1, domelementtype@^2.2.0, domelementtype@^2.3.0: - version "2.3.0" - resolved "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz" - integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw== - -domexception@^2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/domexception/-/domexception-2.0.1.tgz" - integrity sha512-yxJ2mFy/sibVQlu5qHjOkf9J3K6zgmCxgJ94u2EdvDOV09H+32LtRswEcUsmUWN72pVLOEnTSRaIVVzVQgS0dg== - dependencies: - webidl-conversions "^5.0.0" - -domexception@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz" - integrity sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw== - dependencies: - webidl-conversions "^7.0.0" - -domhandler@^3.3.0: - version "3.3.0" - resolved "https://registry.npmjs.org/domhandler/-/domhandler-3.3.0.tgz" - integrity sha512-J1C5rIANUbuYK+FuFL98650rihynUOEzRLxW+90bKZRWB6A1X1Tf82GxR1qAWLyfNPRvjqfip3Q5tdYlmAa9lA== - dependencies: - domelementtype "^2.0.1" - -domhandler@^4.0.0, domhandler@^4.2.0, domhandler@^4.3.1: - version "4.3.1" - resolved "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz" - integrity sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ== - dependencies: - domelementtype "^2.2.0" - -domhandler@^5.0.1, domhandler@^5.0.2, domhandler@^5.0.3: - 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" - -domino@^2.1.2: - version "2.1.6" - resolved "https://registry.npmjs.org/domino/-/domino-2.1.6.tgz" - integrity sha512-3VdM/SXBZX2omc9JF9nOPCtDaYQ67BGp5CoLpIQlO2KCAPETs8TcDHacF26jXadGbvUteZzRTeos2fhID5+ucQ== - -dompurify@^2.0.11: - version "2.4.5" - resolved "https://registry.npmjs.org/dompurify/-/dompurify-2.4.5.tgz" - integrity sha512-jggCCd+8Iqp4Tsz0nIvpcb22InKEBrGz5dw3EQJMs8HPJDsKbFIO3STYtAvCfDx26Muevn1MHVI0XxjgFfmiSA== - -domutils@^2.4.2, domutils@^2.5.2, domutils@^2.8.0: - version "2.8.0" - resolved "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz" - integrity sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A== - dependencies: - dom-serializer "^1.0.1" - domelementtype "^2.2.0" - domhandler "^4.2.0" - -domutils@^3.0.1: - version "3.0.1" - resolved "https://registry.npmjs.org/domutils/-/domutils-3.0.1.tgz" - integrity sha512-z08c1l761iKhDFtfXO04C7kTdPBLi41zwOZl00WS8b5eiaebNpY00HKbztwBq+e3vyqWNwWF3mP9YLUeqIrF+Q== - dependencies: - dom-serializer "^2.0.0" - domelementtype "^2.3.0" - domhandler "^5.0.1" - -duplexer@^0.1.2: - version "0.1.2" - resolved "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz" - integrity sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg== - -easy-extender@^2.3.4: - version "2.3.4" - resolved "https://registry.npmjs.org/easy-extender/-/easy-extender-2.3.4.tgz" - integrity sha512-8cAwm6md1YTiPpOvDULYJL4ZS6WfM5/cTeVVh4JsvyYZAoqlRVUpHL9Gr5Fy7HA6xcSZicUia3DeAgO3Us8E+Q== - dependencies: - lodash "^4.17.10" - -eazy-logger@^4.0.1: - version "4.0.1" - resolved "https://registry.npmjs.org/eazy-logger/-/eazy-logger-4.0.1.tgz" - integrity sha512-2GSFtnnC6U4IEKhEI7+PvdxrmjJ04mdsj3wHZTFiw0tUtG4HCWzTr13ZYTk8XOGnA1xQMaDljoBOYlk3D/MMSw== - dependencies: - chalk "4.1.2" - -ecc-jsbn@~0.1.1: - version "0.1.2" - resolved "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz" - integrity sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw== - dependencies: - jsbn "~0.1.0" - safer-buffer "^2.1.0" - -ee-first@1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz" - integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== - -ejs@^3.1.9: - version "3.1.9" - resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.9.tgz#03c9e8777fe12686a9effcef22303ca3d8eeb361" - integrity sha512-rC+QVNMJWv+MtPgkt0y+0rVEIdbtxVADApW9JXrUVlzHetgcyczP/E7DJmWJ4fJCZF2cPcBk0laWO9ZHMG3DmQ== - dependencies: - jake "^10.8.5" - -electron-to-chromium@^1.4.284: - version "1.4.357" - resolved "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.357.tgz" - integrity sha512-UTkCbNTAcGXABmEnQrGcW4m3cG6fcyBfD4KDF0iyEAlbrGZiY9dmslyDAGOD1Kr5biN2F743Y30aRCOtau35Vw== - -element-resize-detector@^1.2.1: - 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" - -emoji-regex@^8.0.0: - version "8.0.0" - resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz" - integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== - -emojis-list@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz" - integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q== - -encodeurl@~1.0.1, encodeurl@~1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz" - integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== - -encoding@^0.1.13: - version "0.1.13" - resolved "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz" - integrity sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A== - dependencies: - iconv-lite "^0.6.2" - -end-of-stream@^1.1.0: - 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== - dependencies: - once "^1.4.0" - -engine.io-client@~6.4.0: - version "6.4.0" - resolved "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.4.0.tgz" - integrity sha512-GyKPDyoEha+XZ7iEqam49vz6auPnNJ9ZBfy89f+rMMas8AuiMWOZ9PVzu8xb9ZC6rafUqiGHSCfu22ih66E+1g== - dependencies: - "@socket.io/component-emitter" "~3.1.0" - debug "~4.3.1" - engine.io-parser "~5.0.3" - ws "~8.11.0" - xmlhttprequest-ssl "~2.0.0" - -engine.io-parser@~5.0.3: - version "5.0.6" - resolved "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.6.tgz" - integrity sha512-tjuoZDMAdEhVnSFleYPCtdL2GXwVTGtNjoeJd9IhIG3C1xs9uwxqRNEu5WpnDZCaozwVlK/nuQhpodhXSIMaxw== - -engine.io@~6.4.1: - version "6.4.2" - resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-6.4.2.tgz#ffeaf68f69b1364b0286badddf15ff633476473f" - integrity sha512-FKn/3oMiJjrOEOeUub2WCox6JhxBXq/Zn3fZOMCBxKnNYtsdKjxhl7yR3fZhM9PV+rdE75SU5SYMc+2PGzo+Tg== - 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.4.1" - cors "~2.8.5" - debug "~4.3.1" - engine.io-parser "~5.0.3" - ws "~8.11.0" - -enhanced-resolve@^5.10.0: - version "5.12.0" - resolved "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.12.0.tgz" - integrity sha512-QHTXI/sZQmko1cbDoNAa3mJ5qhWUUNAq3vR0/YiD379fWQrcfuoX1+HW2S0MTt7XmoPLapdaDKUtelUSPic7hQ== - dependencies: - graceful-fs "^4.2.4" - tapable "^2.2.0" - -enquirer@^2.3.6: - version "2.3.6" - resolved "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz" - integrity sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg== - dependencies: - ansi-colors "^4.1.1" - -ent@~2.2.0: - version "2.2.0" - resolved "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz" - integrity sha512-GHrMyVZQWvTIdDtpiEXdHZnFQKzeO09apj8Cbl4pKWy4i0Oprcq17usfDt5aO63swf0JOeMWjWQE/LzgSRuWpA== - -entities@^2.0.0: - version "2.2.0" - resolved "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz" - integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A== - -entities@^4.2.0, entities@^4.3.0, entities@^4.4.0: - version "4.4.0" - resolved "https://registry.npmjs.org/entities/-/entities-4.4.0.tgz" - integrity sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA== - -entities@~3.0.1: - version "3.0.1" - resolved "https://registry.npmjs.org/entities/-/entities-3.0.1.tgz" - integrity sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q== - -env-paths@^2.2.0: - version "2.2.1" - resolved "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz" - integrity sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A== - -envinfo@^7.7.3: - version "7.8.1" - resolved "https://registry.npmjs.org/envinfo/-/envinfo-7.8.1.tgz" - integrity sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw== - -err-code@^2.0.2: - 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== - -errno@^0.1.1: - version "0.1.8" - resolved "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz" - integrity sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A== - dependencies: - prr "~1.0.1" - -error-ex@^1.3.1: - version "1.3.2" - resolved "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz" - integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== - dependencies: - is-arrayish "^0.2.1" - -error-stack-parser@^2.0.1: - version "2.1.4" - resolved "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz" - integrity sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ== - dependencies: - stackframe "^1.3.4" - -es-abstract@^1.19.0, es-abstract@^1.20.4: - version "1.21.2" - resolved "https://registry.npmjs.org/es-abstract/-/es-abstract-1.21.2.tgz" - integrity sha512-y/B5POM2iBnIxCiernH1G7rC9qQoM77lLIMQLuob0zhp8C56Po81+2Nj0WFKnd0pNReDTnkYryc+zhOzpEIROg== - dependencies: - array-buffer-byte-length "^1.0.0" - available-typed-arrays "^1.0.5" - call-bind "^1.0.2" - es-set-tostringtag "^2.0.1" - es-to-primitive "^1.2.1" - function.prototype.name "^1.1.5" - get-intrinsic "^1.2.0" - get-symbol-description "^1.0.0" - globalthis "^1.0.3" - gopd "^1.0.1" - has "^1.0.3" - has-property-descriptors "^1.0.0" - has-proto "^1.0.1" - has-symbols "^1.0.3" - internal-slot "^1.0.5" - is-array-buffer "^3.0.2" - is-callable "^1.2.7" - is-negative-zero "^2.0.2" - is-regex "^1.1.4" - is-shared-array-buffer "^1.0.2" - is-string "^1.0.7" - is-typed-array "^1.1.10" - is-weakref "^1.0.2" - object-inspect "^1.12.3" - object-keys "^1.1.1" - object.assign "^4.1.4" - regexp.prototype.flags "^1.4.3" - safe-regex-test "^1.0.0" - string.prototype.trim "^1.2.7" - string.prototype.trimend "^1.0.6" - string.prototype.trimstart "^1.0.6" - typed-array-length "^1.0.4" - unbox-primitive "^1.0.2" - which-typed-array "^1.1.9" - -es-get-iterator@^1.1.2: - version "1.1.3" - resolved "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz" - integrity sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw== - dependencies: - call-bind "^1.0.2" - get-intrinsic "^1.1.3" - has-symbols "^1.0.3" - is-arguments "^1.1.1" - is-map "^2.0.2" - is-set "^2.0.2" - is-string "^1.0.7" - isarray "^2.0.5" - stop-iteration-iterator "^1.0.0" - -es-module-lexer@^0.9.0: - version "0.9.3" - resolved "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.9.3.tgz" - integrity sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ== - -es-set-tostringtag@^2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz" - integrity sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg== - dependencies: - get-intrinsic "^1.1.3" - has "^1.0.3" - has-tostringtag "^1.0.0" - -es-shim-unscopables@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz" - integrity sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w== - dependencies: - has "^1.0.3" - -es-to-primitive@^1.2.1: - version "1.2.1" - resolved "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz" - integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA== - dependencies: - is-callable "^1.1.4" - is-date-object "^1.0.1" - is-symbol "^1.0.2" - -es6-promisify@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/es6-promisify/-/es6-promisify-7.0.0.tgz#9a710008dd6a4ab75a89e280bad787bfb749927b" - integrity sha512-ginqzK3J90Rd4/Yz7qRrqUeIpe3TwSXTPPZtPne7tGBPeAaQiU8qt4fpKApnxHcq1AwtUdHVg5P77x/yrggG8Q== - -esbuild-wasm@0.17.8: - version "0.17.8" - resolved "https://registry.npmjs.org/esbuild-wasm/-/esbuild-wasm-0.17.8.tgz" - integrity sha512-zCmpxv95E0FuCmvdw1K836UHnj4EdiQnFfjTby35y3LAjRPtXMj3sbHDRHjbD8Mqg5lTwq3knacr/1qIFU51CQ== - -esbuild@0.17.8: - version "0.17.8" - resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.17.8.tgz" - integrity sha512-g24ybC3fWhZddZK6R3uD2iF/RIPnRpwJAqLov6ouX3hMbY4+tKolP0VMF3zuIYCaXun+yHwS5IPQ91N2BT191g== - optionalDependencies: - "@esbuild/android-arm" "0.17.8" - "@esbuild/android-arm64" "0.17.8" - "@esbuild/android-x64" "0.17.8" - "@esbuild/darwin-arm64" "0.17.8" - "@esbuild/darwin-x64" "0.17.8" - "@esbuild/freebsd-arm64" "0.17.8" - "@esbuild/freebsd-x64" "0.17.8" - "@esbuild/linux-arm" "0.17.8" - "@esbuild/linux-arm64" "0.17.8" - "@esbuild/linux-ia32" "0.17.8" - "@esbuild/linux-loong64" "0.17.8" - "@esbuild/linux-mips64el" "0.17.8" - "@esbuild/linux-ppc64" "0.17.8" - "@esbuild/linux-riscv64" "0.17.8" - "@esbuild/linux-s390x" "0.17.8" - "@esbuild/linux-x64" "0.17.8" - "@esbuild/netbsd-x64" "0.17.8" - "@esbuild/openbsd-x64" "0.17.8" - "@esbuild/sunos-x64" "0.17.8" - "@esbuild/win32-arm64" "0.17.8" - "@esbuild/win32-ia32" "0.17.8" - "@esbuild/win32-x64" "0.17.8" - -escalade@^3.1.1: - version "3.1.1" - resolved "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz" - integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== - -escape-goat@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/escape-goat/-/escape-goat-3.0.0.tgz" - integrity sha512-w3PwNZJwRxlp47QGzhuEBldEqVHHhh8/tIPcl6ecf2Bou99cdAt0knihBV0Ecc7CGxYduXVBDheH1K2oADRlvw== - -escape-html@~1.0.3: - version "1.0.3" - resolved "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz" - integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== - -escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" - integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== - -escape-string-regexp@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz" - integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== - -escodegen@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/escodegen/-/escodegen-2.0.0.tgz" - integrity sha512-mmHKys/C8BFUGI+MAWNcSYoORYLMdPzjrknd2Vc+bUsjN5bXcr8EhrNB+UTqfL1y3I9c4fw2ihgtMPQLBRiQxw== - dependencies: - esprima "^4.0.1" - estraverse "^5.2.0" - esutils "^2.0.2" - optionator "^0.8.1" - optionalDependencies: - source-map "~0.6.1" - -eslint-import-resolver-node@^0.3.7: - version "0.3.7" - resolved "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.7.tgz" - integrity sha512-gozW2blMLJCeFpBwugLTGyvVjNoeo1knonXAcatC6bjPBZitotxdWf7Gimr25N4c0AAOo4eOUfaG82IJPDpqCA== - dependencies: - debug "^3.2.7" - is-core-module "^2.11.0" - resolve "^1.22.1" - -eslint-module-utils@^2.7.4: - version "2.7.4" - resolved "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.7.4.tgz" - integrity sha512-j4GT+rqzCoRKHwURX7pddtIPGySnX9Si/cgMI5ztrcqOPtk5dDEeZ34CQVPphnqkJytlc97Vuk05Um2mJ3gEQA== - dependencies: - debug "^3.2.7" - -eslint-plugin-deprecation@^1.4.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-deprecation/-/eslint-plugin-deprecation-1.4.1.tgz#09a2889210955fd1a5c37703922c01724aba80eb" - integrity sha512-4vxTghWzxsBukPJVQupi6xlTuDc8Pyi1QlRCrFiLgwLPMJQW3cJCNaehJUKQqQFvuue5m4W27e179Y3Qjzeghg== - dependencies: - "@typescript-eslint/utils" "^5.57.0" - tslib "^2.3.1" - tsutils "^3.21.0" - -eslint-plugin-import@^2.27.5: - version "2.27.5" - resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.27.5.tgz#876a6d03f52608a3e5bb439c2550588e51dd6c65" - integrity sha512-LmEt3GVofgiGuiE+ORpnvP+kAm3h6MLZJ4Q5HCyHADofsb4VzXFsRiWj3c0OFiV+3DWFh0qg3v9gcPlfc3zRow== - dependencies: - array-includes "^3.1.6" - array.prototype.flat "^1.3.1" - array.prototype.flatmap "^1.3.1" - debug "^3.2.7" - doctrine "^2.1.0" - eslint-import-resolver-node "^0.3.7" - eslint-module-utils "^2.7.4" - has "^1.0.3" - is-core-module "^2.11.0" - is-glob "^4.0.3" - minimatch "^3.1.2" - object.values "^1.1.6" - resolve "^1.22.1" - semver "^6.3.0" - tsconfig-paths "^3.14.1" - -eslint-plugin-jsdoc@^39.6.4: - version "39.9.1" - resolved "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-39.9.1.tgz" - integrity sha512-Rq2QY6BZP2meNIs48aZ3GlIlJgBqFCmR55+UBvaDkA3ZNQ0SvQXOs2QKkubakEijV8UbIVbVZKsOVN8G3MuqZw== - dependencies: - "@es-joy/jsdoccomment" "~0.36.1" - comment-parser "1.3.1" - debug "^4.3.4" - escape-string-regexp "^4.0.0" - esquery "^1.4.0" - semver "^7.3.8" - spdx-expression-parse "^3.0.1" - -eslint-plugin-jsonc@^2.6.0: - version "2.6.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-jsonc/-/eslint-plugin-jsonc-2.6.0.tgz#5a439ec15b4930c022bf157264a3d4f4712b982c" - integrity sha512-4bA9YTx58QaWalua1Q1b82zt7eZMB7i+ed8q8cKkbKP75ofOA2SXbtFyCSok7RY6jIXeCqQnKjN9If8zCgv6PA== - dependencies: - eslint-utils "^3.0.0" - jsonc-eslint-parser "^2.0.4" - natural-compare "^1.4.0" - -eslint-plugin-lodash@^7.4.0: - version "7.4.0" - resolved "https://registry.npmjs.org/eslint-plugin-lodash/-/eslint-plugin-lodash-7.4.0.tgz" - integrity sha512-Tl83UwVXqe1OVeBRKUeWcfg6/pCW1GTRObbdnbEJgYwjxp5Q92MEWQaH9+dmzbRt6kvYU1Mp893E79nJiCSM8A== - dependencies: - lodash "^4.17.21" - -eslint-plugin-unused-imports@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-2.0.0.tgz" - integrity sha512-3APeS/tQlTrFa167ThtP0Zm0vctjr4M44HMpeg1P4bK6wItarumq0Ma82xorMKdFsWpphQBlRPzw/pxiVELX1A== - dependencies: - eslint-rule-composer "^0.3.0" - -eslint-rule-composer@^0.3.0: - 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== - -eslint-scope@5.1.1, eslint-scope@^5.1.1: - version "5.1.1" - resolved "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz" - integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== - dependencies: - esrecurse "^4.3.0" - estraverse "^4.1.1" - -eslint-scope@^7.0.0: - version "7.1.1" - resolved "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz" - integrity sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw== - dependencies: - esrecurse "^4.3.0" - estraverse "^5.2.0" - -eslint-scope@^7.2.0: - version "7.2.0" - resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.2.0.tgz#f21ebdafda02352f103634b96dd47d9f81ca117b" - integrity sha512-DYj5deGlHBfMt15J7rdtyKNq/Nqlv5KfU4iodrQ019XESsRnwXH9KAE0y3cwtUHDo2ob7CypAnCqefh6vioWRw== - dependencies: - esrecurse "^4.3.0" - estraverse "^5.2.0" - -eslint-utils@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz" - integrity sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA== - dependencies: - eslint-visitor-keys "^2.0.0" - -eslint-visitor-keys@^2.0.0: - version "2.1.0" - resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz" - integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw== - -eslint-visitor-keys@^3.0.0, eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.0: - version "3.4.0" - resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.0.tgz" - integrity sha512-HPpKPUBQcAsZOsHAFwTtIKcYlCje62XB7SEAcxjtmW6TD1WVpkS6i6/hOVtTZIl4zGj/mBqpFVGvaDneik+VoQ== - -eslint@^8.39.0: - version "8.39.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.39.0.tgz#7fd20a295ef92d43809e914b70c39fd5a23cf3f1" - integrity sha512-mwiok6cy7KTW7rBpo05k6+p4YVZByLNjAZ/ACB9DRCu4YDRwjXI01tWHp6KAUWelsBetTxKK/2sHB0vdS8Z2Og== - dependencies: - "@eslint-community/eslint-utils" "^4.2.0" - "@eslint-community/regexpp" "^4.4.0" - "@eslint/eslintrc" "^2.0.2" - "@eslint/js" "8.39.0" - "@humanwhocodes/config-array" "^0.11.8" - "@humanwhocodes/module-importer" "^1.0.1" - "@nodelib/fs.walk" "^1.2.8" - ajv "^6.10.0" - 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.0" - eslint-visitor-keys "^3.4.0" - espree "^9.5.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" - grapheme-splitter "^1.0.4" - ignore "^5.2.0" - import-fresh "^3.0.0" - imurmurhash "^0.1.4" - is-glob "^4.0.0" - is-path-inside "^3.0.3" - js-sdsl "^4.1.4" - 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.1" - strip-ansi "^6.0.1" - strip-json-comments "^3.1.0" - text-table "^0.2.0" - -esm@^3.2.25: - version "3.2.25" - resolved "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz" - integrity sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA== - -espree@^9.0.0, espree@^9.5.1: - version "9.5.1" - resolved "https://registry.npmjs.org/espree/-/espree-9.5.1.tgz" - integrity sha512-5yxtHSZXRSW5pvv3hAlXM5+/Oswi1AUFqBmbibKb5s6bp3rGIDkyXU6xCoyuuLhijr4SFwPrXRoZjz0AZDN9tg== - dependencies: - acorn "^8.8.0" - acorn-jsx "^5.3.2" - eslint-visitor-keys "^3.4.0" - -esprima@^4.0.0, esprima@^4.0.1: - version "4.0.1" - resolved "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz" - integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== - -esquery@^1.4.0, esquery@^1.4.2: - version "1.5.0" - resolved "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz" - integrity sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg== - dependencies: - estraverse "^5.1.0" - -esrecurse@^4.3.0: - version "4.3.0" - resolved "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz" - integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== - dependencies: - estraverse "^5.2.0" - -estraverse@^4.1.1: - version "4.3.0" - resolved "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz" - integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== - -estraverse@^5.1.0, estraverse@^5.2.0: - version "5.3.0" - resolved "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz" - integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== - -esutils@^2.0.2: - version "2.0.3" - resolved "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz" - integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== - -etag@1.8.1, etag@^1.8.1, etag@~1.8.1: - version "1.8.1" - resolved "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz" - integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== - -eventemitter-asyncresource@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/eventemitter-asyncresource/-/eventemitter-asyncresource-1.0.0.tgz" - integrity sha512-39F7TBIV0G7gTelxwbEqnwhp90eqCPON1k0NwNfwhgKn4Co4ybUbj2pECcXT0B3ztRKZ7Pw1JujUUgmQJHcVAQ== - -eventemitter2@6.4.7: - version "6.4.7" - resolved "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.7.tgz" - integrity sha512-tYUSVOGeQPKt/eC1ABfhHy5Xd96N3oIijJvN3O9+TsC28T5V9yX9oEfEK5faP0EFSNVOG97qtAS68GBrQB2hDg== - -eventemitter3@^4.0.0: - version "4.0.7" - resolved "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz" - integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== - -events@^3.2.0: - version "3.3.0" - resolved "https://registry.npmjs.org/events/-/events-3.3.0.tgz" - integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== - -execa@4.1.0: - version "4.1.0" - resolved "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz" - integrity sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA== - 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" - -execa@^5.0.0: - version "5.1.1" - resolved "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz" - integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg== - 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" - -executable@^4.1.1: - version "4.1.1" - resolved "https://registry.npmjs.org/executable/-/executable-4.1.1.tgz" - integrity sha512-8iA79xD3uAch729dUG8xaaBBFGaEa0wdD2VkYLFHwlqosEj/jT66AzcreRDSgV7ehnNLBW2WR5jIXwGKjVdTLg== - dependencies: - pify "^2.2.0" - -express-rate-limit@^5.1.3: - version "5.5.1" - resolved "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-5.5.1.tgz" - integrity sha512-MTjE2eIbHv5DyfuFz4zLYWxpqVhEhkTiwFGuB74Q9CSou2WHO52nlE5y3Zlg6SIsiYUIPj6ifFxnkPz6O3sIUg== - -express-static-gzip@^2.1.7: - version "2.1.7" - resolved "https://registry.yarnpkg.com/express-static-gzip/-/express-static-gzip-2.1.7.tgz#5904824a07950ba741ec3a23a21839dd04c63506" - integrity sha512-QOCZUC+lhPPCjIJKpQGu1Oa61Axg9Mq09Qvit8Of7kzpMuwDeMSqjjQteQS3OVw/GkENBoSBheuQDWPlngImvw== - dependencies: - serve-static "^1.14.1" - -express@^4.17.3, express@^4.18.2: - version "4.18.2" - resolved "https://registry.npmjs.org/express/-/express-4.18.2.tgz" - integrity sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ== - dependencies: - accepts "~1.3.8" - array-flatten "1.1.1" - body-parser "1.20.1" - content-disposition "0.5.4" - content-type "~1.0.4" - cookie "0.5.0" - cookie-signature "1.0.6" - debug "2.6.9" - depd "2.0.0" - encodeurl "~1.0.2" - escape-html "~1.0.3" - etag "~1.8.1" - finalhandler "1.2.0" - fresh "0.5.2" - http-errors "2.0.0" - merge-descriptors "1.0.1" - methods "~1.1.2" - on-finished "2.4.1" - parseurl "~1.3.3" - path-to-regexp "0.1.7" - proxy-addr "~2.0.7" - qs "6.11.0" - range-parser "~1.2.1" - safe-buffer "5.2.1" - send "0.18.0" - serve-static "1.15.0" - setprototypeof "1.2.0" - statuses "2.0.1" - type-is "~1.6.18" - utils-merge "1.0.1" - vary "~1.1.2" - -extend@^3.0.0, extend@~3.0.2: - version "3.0.2" - resolved "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz" - integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== - -external-editor@^3.0.3: - version "3.1.0" - resolved "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz" - integrity sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew== - dependencies: - chardet "^0.7.0" - iconv-lite "^0.4.24" - tmp "^0.0.33" - -extract-zip@2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz" - integrity sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg== - dependencies: - debug "^4.1.1" - get-stream "^5.1.0" - yauzl "^2.10.0" - optionalDependencies: - "@types/yauzl" "^2.9.1" - -extsprintf@1.3.0, extsprintf@^1.2.0: - version "1.3.0" - resolved "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz" - integrity sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g== - -fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: - 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== - -fast-glob@^3.2.11, fast-glob@^3.2.4, fast-glob@^3.2.9: - version "3.2.12" - resolved "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz" - integrity sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w== - 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" - -fast-json-patch@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/fast-json-patch/-/fast-json-patch-3.1.1.tgz#85064ea1b1ebf97a3f7ad01e23f9337e72c66947" - integrity sha512-vf6IHUX2SBcA+5/+4883dsIjpBTqmfBjmYiWK1savxQmFk4JfBMLa7ynTYOs1Rolp/T1betJxHiGD3g1Mn8lUQ== - -fast-json-stable-stringify@2.1.0, fast-json-stable-stringify@^2.0.0: - 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== - -fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6: - version "2.0.6" - resolved "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz" - integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== - -fast-memoize@^2.5.1: - version "2.5.2" - resolved "https://registry.npmjs.org/fast-memoize/-/fast-memoize-2.5.2.tgz" - integrity sha512-Ue0LwpDYErFbmNnZSF0UH6eImUwDmogUO1jyE+JbN2gsQz/jICm1Ve7t9QT0rNSsfJt+Hs4/S3GnsDVjL4HVrw== - -fastest-levenshtein@^1.0.12: - version "1.0.16" - resolved "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz" - integrity sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg== - -fastq@^1.6.0: - version "1.15.0" - resolved "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz" - integrity sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw== - dependencies: - reusify "^1.0.4" - -faye-websocket@^0.11.3: - version "0.11.4" - resolved "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz" - integrity sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g== - dependencies: - websocket-driver ">=0.5.1" - -fd-slicer@~1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz" - integrity sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g== - dependencies: - pend "~1.2.0" - -figures@^3.0.0, figures@^3.2.0: - version "3.2.0" - resolved "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz" - integrity sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg== - dependencies: - escape-string-regexp "^1.0.5" - -file-entry-cache@^6.0.1: - version "6.0.1" - resolved "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz" - integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg== - dependencies: - flat-cache "^3.0.4" - -filelist@^1.0.1: - 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" - -filesize@^6.1.0: - version "6.4.0" - resolved "https://registry.npmjs.org/filesize/-/filesize-6.4.0.tgz" - integrity sha512-mjFIpOHC4jbfcTfoh4rkWpI31mF7viw9ikj/JyLoKzqlwG/YsefKfvYlYhdYdg/9mtK2z1AzgN/0LvVQ3zdlSQ== - -fill-range@^7.0.1: - version "7.0.1" - resolved "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz" - integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== - dependencies: - to-regex-range "^5.0.1" - -finalhandler@1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.0.tgz" - integrity sha512-ejnvM9ZXYzp6PUPUyQBMBf0Co5VX2gr5H2VQe2Ui2jWXNlxv+PYZo8wpAymJNJdLsG1R4p+M4aynF8KuoUEwRw== - 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" - -finalhandler@1.1.2: - version "1.1.2" - resolved "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz" - integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA== - 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" - -finalhandler@1.2.0: - version "1.2.0" - resolved "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz" - integrity sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg== - dependencies: - debug "2.6.9" - encodeurl "~1.0.2" - escape-html "~1.0.3" - on-finished "2.4.1" - parseurl "~1.3.3" - statuses "2.0.1" - unpipe "~1.0.0" - -find-cache-dir@^3.3.1, find-cache-dir@^3.3.2: - 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== - dependencies: - commondir "^1.0.1" - make-dir "^3.0.2" - pkg-dir "^4.1.0" - -find-up@^4.0.0, find-up@^4.1.0: - version "4.1.0" - resolved "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz" - integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== - dependencies: - locate-path "^5.0.0" - path-exists "^4.0.0" - -find-up@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz" - integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== - dependencies: - locate-path "^6.0.0" - path-exists "^4.0.0" - -flat-cache@^3.0.4: - version "3.0.4" - resolved "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz" - integrity sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg== - dependencies: - flatted "^3.1.0" - rimraf "^3.0.2" - -flatted@^3.1.0, flatted@^3.2.7: - version "3.2.7" - resolved "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz" - integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ== - -follow-redirects@^1.0.0, follow-redirects@^1.14.0, follow-redirects@^1.14.9: - version "1.15.2" - resolved "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz" - integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== - -for-each@^0.3.3: - version "0.3.3" - resolved "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz" - integrity sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw== - dependencies: - is-callable "^1.1.3" - -forever-agent@~0.6.1: - version "0.6.1" - resolved "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz" - integrity sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw== - -form-data@^3.0.0: - version "3.0.1" - resolved "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz" - integrity sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg== - dependencies: - asynckit "^0.4.0" - combined-stream "^1.0.8" - mime-types "^2.1.12" - -form-data@^4.0.0: - 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" - -form-data@~2.3.2: - version "2.3.3" - resolved "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz" - integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ== - dependencies: - asynckit "^0.4.0" - combined-stream "^1.0.6" - mime-types "^2.1.12" - -forwarded@0.2.0: - version "0.2.0" - resolved "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz" - integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== - -fraction.js@^4.2.0: - version "4.2.0" - resolved "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz" - integrity sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA== - -fresh@0.5.2, fresh@^0.5.2: - version "0.5.2" - resolved "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz" - integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== - -fs-extra@3.0.1: - version "3.0.1" - resolved "https://registry.npmjs.org/fs-extra/-/fs-extra-3.0.1.tgz" - integrity sha512-V3Z3WZWVUYd8hoCL5xfXJCaHWYzmtwW5XWYSlLgERi8PWd8bx1kUHUk8L1BT57e49oKnDDD180mjfrHc1yA9rg== - dependencies: - graceful-fs "^4.1.2" - jsonfile "^3.0.0" - universalify "^0.1.0" - -fs-extra@^8.1.0: - version "8.1.0" - resolved "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz" - integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g== - dependencies: - graceful-fs "^4.2.0" - jsonfile "^4.0.0" - universalify "^0.1.0" - -fs-extra@^9.1.0: - version "9.1.0" - resolved "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz" - integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ== - dependencies: - at-least-node "^1.0.0" - graceful-fs "^4.2.0" - jsonfile "^6.0.1" - universalify "^2.0.0" - -fs-minipass@^2.0.0, fs-minipass@^2.1.0: - version "2.1.0" - resolved "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz" - integrity sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg== - dependencies: - minipass "^3.0.0" - -fs-minipass@^3.0.0: - version "3.0.1" - resolved "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.1.tgz" - integrity sha512-MhaJDcFRTuLidHrIttu0RDGyyXs/IYHVmlcxfLAEFIWjc1vdLAkdwT7Ace2u7DbitWC0toKMl5eJZRYNVreIMw== - dependencies: - minipass "^4.0.0" - -fs-monkey@^1.0.3: - version "1.0.3" - resolved "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.3.tgz" - integrity sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q== - -fs.realpath@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz" - integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== - -fscreen@^1.0.1: - version "1.2.0" - resolved "https://registry.npmjs.org/fscreen/-/fscreen-1.2.0.tgz" - integrity sha512-hlq4+BU0hlPmwsFjwGGzZ+OZ9N/wq9Ljg/sq3pX+2CD7hrJsX9tJgWWK/wiNTFM212CLHWhicOoqwXyZGGetJg== - -fsevents@~2.3.2: - version "2.3.2" - resolved "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz" - integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== - -function-bind@^1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz" - integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== - -function.prototype.name@^1.1.5: - version "1.1.5" - resolved "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz" - integrity sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.3" - es-abstract "^1.19.0" - functions-have-names "^1.2.2" - -functions-have-names@^1.2.2: - version "1.2.3" - resolved "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz" - integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== - -gauge@^4.0.3: - version "4.0.4" - resolved "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz" - integrity sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg== - dependencies: - aproba "^1.0.3 || ^2.0.0" - color-support "^1.1.3" - console-control-strings "^1.1.0" - has-unicode "^2.0.1" - signal-exit "^3.0.7" - string-width "^4.2.3" - strip-ansi "^6.0.1" - wide-align "^1.1.5" - -gensync@^1.0.0-beta.2: - 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== - -get-caller-file@^2.0.5: - version "2.0.5" - resolved "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz" - integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== - -get-intrinsic@^1.0.2, get-intrinsic@^1.1.1, get-intrinsic@^1.1.3, get-intrinsic@^1.2.0: - version "1.2.0" - resolved "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz" - integrity sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q== - dependencies: - function-bind "^1.1.1" - has "^1.0.3" - has-symbols "^1.0.3" - -get-package-type@^0.1.0: - version "0.1.0" - resolved "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz" - integrity sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q== - -get-stream@^5.0.0, get-stream@^5.1.0: - version "5.2.0" - resolved "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz" - integrity sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA== - dependencies: - pump "^3.0.0" - -get-stream@^6.0.0: - version "6.0.1" - resolved "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz" - integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== - -get-symbol-description@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz" - integrity sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw== - dependencies: - call-bind "^1.0.2" - get-intrinsic "^1.1.1" - -getos@^3.2.1: - version "3.2.1" - resolved "https://registry.npmjs.org/getos/-/getos-3.2.1.tgz" - integrity sha512-U56CfOK17OKgTVqozZjUKNdkfEv6jk5WISBJ8SHoagjE6L69zOwl3Z+O8myjY9MEW3i2HPWQBt/LTbCgcC973Q== - dependencies: - async "^3.2.0" - -getpass@^0.1.1: - version "0.1.7" - resolved "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz" - integrity sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng== - dependencies: - assert-plus "^1.0.0" - -glob-parent@^5.1.1, glob-parent@^5.1.2, glob-parent@~5.1.2: - 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" - -glob-parent@^6.0.1, glob-parent@^6.0.2: - version "6.0.2" - resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz" - integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== - dependencies: - is-glob "^4.0.3" - -glob-to-regexp@^0.4.1: - 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== - -glob@8.1.0, glob@^8.0.1: - version "8.1.0" - resolved "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz" - integrity sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^5.0.1" - once "^1.3.0" - -glob@^7.0.3: - version "7.2.3" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" - integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== - 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" - -glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, glob@^7.1.7, glob@~7.2.0: - version "7.2.0" - resolved "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz" - integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.0.4" - once "^1.3.0" - path-is-absolute "^1.0.0" - -glob@^9.3.0, glob@^9.3.1: - version "9.3.5" - resolved "https://registry.npmjs.org/glob/-/glob-9.3.5.tgz" - integrity sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q== - dependencies: - fs.realpath "^1.0.0" - minimatch "^8.0.2" - minipass "^4.2.4" - path-scurry "^1.6.1" - -global-dirs@^3.0.0: - version "3.0.1" - resolved "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.1.tgz" - integrity sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA== - dependencies: - ini "2.0.0" - -globals@^11.1.0: - version "11.12.0" - resolved "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz" - integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== - -globals@^13.19.0: - version "13.20.0" - resolved "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz" - integrity sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ== - dependencies: - type-fest "^0.20.2" - -globalthis@^1.0.3: - version "1.0.3" - resolved "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz" - integrity sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA== - dependencies: - define-properties "^1.1.3" - -globby@^11.0.1, globby@^11.1.0: - version "11.1.0" - resolved "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz" - integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== - 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" - -globby@^13.1.1: - version "13.1.4" - resolved "https://registry.npmjs.org/globby/-/globby-13.1.4.tgz" - integrity sha512-iui/IiiW+QrJ1X1hKH5qwlMQyv34wJAYwH1vrf8b9kBA4sNiif3gKsMHa+BrdnOpEudWjpotfa7LrTzB1ERS/g== - dependencies: - dir-glob "^3.0.1" - fast-glob "^3.2.11" - ignore "^5.2.0" - merge2 "^1.4.1" - slash "^4.0.0" - -globby@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/globby/-/globby-5.0.0.tgz#ebd84667ca0dbb330b99bcfc68eac2bc54370e0d" - integrity sha512-HJRTIH2EeH44ka+LWig+EqT2ONSYpVlNfx6pyd592/VF1TbfljJ7elwie7oSwcViLGqOdWocSdu2txwBF9bjmQ== - dependencies: - array-union "^1.0.1" - arrify "^1.0.0" - glob "^7.0.3" - object-assign "^4.0.1" - pify "^2.0.0" - pinkie-promise "^2.0.0" - -gopd@^1.0.1: - 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" - -graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4, graceful-fs@^4.2.6, graceful-fs@^4.2.9: - 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== - -grapheme-splitter@^1.0.4: - version "1.0.4" - resolved "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz" - integrity sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ== - -guess-parser@^0.4.22: - version "0.4.22" - resolved "https://registry.npmjs.org/guess-parser/-/guess-parser-0.4.22.tgz" - integrity sha512-KcUWZ5ACGaBM69SbqwVIuWGoSAgD+9iJnchR9j/IarVI1jHVeXv+bUXBIMeqVMSKt3zrn0Dgf9UpcOEpPBLbSg== - dependencies: - "@wessberg/ts-evaluator" "0.0.27" - -gzip-size@^6.0.0: - version "6.0.0" - resolved "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz" - integrity sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q== - dependencies: - duplexer "^0.1.2" - -handle-thing@^2.0.0: - version "2.0.1" - resolved "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz" - integrity sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg== - -har-schema@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" - integrity sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q== - -har-validator@~5.1.3: - version "5.1.5" - resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.5.tgz#1f0803b9f8cb20c0fa13822df1ecddb36bde1efd" - integrity sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w== - dependencies: - ajv "^6.12.3" - har-schema "^2.0.0" - -has-ansi@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" - integrity sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg== - dependencies: - ansi-regex "^2.0.0" - -has-bigints@^1.0.1, has-bigints@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz" - integrity sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ== - -has-flag@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz" - integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw== - -has-flag@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz" - integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== - -has-property-descriptors@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz" - integrity sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ== - dependencies: - get-intrinsic "^1.1.1" - -has-proto@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz" - integrity sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg== - -has-symbols@^1.0.2, has-symbols@^1.0.3: - version "1.0.3" - resolved "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz" - integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== - -has-tostringtag@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz" - integrity sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ== - dependencies: - has-symbols "^1.0.2" - -has-unicode@^2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz" - integrity sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ== - -has@^1.0.3: - version "1.0.3" - resolved "https://registry.npmjs.org/has/-/has-1.0.3.tgz" - integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== - dependencies: - function-bind "^1.1.1" - -hdr-histogram-js@^2.0.1: - version "2.0.3" - resolved "https://registry.npmjs.org/hdr-histogram-js/-/hdr-histogram-js-2.0.3.tgz" - integrity sha512-Hkn78wwzWHNCp2uarhzQ2SGFLU3JY8SBDDd3TAABK4fc30wm+MuPOrg5QVFVfkKOQd6Bfz3ukJEI+q9sXEkK1g== - dependencies: - "@assemblyscript/loader" "^0.10.1" - base64-js "^1.2.0" - pako "^1.0.3" - -hdr-histogram-percentiles-obj@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/hdr-histogram-percentiles-obj/-/hdr-histogram-percentiles-obj-3.0.0.tgz" - integrity sha512-7kIufnBqdsBGcSZLPJwqHT3yhk1QTsSlFsVD3kx5ixH/AlgBs9yM1q6DPhXZ8f8gtdqgh7N7/5btRLpQsS2gHw== - -hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2: - 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" - -hosted-git-info@^6.0.0: - version "6.1.1" - resolved "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-6.1.1.tgz" - integrity sha512-r0EI+HBMcXadMrugk0GCQ+6BQV39PiWAZVfq7oIckeGiN7sjRGyQxPdft3nQekFTCQbYxLBH+/axZMeH8UX6+w== - dependencies: - lru-cache "^7.5.1" - -hpack.js@^2.1.6: - version "2.1.6" - resolved "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz" - integrity sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ== - dependencies: - inherits "^2.0.1" - obuf "^1.0.0" - readable-stream "^2.0.1" - wbuf "^1.1.0" - -html-encoding-sniffer@^2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz" - integrity sha512-D5JbOMBIR/TVZkubHT+OyT2705QvogUW4IBn6nHd756OwieSF9aDYFj4dv6HHEVGYbHaLETa3WggZYWWMyy3ZQ== - dependencies: - whatwg-encoding "^1.0.5" - -html-encoding-sniffer@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz" - integrity sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA== - dependencies: - whatwg-encoding "^2.0.0" - -html-entities@^2.3.2: - version "2.3.3" - resolved "https://registry.npmjs.org/html-entities/-/html-entities-2.3.3.tgz" - integrity sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA== - -html-escaper@^2.0.0: - version "2.0.2" - resolved "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz" - integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== - -html-parse-stringify@^3.0.1: - 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" - -htmlparser2@^5.0.0: - version "5.0.1" - resolved "https://registry.npmjs.org/htmlparser2/-/htmlparser2-5.0.1.tgz" - integrity sha512-vKZZra6CSe9qsJzh0BjBGXo8dvzNsq/oGvsjfRdOrrryfeD9UOBEEQdeoqCRmKZchF5h2zOBMQ6YuQ0uRUmdbQ== - dependencies: - domelementtype "^2.0.1" - domhandler "^3.3.0" - domutils "^2.4.2" - entities "^2.0.0" - -htmlparser2@^6.1.0: - version "6.1.0" - resolved "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz" - integrity sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A== - dependencies: - domelementtype "^2.0.1" - domhandler "^4.0.0" - domutils "^2.5.2" - entities "^2.0.0" - -htmlparser2@^8.0.0: - version "8.0.2" - resolved "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz" - integrity sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA== - dependencies: - domelementtype "^2.3.0" - domhandler "^5.0.3" - domutils "^3.0.1" - entities "^4.4.0" - -http-cache-semantics@^4.1.0, http-cache-semantics@^4.1.1: - 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== - -http-deceiver@^1.2.7: - version "1.2.7" - resolved "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz" - integrity sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw== - -http-errors@2.0.0: - 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" - -http-errors@~1.6.2: - version "1.6.3" - resolved "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz" - integrity sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A== - dependencies: - depd "~1.1.2" - inherits "2.0.3" - setprototypeof "1.1.0" - statuses ">= 1.4.0 < 2" - -http-parser-js@>=0.5.1: - version "0.5.8" - resolved "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.8.tgz" - integrity sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q== - -http-proxy-agent@^4.0.1: - version "4.0.1" - resolved "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz" - integrity sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg== - dependencies: - "@tootallnate/once" "1" - agent-base "6" - debug "4" - -http-proxy-agent@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz" - integrity sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w== - dependencies: - "@tootallnate/once" "2" - agent-base "6" - debug "4" - -http-proxy-middleware@^1.0.5: - version "1.3.1" - resolved "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-1.3.1.tgz" - integrity sha512-13eVVDYS4z79w7f1+NPllJtOQFx/FdUW4btIvVRMaRlUY9VGstAbo5MOhLEuUgZFRHn3x50ufn25zkj/boZnEg== - dependencies: - "@types/http-proxy" "^1.17.5" - http-proxy "^1.18.1" - is-glob "^4.0.1" - is-plain-obj "^3.0.0" - micromatch "^4.0.2" - -http-proxy-middleware@^2.0.3, http-proxy-middleware@^2.0.6: - version "2.0.6" - resolved "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz" - integrity sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw== - 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" - -http-proxy@^1.18.1: - 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" - -http-signature@~1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" - integrity sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ== - dependencies: - assert-plus "^1.0.0" - jsprim "^1.2.2" - sshpk "^1.7.0" - -http-signature@~1.3.6: - version "1.3.6" - resolved "https://registry.npmjs.org/http-signature/-/http-signature-1.3.6.tgz" - integrity sha512-3adrsD6zqo4GsTqtO7FyrejHNv+NgiIfAfv68+jVlFmSr9OGy7zrxONceFRLKvnnZA5jbxQBX1u9PpB6Wi32Gw== - dependencies: - assert-plus "^1.0.0" - jsprim "^2.0.2" - sshpk "^1.14.1" - -https-proxy-agent@5.0.1, https-proxy-agent@^5.0.0, https-proxy-agent@^5.0.1: - version "5.0.1" - resolved "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz" - integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA== - dependencies: - agent-base "6" - debug "4" - -human-signals@^1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz" - integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw== - -human-signals@^2.1.0: - version "2.1.0" - resolved "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz" - integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== - -humanize-ms@^1.2.1: - version "1.2.1" - resolved "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz" - integrity sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ== - dependencies: - ms "^2.0.0" - -hyphenate-style-name@^1.0.3: - version "1.0.4" - resolved "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz" - integrity sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ== - -i18next@^19.5.0: - 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" - -icomcom-react@^1.0.1: - 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" - -iconv-lite@0.4.24, iconv-lite@^0.4.24: - 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" - -iconv-lite@0.6.3, iconv-lite@^0.6.2, iconv-lite@^0.6.3: - version "0.6.3" - resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz" - integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== - dependencies: - safer-buffer ">= 2.1.2 < 3.0.0" - -icss-utils@^5.0.0, icss-utils@^5.1.0: - version "5.1.0" - resolved "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz" - integrity sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA== - -ieee754@^1.1.13: - version "1.2.1" - resolved "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz" - integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== - -ignore-by-default@^1.0.1: - 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== - -ignore-walk@^6.0.0: - version "6.0.2" - resolved "https://registry.npmjs.org/ignore-walk/-/ignore-walk-6.0.2.tgz" - integrity sha512-ezmQ1Dg2b3jVZh2Dh+ar6Eu2MqNSTkyb32HU2MAQQQX9tKM3q/UQ/9lf03lQ5hW+fOeoMnwxwkleZ0xcNp0/qg== - dependencies: - minimatch "^7.4.2" - -ignore@5.2.4, ignore@^5.2.0: - version "5.2.4" - resolved "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz" - integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ== - -image-size@~0.5.0: - 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== - -immutability-helper@^3.0.1: - version "3.1.1" - resolved "https://registry.npmjs.org/immutability-helper/-/immutability-helper-3.1.1.tgz" - integrity sha512-Q0QaXjPjwIju/28TsugCHNEASwoCcJSyJV3uO1sOIQGI0jKgm9f41Lvz0DZj3n46cNCyAZTsEYoY4C2bVRUzyQ== - -immutable@^3: - version "3.8.2" - resolved "https://registry.npmjs.org/immutable/-/immutable-3.8.2.tgz" - integrity sha512-15gZoQ38eYjEjxkorfbcgBKBL6R7T459OuK+CpcWt7O3KF4uPCx2tD0uFETlUDIyo+1789crbMhTvQBSR5yBMg== - -immutable@^4.0.0: - version "4.3.0" - resolved "https://registry.npmjs.org/immutable/-/immutable-4.3.0.tgz" - integrity sha512-0AOCmOip+xgJwEVTQj1EfiDDOkPmuyllDuTuEX+DDXUgapLAsBIfkg3sxCYyCEA8mQqZrrxPUGjcOQ2JS3WLkg== - -import-fresh@^3.0.0, import-fresh@^3.2.1: - version "3.3.0" - resolved "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz" - integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== - dependencies: - parent-module "^1.0.0" - resolve-from "^4.0.0" - -import-local@^3.0.2: - version "3.1.0" - resolved "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz" - integrity sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg== - dependencies: - pkg-dir "^4.2.0" - resolve-cwd "^3.0.0" - -imurmurhash@^0.1.4: - version "0.1.4" - resolved "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz" - integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== - -indent-string@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz" - integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== - -infer-owner@^1.0.4: - version "1.0.4" - resolved "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz" - integrity sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A== - -inflight@^1.0.4: - version "1.0.6" - resolved "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz" - integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== - dependencies: - once "^1.3.0" - wrappy "1" - -inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3: - version "2.0.4" - resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" - integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== - -inherits@2.0.3: - version "2.0.3" - resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz" - integrity sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw== - -ini@2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz" - integrity sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA== - -ini@3.0.1: - version "3.0.1" - resolved "https://registry.npmjs.org/ini/-/ini-3.0.1.tgz" - integrity sha512-it4HyVAUTKBc6m8e1iXWvXSTdndF7HbdN713+kvLrymxTaU4AUBWrJ4vEooP+V7fexnVD3LKcBshjGGPefSMUQ== - -ini@^1.3.4: - version "1.3.8" - resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" - integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== - -inquirer@8.2.4: - version "8.2.4" - resolved "https://registry.npmjs.org/inquirer/-/inquirer-8.2.4.tgz" - integrity sha512-nn4F01dxU8VeKfq192IjLsxu0/OmMZ4Lg3xKAns148rCaXP6ntAoEkVYZThWjwON8AlzdZZi6oqnhNbxUG9hVg== - dependencies: - ansi-escapes "^4.2.1" - chalk "^4.1.1" - cli-cursor "^3.1.0" - cli-width "^3.0.0" - external-editor "^3.0.3" - figures "^3.0.0" - lodash "^4.17.21" - mute-stream "0.0.8" - ora "^5.4.1" - run-async "^2.4.0" - rxjs "^7.5.5" - string-width "^4.1.0" - strip-ansi "^6.0.0" - through "^2.3.6" - wrap-ansi "^7.0.0" - -internal-slot@^1.0.4, internal-slot@^1.0.5: - version "1.0.5" - resolved "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz" - integrity sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ== - dependencies: - get-intrinsic "^1.2.0" - has "^1.0.3" - side-channel "^1.0.4" - -interpret@^2.2.0: - version "2.2.0" - resolved "https://registry.npmjs.org/interpret/-/interpret-2.2.0.tgz" - integrity sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw== - -intersection-observer@^0.10.0: - version "0.10.0" - resolved "https://registry.npmjs.org/intersection-observer/-/intersection-observer-0.10.0.tgz" - integrity sha512-fn4bQ0Xq8FTej09YC/jqKZwtijpvARlRp6wxL5WTA6yPe2YWSJ5RJh7Nm79rK2qB0wr6iDQzH60XGq5V/7u8YQ== - -invariant@^2.2.4: - 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" - -ip@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz" - integrity sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ== - -ipaddr.js@1.9.1: - 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== - -ipaddr.js@^2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.0.1.tgz" - integrity sha512-1qTgH9NG+IIJ4yfKs2e6Pp1bZg8wbDbKHT21HrLIeYBTRLgMYKnMTPAuI3Lcs61nfx5h1xlXnbJtH1kX5/d/ng== - -is-arguments@^1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz" - integrity sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA== - dependencies: - call-bind "^1.0.2" - has-tostringtag "^1.0.0" - -is-array-buffer@^3.0.1, is-array-buffer@^3.0.2: - version "3.0.2" - resolved "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz" - integrity sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w== - dependencies: - call-bind "^1.0.2" - get-intrinsic "^1.2.0" - is-typed-array "^1.1.10" - -is-arrayish@^0.2.1: - version "0.2.1" - resolved "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz" - integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg== - -is-bigint@^1.0.1: - version "1.0.4" - resolved "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz" - integrity sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg== - dependencies: - has-bigints "^1.0.1" - -is-binary-path@~2.1.0: - 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" - -is-boolean-object@^1.1.0: - version "1.1.2" - resolved "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz" - integrity sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA== - dependencies: - call-bind "^1.0.2" - has-tostringtag "^1.0.0" - -is-buffer@~1.1.6: - version "1.1.6" - resolved "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz" - integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== - -is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.2.7: - version "1.2.7" - resolved "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz" - integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== - -is-ci@^3.0.0: - version "3.0.1" - resolved "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz" - integrity sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ== - dependencies: - ci-info "^3.2.0" - -is-core-module@^2.11.0, is-core-module@^2.8.1, is-core-module@^2.9.0: - version "2.12.0" - resolved "https://registry.npmjs.org/is-core-module/-/is-core-module-2.12.0.tgz" - integrity sha512-RECHCBCd/viahWmwj6enj19sKbHfJrddi/6cBDsNTKbNq0f7VeaUkBo60BqzvPqo/W54ChS62Z5qyun7cfOMqQ== - dependencies: - has "^1.0.3" - -is-date-object@^1.0.1, is-date-object@^1.0.5: - version "1.0.5" - resolved "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz" - integrity sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ== - dependencies: - has-tostringtag "^1.0.0" - -is-docker@^2.0.0, is-docker@^2.1.1: - version "2.2.1" - resolved "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz" - integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ== - -is-extglob@^2.1.1: - version "2.1.1" - resolved "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz" - integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== - -is-fullwidth-code-point@^3.0.0: - 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== - -is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: - 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" - -is-in-browser@^1.0.2, is-in-browser@^1.1.3: - 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== - -is-installed-globally@~0.4.0: - version "0.4.0" - resolved "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.4.0.tgz" - integrity sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ== - dependencies: - global-dirs "^3.0.0" - is-path-inside "^3.0.2" - -is-interactive@^1.0.0: - 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== - -is-lambda@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz" - integrity sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ== - -is-map@^2.0.1, is-map@^2.0.2: - version "2.0.2" - resolved "https://registry.npmjs.org/is-map/-/is-map-2.0.2.tgz" - integrity sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg== - -is-negative-zero@^2.0.2: - version "2.0.2" - resolved "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz" - integrity sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA== - -is-number-like@^1.0.3: - version "1.0.8" - resolved "https://registry.npmjs.org/is-number-like/-/is-number-like-1.0.8.tgz" - integrity sha512-6rZi3ezCyFcn5L71ywzz2bS5b2Igl1En3eTlZlvKjpz1n3IZLAYMbKYAIQgFmEu0GENg92ziU/faEOA/aixjbA== - dependencies: - lodash.isfinite "^3.3.2" - -is-number-object@^1.0.4: - version "1.0.7" - resolved "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz" - integrity sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ== - dependencies: - has-tostringtag "^1.0.0" - -is-number@^7.0.0: - version "7.0.0" - resolved "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz" - integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== - -is-path-cwd@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-1.0.0.tgz#d225ec23132e89edd38fda767472e62e65f1106d" - integrity sha512-cnS56eR9SPAscL77ik76ATVqoPARTqPIVkMDVxRaWH06zT+6+CzIroYRJ0VVvm0Z1zfAvxvz9i/D3Ppjaqt5Nw== - -is-path-in-cwd@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-path-in-cwd/-/is-path-in-cwd-1.0.1.tgz#5ac48b345ef675339bd6c7a48a912110b241cf52" - integrity sha512-FjV1RTW48E7CWM7eE/J2NJvAEEVektecDBVBE5Hh3nM1Jd0kvhHtX68Pr3xsDf857xt3Y4AkwVULK1Vku62aaQ== - dependencies: - is-path-inside "^1.0.0" - -is-path-inside@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-1.0.1.tgz#8ef5b7de50437a3fdca6b4e865ef7aa55cb48036" - integrity sha512-qhsCR/Esx4U4hg/9I19OVUAJkGWtjRYHMRgUMZE2TDdj+Ag+kttZanLupfddNyglzz50cUlmWzUaI37GDfNx/g== - dependencies: - path-is-inside "^1.0.1" - -is-path-inside@^3.0.2, is-path-inside@^3.0.3: - 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== - -is-plain-obj@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz" - integrity sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA== - -is-plain-object@^2.0.4: - 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== - dependencies: - isobject "^3.0.1" - -is-plain-object@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz" - integrity sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q== - -is-potential-custom-element-name@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz" - integrity sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ== - -is-regex@^1.1.4: - version "1.1.4" - resolved "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz" - integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg== - dependencies: - call-bind "^1.0.2" - has-tostringtag "^1.0.0" - -is-set@^2.0.1, is-set@^2.0.2: - version "2.0.2" - resolved "https://registry.npmjs.org/is-set/-/is-set-2.0.2.tgz" - integrity sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g== - -is-shared-array-buffer@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz" - integrity sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA== - dependencies: - call-bind "^1.0.2" - -is-stream@^2.0.0: - version "2.0.1" - resolved "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz" - integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== - -is-string@^1.0.5, is-string@^1.0.7: - version "1.0.7" - resolved "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz" - integrity sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg== - dependencies: - has-tostringtag "^1.0.0" - -is-symbol@^1.0.2, is-symbol@^1.0.3: - version "1.0.4" - resolved "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz" - integrity sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg== - dependencies: - has-symbols "^1.0.2" - -is-typed-array@^1.1.10, is-typed-array@^1.1.9: - version "1.1.10" - resolved "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.10.tgz" - integrity sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A== - dependencies: - available-typed-arrays "^1.0.5" - call-bind "^1.0.2" - for-each "^0.3.3" - gopd "^1.0.1" - has-tostringtag "^1.0.0" - -is-typedarray@~1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz" - integrity sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA== - -is-unicode-supported@^0.1.0: - version "0.1.0" - resolved "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz" - integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== - -is-weakmap@^2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.1.tgz" - integrity sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA== - -is-weakref@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz" - integrity sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ== - dependencies: - call-bind "^1.0.2" - -is-weakset@^2.0.1: - version "2.0.2" - resolved "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.2.tgz" - integrity sha512-t2yVvttHkQktwnNNmBQ98AhENLdPUTDTE21uPqAQ0ARwQfGeQKRVS0NNurH7bTf7RrvcVn1OOge45CnBeHCSmg== - dependencies: - call-bind "^1.0.2" - get-intrinsic "^1.1.1" - -is-what@^3.14.1: - version "3.14.1" - resolved "https://registry.npmjs.org/is-what/-/is-what-3.14.1.tgz" - integrity sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA== - -is-wsl@^1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz" - integrity sha512-gfygJYZ2gLTDlmbWMI0CE2MwnFzSN/2SZfkMlItC4K/JBlsWVDB0bO6XhqcY13YXE7iMcAJnzTCJjPiTeJJ0Mw== - -is-wsl@^2.2.0: - version "2.2.0" - resolved "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz" - integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww== - dependencies: - is-docker "^2.0.0" - -isarray@^2.0.5: - version "2.0.5" - resolved "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz" - integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw== - -isarray@~1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz" - integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== - -isbinaryfile@^4.0.8: - version "4.0.10" - resolved "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz" - integrity sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw== - -isbot@^3.6.10: - version "3.6.10" - resolved "https://registry.yarnpkg.com/isbot/-/isbot-3.6.10.tgz#7b66334e81794f0461794debb567975cf08eaf2b" - integrity sha512-+I+2998oyP4oW9+OTQD8TS1r9P6wv10yejukj+Ksj3+UR5pUhsZN3f8W7ysq0p1qxpOVNbl5mCuv0bCaF8y5iQ== - -isexe@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz" - integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== - -isobject@^3.0.1: - version "3.0.1" - resolved "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz" - integrity sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg== - -isomorphic-unfetch@^3.0.0: - 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" - -isstream@~0.1.2: - version "0.1.2" - resolved "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz" - integrity sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g== - -istanbul-lib-coverage@^2.0.5: - version "2.0.5" - resolved "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz" - integrity sha512-8aXznuEPCJvGnMSRft4udDRDtb1V3pkQkMMI5LI+6HuQz5oQ4J2UFn1H82raA3qJtyOLkkwVqICBQkjnGtn5mA== - -istanbul-lib-coverage@^3.0.0, istanbul-lib-coverage@^3.2.0: - version "3.2.0" - resolved "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz" - integrity sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw== - -istanbul-lib-instrument@^5.0.4: - version "5.2.1" - resolved "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz" - integrity sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg== - 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" - -istanbul-lib-report@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz" - integrity sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw== - dependencies: - istanbul-lib-coverage "^3.0.0" - make-dir "^3.0.0" - supports-color "^7.1.0" - -istanbul-lib-source-maps@^3.0.6: - 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== - 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" - -istanbul-reports@^3.0.2: - version "3.1.5" - resolved "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.5.tgz" - integrity sha512-nUsEMa9pBt/NOHqbcbeJEgqIlY/K7rVWUX6Lql2orY5e9roQOthbR3vtY4zzf2orPELg80fnxxk9zUyPlgwD1w== - dependencies: - html-escaper "^2.0.0" - istanbul-lib-report "^3.0.0" - -jake@^10.8.5: - version "10.8.5" - resolved "https://registry.npmjs.org/jake/-/jake-10.8.5.tgz" - integrity sha512-sVpxYeuAhWt0OTWITwT98oyV0GsXyMlXCF+3L1SuafBVUIr/uILGRB+NqwkzhgXKvoJpDIpQvqkUALgdmQsQxw== - dependencies: - async "^3.2.3" - chalk "^4.0.2" - filelist "^1.0.1" - minimatch "^3.0.4" - -jasmine-core@^3.6.0, jasmine-core@^3.8.0: - version "3.99.1" - resolved "https://registry.npmjs.org/jasmine-core/-/jasmine-core-3.99.1.tgz" - integrity sha512-Hu1dmuoGcZ7AfyynN3LsfruwMbxMALMka+YtZeGoLuDEySVmVAPaonkNoBRIw/ectu8b9tVQCJNgp4a4knp+tg== - -jasmine-marbles@0.9.2: - version "0.9.2" - resolved "https://registry.npmjs.org/jasmine-marbles/-/jasmine-marbles-0.9.2.tgz" - integrity sha512-T7RjG4fRsdiGGzbQZ6Kj39qYt6O1/KIcR4FkUNsD3DUGkd/AzpwzN+xtk0DXlLWEz5BaVdK1SzMgQDVw879c4Q== - dependencies: - lodash "^4.17.20" - -jest-worker@^27.4.5: - version "27.5.1" - resolved "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz" - integrity sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg== - dependencies: - "@types/node" "*" - merge-stream "^2.0.0" - supports-color "^8.0.0" - -js-cookie@2.2.1: - version "2.2.1" - resolved "https://registry.npmjs.org/js-cookie/-/js-cookie-2.2.1.tgz" - integrity sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ== - -js-sdsl@^4.1.4: - version "4.4.0" - resolved "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.4.0.tgz" - integrity sha512-FfVSdx6pJ41Oa+CF7RDaFmTnCaFhua+SNYQX74riGOpl96x+2jQCqEfQ2bnXu/5DPCqlRuiqyvTJM0Qjz26IVg== - -"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz" - integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== - -js-yaml@^3.13.1: - version "3.14.1" - resolved "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz" - integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g== - dependencies: - argparse "^1.0.7" - esprima "^4.0.0" - -js-yaml@^4.1.0: - 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" - -jsbn@~0.1.0: - version "0.1.1" - resolved "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz" - integrity sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg== - -jsdoc-type-pratt-parser@~3.1.0: - version "3.1.0" - resolved "https://registry.npmjs.org/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-3.1.0.tgz" - integrity sha512-MgtD0ZiCDk9B+eI73BextfRrVQl0oyzRG8B2BjORts6jbunj4ScKPcyXGTbB6eXL4y9TzxCm6hyeLq/2ASzNdw== - -jsdom@21.1.0: - version "21.1.0" - resolved "https://registry.npmjs.org/jsdom/-/jsdom-21.1.0.tgz" - integrity sha512-m0lzlP7qOtthD918nenK3hdItSd2I+V3W9IrBcB36sqDwG+KnUs66IF5GY7laGWUnlM9vTsD0W1QwSEBYWWcJg== - dependencies: - abab "^2.0.6" - acorn "^8.8.1" - acorn-globals "^7.0.0" - cssom "^0.5.0" - cssstyle "^2.3.0" - data-urls "^3.0.2" - decimal.js "^10.4.2" - domexception "^4.0.0" - escodegen "^2.0.0" - form-data "^4.0.0" - html-encoding-sniffer "^3.0.0" - http-proxy-agent "^5.0.0" - https-proxy-agent "^5.0.1" - is-potential-custom-element-name "^1.0.1" - nwsapi "^2.2.2" - parse5 "^7.1.1" - saxes "^6.0.0" - symbol-tree "^3.2.4" - tough-cookie "^4.1.2" - w3c-xmlserializer "^4.0.0" - webidl-conversions "^7.0.0" - whatwg-encoding "^2.0.0" - whatwg-mimetype "^3.0.0" - whatwg-url "^11.0.0" - ws "^8.11.0" - xml-name-validator "^4.0.0" - -jsdom@^16.4.0: - version "16.7.0" - resolved "https://registry.npmjs.org/jsdom/-/jsdom-16.7.0.tgz" - integrity sha512-u9Smc2G1USStM+s/x1ru5Sxrl6mPYCbByG1U/hUmqaVsm4tbNyS7CicOSRyuGQYZhTu0h84qkZZQ/I+dzizSVw== - dependencies: - abab "^2.0.5" - acorn "^8.2.4" - acorn-globals "^6.0.0" - cssom "^0.4.4" - cssstyle "^2.3.0" - data-urls "^2.0.0" - decimal.js "^10.2.1" - domexception "^2.0.1" - escodegen "^2.0.0" - form-data "^3.0.0" - html-encoding-sniffer "^2.0.1" - http-proxy-agent "^4.0.1" - https-proxy-agent "^5.0.0" - is-potential-custom-element-name "^1.0.1" - nwsapi "^2.2.0" - parse5 "6.0.1" - saxes "^5.0.1" - symbol-tree "^3.2.4" - tough-cookie "^4.0.0" - w3c-hr-time "^1.0.2" - w3c-xmlserializer "^2.0.0" - webidl-conversions "^6.1.0" - whatwg-encoding "^1.0.5" - whatwg-mimetype "^2.3.0" - whatwg-url "^8.5.0" - ws "^7.4.6" - xml-name-validator "^3.0.0" - -jsesc@^2.5.1: - version "2.5.2" - resolved "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz" - integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== - -jsesc@~0.5.0: - version "0.5.0" - resolved "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz" - integrity sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA== - -json-parse-even-better-errors@^2.3.0, json-parse-even-better-errors@^2.3.1: - 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== - -json-parse-even-better-errors@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.0.tgz" - integrity sha512-iZbGHafX/59r39gPwVPRBGw0QQKnA7tte5pSMrhWOW7swGsVvVTjmfyAV9pNqk8YGT7tRCdxRu8uzcgZwoDooA== - -json-schema-traverse@^0.4.1: - 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== - -json-schema-traverse@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz" - integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== - -json-schema@0.4.0: - version "0.4.0" - resolved "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz" - integrity sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA== - -json-stable-stringify-without-jsonify@^1.0.1: - 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== - -json-stringify-safe@~5.0.1: - version "5.0.1" - resolved "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz" - integrity sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA== - -json5@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz" - integrity sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA== - dependencies: - minimist "^1.2.0" - -json5@^2.1.2, json5@^2.2.1, json5@^2.2.2, json5@^2.2.3: - version "2.2.3" - resolved "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz" - integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== - -jsonc-eslint-parser@^2.0.4: - version "2.1.0" - resolved "https://registry.yarnpkg.com/jsonc-eslint-parser/-/jsonc-eslint-parser-2.1.0.tgz#4c126b530aa583d85308d0b3041ff81ce402bbb2" - integrity sha512-qCRJWlbP2v6HbmKW7R3lFbeiVWHo+oMJ0j+MizwvauqnCV/EvtAeEeuCgoc/ErtsuoKgYB8U4Ih8AxJbXoE6/g== - dependencies: - acorn "^8.5.0" - eslint-visitor-keys "^3.0.0" - espree "^9.0.0" - semver "^7.3.5" - -jsonc-parser@3.0.0, jsonc-parser@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.0.0.tgz" - integrity sha512-fQzRfAbIBnR0IQvftw9FJveWiHp72Fg20giDrHz6TdfB12UH/uue0D3hm57UB5KgAVuniLMCaS8P1IMj9NR7cA== - -jsonc-parser@3.2.0: - version "3.2.0" - resolved "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz" - integrity sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w== - -jsonfile@^3.0.0: - version "3.0.1" - resolved "https://registry.npmjs.org/jsonfile/-/jsonfile-3.0.1.tgz" - integrity sha512-oBko6ZHlubVB5mRFkur5vgYR1UyqX+S6Y/oCfLhqNdcc2fYFlDpIoNc7AfKS1KOGcnNAkvsr0grLck9ANM815w== - optionalDependencies: - graceful-fs "^4.1.6" - -jsonfile@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz" - integrity sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg== - optionalDependencies: - graceful-fs "^4.1.6" - -jsonfile@^6.0.1: - version "6.1.0" - resolved "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz" - integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ== - dependencies: - universalify "^2.0.0" - optionalDependencies: - graceful-fs "^4.1.6" - -jsonparse@^1.3.1: - version "1.3.1" - resolved "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz" - integrity sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg== - -jsonschema@1.4.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/jsonschema/-/jsonschema-1.4.1.tgz#cc4c3f0077fb4542982973d8a083b6b34f482dab" - integrity sha512-S6cATIPVv1z0IlxdN+zUk5EPjkGCdnhN4wVSBlvoUO1tOLJootbo9CquNJmbIh4yikWHiUedhRYrNPn1arpEmQ== - -jsprim@^1.2.2: - version "1.4.2" - resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.2.tgz#712c65533a15c878ba59e9ed5f0e26d5b77c5feb" - integrity sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw== - dependencies: - assert-plus "1.0.0" - extsprintf "1.3.0" - json-schema "0.4.0" - verror "1.10.0" - -jsprim@^2.0.2: - version "2.0.2" - resolved "https://registry.npmjs.org/jsprim/-/jsprim-2.0.2.tgz" - integrity sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ== - dependencies: - assert-plus "1.0.0" - extsprintf "1.3.0" - json-schema "0.4.0" - verror "1.10.0" - -jss-plugin-camel-case@^10.5.1: - 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" - -jss-plugin-default-unit@^10.5.1: - 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" - -jss-plugin-global@^10.5.1: - 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" - -jss-plugin-nested@^10.5.1: - 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" - -jss-plugin-props-sort@^10.5.1: - 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" - -jss-plugin-rule-value-function@^10.5.1: - 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" - -jss-plugin-vendor-prefixer@^10.5.1: - 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" - -jss-rtl@^0.3.0: - 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" - -jss@10.10.0, jss@^10.3.0, jss@^10.5.1: - 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" - -juice@^8.0.0: - version "8.1.0" - resolved "https://registry.npmjs.org/juice/-/juice-8.1.0.tgz" - integrity sha512-FLzurJrx5Iv1e7CfBSZH68dC04EEvXvvVvPYB7Vx1WAuhCp1ZPIMtqxc+WTWxVkpTIC2Ach/GAv0rQbtGf6YMA== - dependencies: - cheerio "1.0.0-rc.10" - commander "^6.1.0" - mensch "^0.3.4" - slick "^1.12.2" - web-resource-inliner "^6.0.1" - -jwt-decode@^3.1.2: - 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== - -karma-chrome-launcher@~3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/karma-chrome-launcher/-/karma-chrome-launcher-3.2.0.tgz#eb9c95024f2d6dfbb3748d3415ac9b381906b9a9" - integrity sha512-rE9RkUPI7I9mAxByQWkGJFXfFD6lE4gC5nPuZdobf/QdTEJI6EU4yIay/cfU/xV4ZxlM5JiTv7zWYgA64NpS5Q== - dependencies: - which "^1.2.1" - -karma-coverage-istanbul-reporter@~3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/karma-coverage-istanbul-reporter/-/karma-coverage-istanbul-reporter-3.0.3.tgz#f3b5303553aadc8e681d40d360dfdc19bc7e9fe9" - integrity sha512-wE4VFhG/QZv2Y4CdAYWDbMmcAHeS926ZIji4z+FkB2aF/EposRb6DP6G5ncT/wXhqUfAb/d7kZrNKPonbvsATw== - 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" - -karma-jasmine-html-reporter@^1.5.0: - 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== - -karma-jasmine@~4.0.0: - version "4.0.2" - resolved "https://registry.npmjs.org/karma-jasmine/-/karma-jasmine-4.0.2.tgz" - integrity sha512-ggi84RMNQffSDmWSyyt4zxzh2CQGwsxvYYsprgyR1j8ikzIduEdOlcLvXjZGwXG/0j41KUXOWsUCBfbEHPWP9g== - dependencies: - jasmine-core "^3.6.0" - -karma-mocha-reporter@2.2.5: - version "2.2.5" - resolved "https://registry.npmjs.org/karma-mocha-reporter/-/karma-mocha-reporter-2.2.5.tgz" - integrity sha512-Hr6nhkIp0GIJJrvzY8JFeHpQZNseuIakGac4bpw8K1+5F0tLb6l7uvXRa8mt2Z+NVwYgCct4QAfp2R2QP6o00w== - dependencies: - chalk "^2.1.0" - log-symbols "^2.1.0" - strip-ansi "^4.0.0" - -karma-source-map-support@1.4.0: - 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== - dependencies: - source-map-support "^0.5.5" - -karma@^6.4.2: - version "6.4.2" - resolved "https://registry.yarnpkg.com/karma/-/karma-6.4.2.tgz#a983f874cee6f35990c4b2dcc3d274653714de8e" - integrity sha512-C6SU/53LB31BEgRg+omznBEMY4SjHU3ricV6zBcAe1EeILKkeScr+fZXtaI5WyDbkVowJxxAI6h73NcFPmXolQ== - 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.4.1" - source-map "^0.6.1" - tmp "^0.2.1" - ua-parser-js "^0.7.30" - yargs "^16.1.1" - -kind-of@^6.0.2: - version "6.0.3" - resolved "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz" - integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== - -klaro@^0.7.18: - version "0.7.18" - resolved "https://registry.npmjs.org/klaro/-/klaro-0.7.18.tgz" - integrity sha512-Q5nehkGeZuFerisW4oeeB1ax6dHDszWckR70EcxKvhFiJQ61CM0WBSo20yLbdvz8N9MFsOFu4RNCO9JsbkCxgQ== - -kleur@^3.0.3: - version "3.0.3" - resolved "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz" - integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== - -klona@^2.0.4, klona@^2.0.5: - version "2.0.6" - resolved "https://registry.npmjs.org/klona/-/klona-2.0.6.tgz" - integrity sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA== - -launch-editor@^2.6.0: - version "2.6.0" - resolved "https://registry.npmjs.org/launch-editor/-/launch-editor-2.6.0.tgz" - integrity sha512-JpDCcQnyAAzZZaZ7vEiSqL690w7dAEyLao+KC96zBplnYbJS7TYNjvM3M7y3dGz+v7aIsJk3hllWuc0kWAjyRQ== - dependencies: - picocolors "^1.0.0" - shell-quote "^1.7.3" - -lazy-ass@^1.6.0: - 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== - -less-loader@11.1.0: - version "11.1.0" - resolved "https://registry.npmjs.org/less-loader/-/less-loader-11.1.0.tgz" - integrity sha512-C+uDBV7kS7W5fJlUjq5mPBeBVhYpTIm5gB09APT9o3n/ILeaXVsiSFTbZpTJCJwQ/Crczfn3DmfQFwxYusWFug== - dependencies: - klona "^2.0.4" - -less@4.1.3: - version "4.1.3" - resolved "https://registry.npmjs.org/less/-/less-4.1.3.tgz" - integrity sha512-w16Xk/Ta9Hhyei0Gpz9m7VS8F28nieJaL/VyShID7cYvP6IL5oHeL6p4TXSDJqZE/lNv0oJ2pGVjJsRkfwm5FA== - dependencies: - copy-anything "^2.0.1" - parse-node-version "^1.0.1" - tslib "^2.3.0" - 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" - -levn@^0.4.1: - version "0.4.1" - resolved "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz" - integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ== - dependencies: - prelude-ls "^1.2.1" - type-check "~0.4.0" - -levn@~0.3.0: - version "0.3.0" - resolved "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz" - integrity sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA== - dependencies: - prelude-ls "~1.1.2" - type-check "~0.3.2" - -license-webpack-plugin@4.0.2: - version "4.0.2" - resolved "https://registry.npmjs.org/license-webpack-plugin/-/license-webpack-plugin-4.0.2.tgz" - integrity sha512-771TFWFD70G1wLTC4oU2Cw4qvtmNrIw+wRvBtn+okgHl7slJVi7zfNcdmqDL72BojM30VNJ2UHylr1o77U37Jw== - dependencies: - webpack-sources "^3.0.0" - -limiter@^1.0.5: - version "1.1.5" - resolved "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz" - integrity sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA== - -lines-and-columns@^1.1.6: - version "1.2.4" - resolved "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz" - integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== - -linkify-it@^4.0.1: - 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" - -listr2@^3.8.3: - version "3.14.0" - resolved "https://registry.npmjs.org/listr2/-/listr2-3.14.0.tgz" - integrity sha512-TyWI8G99GX9GjE54cJ+RrNMcIFBfwMPxc3XTFiAYGN4s10hWROGtOg7+O6u6LE3mNkyld7RSLE6nrKBvTfcs3g== - 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" - -loader-runner@^4.2.0: - version "4.3.0" - resolved "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz" - integrity sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg== - -loader-utils@3.2.1: - version "3.2.1" - resolved "https://registry.npmjs.org/loader-utils/-/loader-utils-3.2.1.tgz" - integrity sha512-ZvFw1KWS3GVyYBYb7qkmRM/WwL2TQQBxgCK62rlvm4WpVQ23Nb4tYjApUlfjrEGvOs7KHEsmyUn75OHZrJMWPw== - -loader-utils@^2.0.0: - version "2.0.4" - resolved "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz" - integrity sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw== - dependencies: - big.js "^5.2.2" - emojis-list "^3.0.0" - json5 "^2.1.2" - -localtunnel@^2.0.1: - version "2.0.2" - resolved "https://registry.npmjs.org/localtunnel/-/localtunnel-2.0.2.tgz" - integrity sha512-n418Cn5ynvJd7m/N1d9WVJISLJF/ellZnfsLnx8WBWGzxv/ntNcFkJ1o6se5quUhCplfLGBNL5tYHiq5WF3Nug== - dependencies: - axios "0.21.4" - debug "4.3.2" - openurl "1.1.1" - yargs "17.1.1" - -locate-path@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz" - integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== - dependencies: - p-locate "^4.1.0" - -locate-path@^6.0.0: - version "6.0.0" - resolved "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz" - integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== - dependencies: - p-locate "^5.0.0" - -lodash.debounce@^4.0.8: - version "4.0.8" - resolved "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz" - integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow== - -lodash.isfinite@^3.3.2: - version "3.3.2" - resolved "https://registry.npmjs.org/lodash.isfinite/-/lodash.isfinite-3.3.2.tgz" - integrity sha512-7FGG40uhC8Mm633uKW1r58aElFlBlxCrg9JfSi3P6aYiWmfiWF0PgMd86ZUsxE5GwWPdHoS2+48bwTh2VPkIQA== - -lodash.merge@^4.6.2: - version "4.6.2" - resolved "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz" - integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== - -lodash.once@^4.1.1: - version "4.1.1" - resolved "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz" - integrity sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg== - -lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.7.0: - version "4.17.21" - resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz" - integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== - -log-symbols@^2.1.0: - version "2.2.0" - resolved "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz" - integrity sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg== - dependencies: - chalk "^2.0.1" - -log-symbols@^4.0.0, log-symbols@^4.1.0: - version "4.1.0" - resolved "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz" - integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg== - dependencies: - chalk "^4.1.0" - is-unicode-supported "^0.1.0" - -log-update@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/log-update/-/log-update-4.0.0.tgz" - integrity sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg== - dependencies: - ansi-escapes "^4.3.0" - cli-cursor "^3.1.0" - slice-ansi "^4.0.0" - wrap-ansi "^6.2.0" - -log4js@^6.4.1: - version "6.9.1" - resolved "https://registry.npmjs.org/log4js/-/log4js-6.9.1.tgz" - integrity sha512-1somDdy9sChrr9/f4UlzhdaGfDR2c/SaD2a4T7qEkG4jTS57/B3qmnjLYePwQ8cqWnUHZI0iAKxMBpCZICiZ2g== - dependencies: - date-format "^4.0.14" - debug "^4.3.4" - flatted "^3.2.7" - rfdc "^1.3.0" - streamroller "^3.1.5" - -loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0: - 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" - -lru-cache@^5.1.1: - 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" - -lru-cache@^6.0.0: - 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" - -lru-cache@^7.14.1, lru-cache@^7.4.4, lru-cache@^7.5.1, lru-cache@^7.7.1: - version "7.18.3" - resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz" - integrity sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA== - -lru-cache@^9.0.0: - version "9.0.1" - resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-9.0.1.tgz" - integrity sha512-C8QsKIN1UIXeOs3iWmiZ1lQY+EnKDojWd37fXy1aSbJvH4iSma1uy2OWuoB3m4SYRli5+CUjDv3Dij5DVoetmg== - -magic-string@0.25.7: - version "0.25.7" - resolved "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz" - integrity sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA== - dependencies: - sourcemap-codec "^1.4.4" - -magic-string@0.29.0: - version "0.29.0" - resolved "https://registry.npmjs.org/magic-string/-/magic-string-0.29.0.tgz" - integrity sha512-WcfidHrDjMY+eLjlU+8OvwREqHwpgCeKVBUpQ3OhYYuvfaYCUgcbuBzappNzZvg/v8onU3oQj+BYpkOJe9Iw4Q== - dependencies: - "@jridgewell/sourcemap-codec" "^1.4.13" - -magic-string@^0.27.0: - version "0.27.0" - resolved "https://registry.npmjs.org/magic-string/-/magic-string-0.27.0.tgz" - integrity sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA== - dependencies: - "@jridgewell/sourcemap-codec" "^1.4.13" - -make-dir@^2.1.0: - version "2.1.0" - resolved "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz" - integrity sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA== - dependencies: - pify "^4.0.1" - semver "^5.6.0" - -make-dir@^3.0.0, make-dir@^3.0.2: - 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== - dependencies: - semver "^6.0.0" - -make-error@^1.1.1: - version "1.3.6" - resolved "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz" - integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== - -make-fetch-happen@^10.0.3: - version "10.2.1" - resolved "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-10.2.1.tgz" - integrity sha512-NgOPbRiaQM10DYXvN3/hhGVI2M5MtITFryzBGxHM5p4wnFxsVCbxkrBrDsk+EZ5OB4jEOT7AjDxtdF+KVEFT7w== - dependencies: - agentkeepalive "^4.2.1" - cacache "^16.1.0" - http-cache-semantics "^4.1.0" - http-proxy-agent "^5.0.0" - https-proxy-agent "^5.0.0" - is-lambda "^1.0.1" - lru-cache "^7.7.1" - minipass "^3.1.6" - minipass-collect "^1.0.2" - minipass-fetch "^2.0.3" - minipass-flush "^1.0.5" - minipass-pipeline "^1.2.4" - negotiator "^0.6.3" - promise-retry "^2.0.1" - socks-proxy-agent "^7.0.0" - ssri "^9.0.0" - -make-fetch-happen@^11.0.0, make-fetch-happen@^11.0.1: - version "11.0.3" - resolved "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-11.0.3.tgz" - integrity sha512-oPLh5m10lRNNZDjJ2kP8UpboUx2uFXVaVweVe/lWut4iHWcQEmfqSVJt2ihZsFI8HbpwyyocaXbCAWf0g1ukIA== - dependencies: - agentkeepalive "^4.2.1" - cacache "^17.0.0" - http-cache-semantics "^4.1.1" - http-proxy-agent "^5.0.0" - https-proxy-agent "^5.0.0" - is-lambda "^1.0.1" - lru-cache "^7.7.1" - minipass "^4.0.0" - minipass-fetch "^3.0.0" - minipass-flush "^1.0.5" - minipass-pipeline "^1.2.4" - negotiator "^0.6.3" - promise-retry "^2.0.1" - socks-proxy-agent "^7.0.0" - ssri "^10.0.0" - -manifesto.js@^4.2.0: - 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" - -markdown-it-mathjax3@^4.3.2: - version "4.3.2" - resolved "https://registry.yarnpkg.com/markdown-it-mathjax3/-/markdown-it-mathjax3-4.3.2.tgz#1e34aa86f8560b283fd283008334adc2d6b05a37" - integrity sha512-TX3GW5NjmupgFtMJGRauioMbbkGsOXAAt1DZ/rzzYmTHqzkO1rNAdiMD4NiruurToPApn2kYy76x02QN26qr2w== - dependencies: - juice "^8.0.0" - mathjax-full "^3.2.0" - -markdown-it@^13.0.1: - version "13.0.1" - resolved "https://registry.npmjs.org/markdown-it/-/markdown-it-13.0.1.tgz" - integrity sha512-lTlxriVoy2criHP0JKRhO2VDG9c2ypWCsT237eDiLqi09rmbKoUetyGHq2uOIRoRS//kfoJckS0eUzzkDR+k2Q== - dependencies: - argparse "^2.0.1" - entities "~3.0.1" - linkify-it "^4.0.1" - mdurl "^1.0.1" - uc.micro "^1.0.5" - -mathjax-full@^3.2.0: - version "3.2.2" - resolved "https://registry.npmjs.org/mathjax-full/-/mathjax-full-3.2.2.tgz" - integrity sha512-+LfG9Fik+OuI8SLwsiR02IVdjcnRCy5MufYLi0C3TdMT56L/pjB0alMVGgoWJF8pN9Rc7FESycZB9BMNWIid5w== - dependencies: - esm "^3.2.25" - mhchemparser "^4.1.0" - mj-context-menu "^0.6.1" - speech-rule-engine "^4.0.6" - -md5@^2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/md5/-/md5-2.3.0.tgz#c3da9a6aae3a30b46b7b0c349b87b110dc3bda4f" - integrity sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g== - dependencies: - charenc "0.0.2" - crypt "0.0.2" - is-buffer "~1.1.6" - -mdurl@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz" - integrity sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g== - -media-typer@0.3.0: - version "0.3.0" - resolved "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz" - integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== - -memfs@^3.4.12, memfs@^3.4.3: - version "3.5.0" - resolved "https://registry.npmjs.org/memfs/-/memfs-3.5.0.tgz" - integrity sha512-yK6o8xVJlQerz57kvPROwTMgx5WtGwC2ZxDtOUsnGl49rHjYkfQoPNZPCKH73VdLE1BwBu/+Fx/NL8NYMUw2aA== - dependencies: - fs-monkey "^1.0.3" - -"memoize-one@>=3.1.1 <6", memoize-one@^5.1.1: - 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== - -mensch@^0.3.4: - version "0.3.4" - resolved "https://registry.npmjs.org/mensch/-/mensch-0.3.4.tgz" - integrity sha512-IAeFvcOnV9V0Yk+bFhYR07O3yNina9ANIN5MoXBKYJ/RLYPurd2d0yw14MDhpr9/momp0WofT1bPUh3hkzdi/g== - -merge-descriptors@1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz" - integrity sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w== - -merge-stream@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz" - integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== - -merge2@^1.3.0, merge2@^1.4.1: - version "1.4.1" - resolved "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz" - integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== - -methods@~1.1.2: - version "1.1.2" - resolved "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz" - integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== - -mhchemparser@^4.1.0: - version "4.1.1" - resolved "https://registry.npmjs.org/mhchemparser/-/mhchemparser-4.1.1.tgz" - integrity sha512-R75CUN6O6e1t8bgailrF1qPq+HhVeFTM3XQ0uzI+mXTybmphy3b6h4NbLOYhemViQ3lUs+6CKRkC3Ws1TlYREA== - -micromatch@^4.0.2, micromatch@^4.0.4: - version "4.0.5" - resolved "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz" - integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== - dependencies: - braces "^3.0.2" - picomatch "^2.3.1" - -mime-db@1.52.0, "mime-db@>= 1.43.0 < 2": - version "1.52.0" - resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz" - integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== - -mime-types@^2.1.12, mime-types@^2.1.27, mime-types@^2.1.31, mime-types@~2.1.17, mime-types@~2.1.19, mime-types@~2.1.24, mime-types@~2.1.34: - 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" - -mime@1.4.1: - version "1.4.1" - resolved "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz" - integrity sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ== - -mime@1.6.0, mime@^1.4.1: - version "1.6.0" - resolved "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz" - integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== - -mime@^2.4.6, mime@^2.5.2: - version "2.6.0" - resolved "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz" - integrity sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg== - -mimic-fn@^2.1.0: - version "2.1.0" - resolved "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz" - integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== - -mini-css-extract-plugin@2.7.2: - version "2.7.2" - resolved "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.7.2.tgz" - integrity sha512-EdlUizq13o0Pd+uCp+WO/JpkLvHRVGt97RqfeGhXqAcorYo1ypJSpkV+WDT0vY/kmh/p7wRdJNJtuyK540PXDw== - dependencies: - schema-utils "^4.0.0" - -minimalistic-assert@^1.0.0: - version "1.0.1" - resolved "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz" - integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== - -minimatch@^3.0.2, minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: - 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" - -minimatch@^5.0.1: - 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" - -minimatch@^7.4.2: - version "7.4.6" - resolved "https://registry.npmjs.org/minimatch/-/minimatch-7.4.6.tgz" - integrity sha512-sBz8G/YjVniEz6lKPNpKxXwazJe4c19fEfV2GDMX6AjFz+MX9uDWIZW8XreVhkFW3fkIdTv/gxWr/Kks5FFAVw== - dependencies: - brace-expansion "^2.0.1" - -minimatch@^8.0.2, minimatch@^8.0.3: - version "8.0.4" - resolved "https://registry.npmjs.org/minimatch/-/minimatch-8.0.4.tgz" - integrity sha512-W0Wvr9HyFXZRGIDgCicunpQ299OKXs9RgZfaukz4qAW/pJhcpUfupc9c+OObPOFueNy8VSrZgEmDtk6Kh4WzDA== - dependencies: - brace-expansion "^2.0.1" - -minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.6, minimist@^1.2.8: - version "1.2.8" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" - integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== - -minipass-collect@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz" - integrity sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA== - dependencies: - minipass "^3.0.0" - -minipass-fetch@^2.0.3: - version "2.1.2" - resolved "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-2.1.2.tgz" - integrity sha512-LT49Zi2/WMROHYoqGgdlQIZh8mLPZmOrN2NdJjMXxYe4nkN6FUyuPuOAOedNJDrx0IRGg9+4guZewtp8hE6TxA== - dependencies: - minipass "^3.1.6" - minipass-sized "^1.0.3" - minizlib "^2.1.2" - optionalDependencies: - encoding "^0.1.13" - -minipass-fetch@^3.0.0: - version "3.0.1" - resolved "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-3.0.1.tgz" - integrity sha512-t9/wowtf7DYkwz8cfMSt0rMwiyNIBXf5CKZ3S5ZMqRqMYT0oLTp0x1WorMI9WTwvaPg21r1JbFxJMum8JrLGfw== - dependencies: - minipass "^4.0.0" - minipass-sized "^1.0.3" - minizlib "^2.1.2" - optionalDependencies: - encoding "^0.1.13" - -minipass-flush@^1.0.5: - version "1.0.5" - resolved "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz" - integrity sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw== - dependencies: - minipass "^3.0.0" - -minipass-json-stream@^1.0.1: - 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== - dependencies: - jsonparse "^1.3.1" - minipass "^3.0.0" - -minipass-pipeline@^1.2.2, minipass-pipeline@^1.2.4: - version "1.2.4" - resolved "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz" - integrity sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A== - dependencies: - minipass "^3.0.0" - -minipass-sized@^1.0.3: - version "1.0.3" - resolved "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz" - integrity sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g== - dependencies: - minipass "^3.0.0" - -minipass@^3.0.0, minipass@^3.1.1, minipass@^3.1.6: - version "3.3.6" - resolved "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz" - integrity sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw== - dependencies: - yallist "^4.0.0" - -minipass@^4.0.0, minipass@^4.2.4: - version "4.2.8" - resolved "https://registry.npmjs.org/minipass/-/minipass-4.2.8.tgz" - integrity sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ== - -minipass@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz" - integrity sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ== - -minizlib@^2.1.1, minizlib@^2.1.2: - version "2.1.2" - resolved "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz" - integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg== - dependencies: - minipass "^3.0.0" - yallist "^4.0.0" - -mirador-dl-plugin@^0.13.0: - version "0.13.0" - resolved "https://registry.npmjs.org/mirador-dl-plugin/-/mirador-dl-plugin-0.13.0.tgz" - integrity sha512-I/6etIvpTtO1zgjxx2uEUFoyB9NxQ43JWg8CMkKmZqblW7AAeFqRn1/zUlQH7N8KFZft9Rah6D8qxtuNAo9jmA== - -mirador-share-plugin@^0.11.0: - version "0.11.0" - resolved "https://registry.npmjs.org/mirador-share-plugin/-/mirador-share-plugin-0.11.0.tgz" - integrity sha512-fHcdDXyrtfy5pn1zdQNX9BvE5Tjup66eQwyNippE5PMaP8ImUcrFaSL+mStdn+v6agsHcsdRqLhseZ0XWgEuAw== - -mirador@^3.3.0: - version "3.3.0" - resolved "https://registry.npmjs.org/mirador/-/mirador-3.3.0.tgz" - integrity sha512-BmGfRnWJ45B+vtiAwcFT7n9nKialfejE9UvuUK0NorO37ShArpsKr3yVSD4jQASwSR4DRRpPEG21jOk4WN7H3w== - dependencies: - "@material-ui/core" "^4.11.0" - "@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" - 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" - -mitt@^1.1.3: - version "1.2.0" - resolved "https://registry.npmjs.org/mitt/-/mitt-1.2.0.tgz" - integrity sha512-r6lj77KlwqLhIUku9UWYes7KJtsczvolZkzp8hbaDPPaE24OmWl5s539Mytlj22siEQKosZ26qCBgda2PKwoJw== - -mj-context-menu@^0.6.1: - version "0.6.1" - resolved "https://registry.npmjs.org/mj-context-menu/-/mj-context-menu-0.6.1.tgz" - integrity sha512-7NO5s6n10TIV96d4g2uDpG7ZDpIhMh0QNfGdJw/W47JswFcosz457wqz/b5sAKvl12sxINGFCn80NZHKwxQEXA== - -mkdirp@^0.5.5: - version "0.5.6" - resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz" - integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw== - dependencies: - minimist "^1.2.6" - -mkdirp@^1.0.3, mkdirp@^1.0.4: - version "1.0.4" - resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz" - integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== - -morgan@^1.10.0: - 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" - -mrmime@^1.0.0: - version "1.0.1" - resolved "https://registry.npmjs.org/mrmime/-/mrmime-1.0.1.tgz" - integrity sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw== - -ms@2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz" - integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== - -ms@2.1.2, ms@^2.0.0, ms@^2.1.1: - version "2.1.2" - resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz" - integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== - -ms@2.1.3: - version "2.1.3" - resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz" - integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== - -multicast-dns@^7.2.5: - version "7.2.5" - resolved "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz" - integrity sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg== - dependencies: - dns-packet "^5.2.2" - thunky "^1.0.2" - -mute-stream@0.0.8: - version "0.0.8" - resolved "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz" - integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== - -nanoid@^3.3.4, nanoid@^3.3.6: - version "3.3.6" - resolved "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz" - integrity sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA== - -natural-compare-lite@^1.4.0: - version "1.4.0" - resolved "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz" - integrity sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g== - -natural-compare@^1.4.0: - version "1.4.0" - resolved "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz" - integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== - -needle@^3.1.0: - version "3.2.0" - resolved "https://registry.npmjs.org/needle/-/needle-3.2.0.tgz" - integrity sha512-oUvzXnyLiVyVGoianLijF9O/RecZUf7TkBfimjGrLM4eQhXyeJwM6GeAWccwfQ9aa4gMCZKqhAOuLaMIcQxajQ== - dependencies: - debug "^3.2.6" - iconv-lite "^0.6.3" - sax "^1.2.4" - -negotiator@0.6.3, negotiator@^0.6.3: - version "0.6.3" - resolved "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz" - integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== - -neo-async@^2.6.2: - version "2.6.2" - resolved "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz" - integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== - -ng-mocks@^14.10.0: - version "14.10.0" - resolved "https://registry.yarnpkg.com/ng-mocks/-/ng-mocks-14.10.0.tgz#36064650c52f6e01be5c810228a0c9585109bb27" - integrity sha512-gUnrSH4ejlkSpd9wgEe7PNKEyADJCriemSIbyebS0SQjyotSZfp/8jBuMl7Euvk0sPanV2IGhOv6W9e6hNRe/A== - -ng2-file-upload@1.4.0: - version "1.4.0" - resolved "https://registry.npmjs.org/ng2-file-upload/-/ng2-file-upload-1.4.0.tgz" - integrity sha512-3J/KPU/tyh/ad6TFeUbrxX+SihUj0iOEt2Zsg4EX7mB3GFiQscXOfcUOxCkBtPWWWaqt3azrYbVGzsYa3/7NzQ== - dependencies: - tslib "^1.9.0" - -ng2-nouislider@^1.8.3: - version "1.8.3" - resolved "https://registry.npmjs.org/ng2-nouislider/-/ng2-nouislider-1.8.3.tgz" - integrity sha512-Vl8tHCcJ/ioJLAs2t6FBC35sZq1P/O5ZdqdFwYxOCOMVbILGWNg+2gWZIjFstvv9pqb/mVvVUYe6qGG/mA/RBQ== - -ngx-infinite-scroll@^15.0.0: - version "15.0.0" - resolved "https://registry.yarnpkg.com/ngx-infinite-scroll/-/ngx-infinite-scroll-15.0.0.tgz#61bbf89f121a796880a4ad67fe785b406532b220" - integrity sha512-FSP5UphRdl47vW8f/dRnPAU+napzruAKizBX2HS7MH292Ca+va6IU0J5+UQQmO5rXod2RZV7UhJ6+7g4xLfs1A== - dependencies: - tslib "^2.3.0" - -ngx-mask@^13.1.7: - version "13.1.15" - resolved "https://registry.npmjs.org/ngx-mask/-/ngx-mask-13.1.15.tgz" - integrity sha512-fplyzkFa6lFTzPo/AHaI3TBQxTMdcqQClR9BLLAWTvCyDZkV28fLqWkpIpy0VvPc9ADogFpJJj7R1356mszjag== - dependencies: - tslib "^2.3.0" - -ngx-pagination@6.0.3: - version "6.0.3" - resolved "https://registry.yarnpkg.com/ngx-pagination/-/ngx-pagination-6.0.3.tgz#083f77a5c9c3ebf5800fb57a280da71aacc10a18" - integrity sha512-lONjTQ7hFPh1SyhwDrRd5ZwM4NMGQ7bNR6vLrs6mrU0Z8Q1zCcWbf/pvyp4DOlGyd9uyZxRy2wUsSZLeIPjbAw== - dependencies: - tslib "^2.3.0" - -ngx-sortablejs@^11.1.0: - version "11.1.0" - resolved "https://registry.npmjs.org/ngx-sortablejs/-/ngx-sortablejs-11.1.0.tgz" - integrity sha512-eM4dHwWSmXDcvF5gUmyMMQ0qqcqBXWCSZ9IRpqx4UkBKfo4N7pk/QuYh5io2fXVHWVFDaxW1yhn2FNpqxV6Jqw== - dependencies: - tslib "^2.0.0" - -ngx-ui-switch@^14.0.3: - version "14.0.3" - resolved "https://registry.npmjs.org/ngx-ui-switch/-/ngx-ui-switch-14.0.3.tgz" - integrity sha512-SZ8ZnTCuNJgNWuY3/mOG3TdsRUNPCX3vGdCKKx1ZHVMTUynerJQlTWck2JrYlnrexNnd7wy4P10jDvdtDwoYeg== - dependencies: - tslib "^2.3.0" - -nice-napi@^1.0.2: - 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== - dependencies: - node-addon-api "^3.0.0" - node-gyp-build "^4.2.2" - -node-addon-api@^3.0.0: - version "3.2.1" - resolved "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz" - integrity sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A== - -node-fetch@^2.6.0, node-fetch@^2.6.1: - version "2.6.9" - resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.9.tgz" - integrity sha512-DJm/CJkZkRjKKj4Zi4BsKVZh3ValV5IR5s7LVZnW+6YMh0W1BfNA8XSs6DLMGYlId5F3KnA70uu2qepcR08Qqg== - dependencies: - whatwg-url "^5.0.0" - -node-forge@^1: - version "1.3.1" - resolved "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz" - integrity sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA== - -node-gyp-build@^4.2.2: - version "4.6.0" - resolved "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.6.0.tgz" - integrity sha512-NTZVKn9IylLwUzaKjkas1e4u2DLNcV4rdYagA4PWdPwW87Bi7z+BznyKSRwS/761tV/lzCGXplWsiaMjLqP2zQ== - -node-gyp@^9.0.0: - version "9.3.1" - resolved "https://registry.npmjs.org/node-gyp/-/node-gyp-9.3.1.tgz" - integrity sha512-4Q16ZCqq3g8awk6UplT7AuxQ35XN4R/yf/+wSAwcBUAjg7l58RTactWaP8fIDTi0FzI7YcVLujwExakZlfWkXg== - dependencies: - env-paths "^2.2.0" - glob "^7.1.4" - graceful-fs "^4.2.6" - make-fetch-happen "^10.0.3" - nopt "^6.0.0" - npmlog "^6.0.0" - rimraf "^3.0.2" - semver "^7.3.5" - tar "^6.1.2" - which "^2.0.2" - -node-releases@^2.0.8: - version "2.0.10" - resolved "https://registry.npmjs.org/node-releases/-/node-releases-2.0.10.tgz" - integrity sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w== - -nodemon@^2.0.22: - version "2.0.22" - resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-2.0.22.tgz#182c45c3a78da486f673d6c1702e00728daf5258" - integrity sha512-B8YqaKMmyuCO7BowF1Z1/mkPqLk6cs/l63Ojtd6otKjMx47Dq1utxfRxcavH1I7VSaL8n5BUaoutadnsX3AAVQ== - 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" - -nopt@^6.0.0: - version "6.0.0" - resolved "https://registry.npmjs.org/nopt/-/nopt-6.0.0.tgz" - integrity sha512-ZwLpbTgdhuZUnZzjd7nb1ZV+4DoiC6/sfiVKok72ym/4Tlf+DFdlHYmT2JPmcNNWV6Pi3SDf1kT+A4r9RTuT9g== - dependencies: - abbrev "^1.0.0" - -nopt@~1.0.10: - version "1.0.10" - resolved "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz" - integrity sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg== - dependencies: - abbrev "1" - -normalize-package-data@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-5.0.0.tgz" - integrity sha512-h9iPVIfrVZ9wVYQnxFgtw1ugSvGEMOlyPWWtm8BMJhnwyEL/FLbYbTY3V3PpjI/BUK67n9PEWDu6eHzu1fB15Q== - dependencies: - hosted-git-info "^6.0.0" - is-core-module "^2.8.1" - semver "^7.3.5" - validate-npm-package-license "^3.0.4" - -normalize-path@^3.0.0, normalize-path@~3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz" - integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== - -normalize-range@^0.1.2: - version "0.1.2" - resolved "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz" - integrity sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA== - -normalize-url@^4.5.0: - version "4.5.1" - resolved "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.1.tgz" - integrity sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA== - -nouislider@^14.6.3: - version "14.7.0" - resolved "https://registry.npmjs.org/nouislider/-/nouislider-14.7.0.tgz" - integrity sha512-4RtQ1+LHJKesDCNJrXkQcwXAWCrC2aggdLYMstS/G5fEWL+fXZbUA9pwVNHFghMGuFGRATlDLNInRaPeRKzpFQ== - -npm-bundled@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/npm-bundled/-/npm-bundled-3.0.0.tgz" - integrity sha512-Vq0eyEQy+elFpzsKjMss9kxqb9tG3YHg4dsyWuUENuzvSUWe1TCnW/vV9FkhvBk/brEDoDiVd+M1Btosa6ImdQ== - dependencies: - npm-normalize-package-bin "^3.0.0" - -npm-install-checks@^6.0.0: - version "6.1.0" - resolved "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-6.1.0.tgz" - integrity sha512-udSGENih/5xKh3Ex+L0PtZcOt0Pa+6ppDLnpG5D49/EhMja3LupaY9E/DtJTxyFBwE09ot7Fc+H4DywnZNWTVA== - dependencies: - semver "^7.1.1" - -npm-normalize-package-bin@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-3.0.0.tgz" - integrity sha512-g+DPQSkusnk7HYXr75NtzkIP4+N81i3RPsGFidF3DzHd9MT9wWngmqoeg/fnHFz5MNdtG4w03s+QnhewSLTT2Q== - -npm-package-arg@10.1.0, npm-package-arg@^10.0.0: - version "10.1.0" - resolved "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-10.1.0.tgz" - integrity sha512-uFyyCEmgBfZTtrKk/5xDfHp6+MdrqGotX/VoOyEEl3mBwiEE5FlBaePanazJSVMPT7vKepcjYBY2ztg9A3yPIA== - dependencies: - hosted-git-info "^6.0.0" - proc-log "^3.0.0" - semver "^7.3.5" - validate-npm-package-name "^5.0.0" - -npm-packlist@^7.0.0: - version "7.0.4" - resolved "https://registry.npmjs.org/npm-packlist/-/npm-packlist-7.0.4.tgz" - integrity sha512-d6RGEuRrNS5/N84iglPivjaJPxhDbZmlbTwTDX2IbcRHG5bZCdtysYMhwiPvcF4GisXHGn7xsxv+GQ7T/02M5Q== - dependencies: - ignore-walk "^6.0.0" - -npm-pick-manifest@8.0.1, npm-pick-manifest@^8.0.0: - version "8.0.1" - resolved "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-8.0.1.tgz" - integrity sha512-mRtvlBjTsJvfCCdmPtiu2bdlx8d/KXtF7yNXNWe7G0Z36qWA9Ny5zXsI2PfBZEv7SXgoxTmNaTzGSbbzDZChoA== - dependencies: - npm-install-checks "^6.0.0" - npm-normalize-package-bin "^3.0.0" - npm-package-arg "^10.0.0" - semver "^7.3.5" - -npm-registry-fetch@^14.0.0: - version "14.0.3" - resolved "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-14.0.3.tgz" - integrity sha512-YaeRbVNpnWvsGOjX2wk5s85XJ7l1qQBGAp724h8e2CZFFhMSuw9enom7K1mWVUtvXO1uUSFIAPofQK0pPN0ZcA== - dependencies: - make-fetch-happen "^11.0.0" - minipass "^4.0.0" - minipass-fetch "^3.0.0" - minipass-json-stream "^1.0.1" - minizlib "^2.1.2" - npm-package-arg "^10.0.0" - proc-log "^3.0.0" - -npm-run-path@^4.0.0, npm-run-path@^4.0.1: - version "4.0.1" - resolved "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz" - integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== - dependencies: - path-key "^3.0.0" - -npmlog@^6.0.0: - version "6.0.2" - resolved "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz" - integrity sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg== - dependencies: - are-we-there-yet "^3.0.0" - console-control-strings "^1.1.0" - gauge "^4.0.3" - set-blocking "^2.0.0" - -nth-check@^2.0.1: - 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" - -nwsapi@^2.2.0, nwsapi@^2.2.2: - version "2.2.3" - resolved "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.3.tgz" - integrity sha512-jscxIO4/VKScHlbmFBdV1Z6LXnLO+ZR4VMtypudUdfwtKxUN3TQcNFIHLwKtrUbDyHN4/GycY9+oRGZ2XMXYPw== - -oauth-sign@~0.9.0: - version "0.9.0" - resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" - integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== - -object-assign@^4, object-assign@^4.0.1, object-assign@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" - integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== - -object-inspect@^1.12.3, object-inspect@^1.9.0: - version "1.12.3" - resolved "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz" - integrity sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g== - -object-is@^1.1.5: - version "1.1.5" - resolved "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz" - integrity sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.3" - -object-keys@^1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz" - integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== - -object-path@^0.11.5: - version "0.11.8" - resolved "https://registry.npmjs.org/object-path/-/object-path-0.11.8.tgz" - integrity sha512-YJjNZrlXJFM42wTBn6zgOJVar9KFJvzx6sTWDte8sWZF//cnjl0BxHNpfZx+ZffXX63A9q0b1zsFiBX4g4X5KA== - -object.assign@^4.1.4: - version "4.1.4" - resolved "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz" - integrity sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.4" - has-symbols "^1.0.3" - object-keys "^1.1.1" - -object.values@^1.1.6: - version "1.1.6" - resolved "https://registry.npmjs.org/object.values/-/object.values-1.1.6.tgz" - integrity sha512-FVVTkD1vENCsAcwNs9k6jea2uHC/X0+JcjG8YA60FN5CMaJmG95wT9jek/xX9nornqGRrBkKtzuAu2wuHpKqvw== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.4" - es-abstract "^1.20.4" - -obuf@^1.0.0, obuf@^1.1.2: - version "1.1.2" - resolved "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz" - integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg== - -on-finished@2.4.1: - 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" - -on-finished@~2.3.0: - 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" - -on-headers@~1.0.2: - 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== - -once@^1.3.0, once@^1.3.1, once@^1.4.0: - version "1.4.0" - resolved "https://registry.npmjs.org/once/-/once-1.4.0.tgz" - integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== - dependencies: - wrappy "1" - -onetime@^5.1.0, onetime@^5.1.2: - version "5.1.2" - resolved "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz" - integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== - dependencies: - mimic-fn "^2.1.0" - -open@8.4.1: - version "8.4.1" - resolved "https://registry.npmjs.org/open/-/open-8.4.1.tgz" - integrity sha512-/4b7qZNhv6Uhd7jjnREh1NjnPxlTq+XNWPG88Ydkj5AILcA5m3ajvcg57pB24EQjKv0dK62XnDqk9c/hkIG5Kg== - dependencies: - define-lazy-prop "^2.0.0" - is-docker "^2.1.1" - is-wsl "^2.2.0" - -open@^8.0.9: - version "8.4.0" - resolved "https://registry.npmjs.org/open/-/open-8.4.0.tgz" - integrity sha512-XgFPPM+B28FtCCgSb9I+s9szOC1vZRSwgWsRUA5ylIxRTgKozqjOCrVOqGsYABPYK5qnfqClxZTFBa8PKt2v6Q== - dependencies: - define-lazy-prop "^2.0.0" - is-docker "^2.1.1" - is-wsl "^2.2.0" - -opener@^1.5.2: - version "1.5.2" - resolved "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz" - integrity sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A== - -openseadragon@^2.4.2: - version "2.4.2" - resolved "https://registry.npmjs.org/openseadragon/-/openseadragon-2.4.2.tgz" - integrity sha512-398KbZwRtOYA6OmeWRY4Q0737NTacQ9Q6whmr9Lp1MNQO3p0eBz5LIASRne+4gwequcSM1vcHcjfy3dIndQziw== - -openurl@1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/openurl/-/openurl-1.1.1.tgz" - integrity sha512-d/gTkTb1i1GKz5k3XE3XFV/PxQ1k45zDqGP2OA7YhgsaLoqm6qRvARAZOFer1fcXritWlGBRCu/UgeS4HAnXAA== - -opn@5.3.0: - 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== - dependencies: - is-wsl "^1.1.0" - -optionator@^0.8.1: - version "0.8.3" - resolved "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz" - integrity sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA== - dependencies: - deep-is "~0.1.3" - fast-levenshtein "~2.0.6" - levn "~0.3.0" - prelude-ls "~1.1.2" - type-check "~0.3.2" - word-wrap "~1.2.3" - -optionator@^0.9.1: - version "0.9.1" - resolved "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz" - integrity sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw== - 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.3" - -ora@5.4.1, ora@^5.1.0, ora@^5.4.1: - version "5.4.1" - resolved "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz" - integrity sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ== - 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" - -os-tmpdir@^1.0.2, os-tmpdir@~1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" - integrity sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g== - -ospath@^1.2.2: - version "1.2.2" - resolved "https://registry.npmjs.org/ospath/-/ospath-1.2.2.tgz" - integrity sha512-o6E5qJV5zkAbIDNhGSIlyOhScKXgQrSRMilfph0clDfM0nEnBOlKlH4sWDmG95BW/CvwNz0vmm7dJVtU2KlMiA== - -p-limit@^2.2.0: - version "2.3.0" - resolved "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz" - integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== - dependencies: - p-try "^2.0.0" - -p-limit@^3.0.2: - version "3.1.0" - resolved "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz" - integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== - dependencies: - yocto-queue "^0.1.0" - -p-locate@^4.1.0: - version "4.1.0" - resolved "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz" - integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== - dependencies: - p-limit "^2.2.0" - -p-locate@^5.0.0: - 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== - dependencies: - p-limit "^3.0.2" - -p-map@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz" - integrity sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ== - dependencies: - aggregate-error "^3.0.0" - -p-retry@^4.5.0: - version "4.6.2" - resolved "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz" - integrity sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ== - dependencies: - "@types/retry" "0.12.0" - retry "^0.13.1" - -p-try@^2.0.0: - version "2.2.0" - resolved "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz" - integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== - -pacote@15.1.0: - version "15.1.0" - resolved "https://registry.npmjs.org/pacote/-/pacote-15.1.0.tgz" - integrity sha512-FFcjtIl+BQNfeliSm7MZz5cpdohvUV1yjGnqgVM4UnVF7JslRY0ImXAygdaCDV0jjUADEWu4y5xsDV8brtrTLg== - dependencies: - "@npmcli/git" "^4.0.0" - "@npmcli/installed-package-contents" "^2.0.1" - "@npmcli/promise-spawn" "^6.0.1" - "@npmcli/run-script" "^6.0.0" - cacache "^17.0.0" - fs-minipass "^3.0.0" - minipass "^4.0.0" - npm-package-arg "^10.0.0" - npm-packlist "^7.0.0" - npm-pick-manifest "^8.0.0" - npm-registry-fetch "^14.0.0" - proc-log "^3.0.0" - promise-retry "^2.0.1" - read-package-json "^6.0.0" - read-package-json-fast "^3.0.0" - sigstore "^1.0.0" - ssri "^10.0.0" - tar "^6.1.11" - -pako@^1.0.3: - version "1.0.11" - resolved "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz" - integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw== - -parent-module@^1.0.0: - 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== - dependencies: - callsites "^3.0.0" - -parse-json@^5.0.0: - version "5.2.0" - resolved "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz" - integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== - 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" - -parse-node-version@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz" - integrity sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA== - -parse-srcset@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz" - integrity sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q== - -parse5-html-rewriting-stream@7.0.0: - 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== - dependencies: - entities "^4.3.0" - parse5 "^7.0.0" - parse5-sax-parser "^7.0.0" - -parse5-htmlparser2-tree-adapter@^6.0.1: - version "6.0.1" - resolved "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz" - integrity sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA== - dependencies: - parse5 "^6.0.1" - -parse5-sax-parser@^7.0.0: - version "7.0.0" - resolved "https://registry.npmjs.org/parse5-sax-parser/-/parse5-sax-parser-7.0.0.tgz" - integrity sha512-5A+v2SNsq8T6/mG3ahcz8ZtQ0OUFTatxPbeidoMB7tkJSGDY3tdfl4MHovtLQHkEn5CGxijNWRQHhRQ6IRpXKg== - dependencies: - parse5 "^7.0.0" - -parse5@6.0.1, parse5@^6.0.1: - version "6.0.1" - resolved "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz" - integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw== - -parse5@^7.0.0, parse5@^7.1.1, parse5@^7.1.2: - version "7.1.2" - resolved "https://registry.yarnpkg.com/parse5/-/parse5-7.1.2.tgz#0736bebbfd77793823240a23b7fc5e010b7f8e32" - integrity sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw== - dependencies: - entities "^4.4.0" - -parseurl@~1.3.2, parseurl@~1.3.3: - version "1.3.3" - resolved "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz" - integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== - -path-exists@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz" - integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== - -path-is-absolute@^1.0.0: - version "1.0.1" - resolved "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz" - integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== - -path-is-inside@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53" - integrity sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w== - -path-key@^3.0.0, path-key@^3.1.0: - version "3.1.1" - resolved "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz" - integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== - -path-parse@^1.0.7: - version "1.0.7" - resolved "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz" - integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== - -path-scurry@^1.6.1: - version "1.6.4" - resolved "https://registry.npmjs.org/path-scurry/-/path-scurry-1.6.4.tgz" - integrity sha512-Qp/9IHkdNiXJ3/Kon++At2nVpnhRiPq/aSvQN+H3U1WZbvNRK0RIQK/o4HMqPoXjpuGJUEWpHSs6Mnjxqh3TQg== - dependencies: - lru-cache "^9.0.0" - minipass "^5.0.0" - -path-to-regexp@0.1.7: - version "0.1.7" - resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz" - integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ== - -path-type@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz" - integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== - -pem@1.14.7: - version "1.14.7" - resolved "https://registry.yarnpkg.com/pem/-/pem-1.14.7.tgz#dae9831ee5fa7c88547327fb7738898cd76412c6" - integrity sha512-tN5+bp2/Yh/2yuv/JFXXHXrd5RVfsEBwlV7BshuYPX0OJWbR/MeAr89CKWcIp/W0cEnaTPT44haXyaEz1T6XeA== - dependencies: - es6-promisify "^7.0.0" - md5 "^2.3.0" - os-tmpdir "^1.0.2" - which "^2.0.2" - -pend@~1.2.0: - version "1.2.0" - resolved "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz" - integrity sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg== - -performance-now@^2.1.0: - version "2.1.0" - resolved "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz" - integrity sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow== - -picocolors@^0.2.1: - version "0.2.1" - resolved "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz" - integrity sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA== - -picocolors@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz" - integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== - -picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1: - version "2.3.1" - resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz" - integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== - -pify@^2.0.0, pify@^2.2.0, pify@^2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" - integrity sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog== - -pify@^4.0.1: - version "4.0.1" - resolved "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz" - integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g== - -pinkie-promise@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" - integrity sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw== - dependencies: - pinkie "^2.0.0" - -pinkie@^2.0.0: - version "2.0.4" - resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" - integrity sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg== - -piscina@3.2.0, piscina@~3.2.0: - version "3.2.0" - resolved "https://registry.npmjs.org/piscina/-/piscina-3.2.0.tgz" - integrity sha512-yn/jMdHRw+q2ZJhFhyqsmANcbF6V2QwmD84c6xRau+QpQOmtrBCoRGdvTfeuFDYXB5W2m6MfLkjkvQa9lUSmIA== - dependencies: - eventemitter-asyncresource "^1.0.0" - hdr-histogram-js "^2.0.1" - hdr-histogram-percentiles-obj "^3.0.0" - optionalDependencies: - nice-napi "^1.0.2" - -pkg-dir@^4.1.0, pkg-dir@^4.2.0: - 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== - dependencies: - find-up "^4.0.0" - -popper.js@1.16.1-lts: - 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== - -portscanner@2.2.0: - version "2.2.0" - resolved "https://registry.npmjs.org/portscanner/-/portscanner-2.2.0.tgz" - integrity sha512-IFroCz/59Lqa2uBvzK3bKDbDDIEaAY8XJ1jFxcLWTqosrsc32//P4VuSB2vZXoHiHqOmx8B5L5hnKOxL/7FlPw== - dependencies: - async "^2.6.0" - is-number-like "^1.0.3" - -postcss-apply@0.12.0: - version "0.12.0" - resolved "https://registry.npmjs.org/postcss-apply/-/postcss-apply-0.12.0.tgz" - integrity sha512-u8qZLyA9P86cD08IhqjSVV8tf1eGiKQ4fPvjcG3Ic/eOU65EAkDQClp8We7d15TG+RIWRVPSy9v7cJ2D9OReqw== - dependencies: - balanced-match "^1.0.0" - postcss "^7.0.14" - -postcss-attribute-case-insensitive@^5.0.2: - 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== - dependencies: - postcss-selector-parser "^6.0.10" - -postcss-clamp@^4.1.0: - version "4.1.0" - resolved "https://registry.npmjs.org/postcss-clamp/-/postcss-clamp-4.1.0.tgz" - integrity sha512-ry4b1Llo/9zz+PKC+030KUnPITTJAHeOwjfAyyB60eT0AorGLdzp52s31OsPRHRf8NchkgFoG2y6fCfn1IV1Ow== - dependencies: - postcss-value-parser "^4.2.0" - -postcss-color-functional-notation@^4.2.4: - 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== - dependencies: - postcss-value-parser "^4.2.0" - -postcss-color-hex-alpha@^8.0.4: - version "8.0.4" - resolved "https://registry.npmjs.org/postcss-color-hex-alpha/-/postcss-color-hex-alpha-8.0.4.tgz" - integrity sha512-nLo2DCRC9eE4w2JmuKgVA3fGL3d01kGq752pVALF68qpGLmx2Qrk91QTKkdUqqp45T1K1XV8IhQpcu1hoAQflQ== - dependencies: - postcss-value-parser "^4.2.0" - -postcss-color-rebeccapurple@^7.1.1: - 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== - dependencies: - postcss-value-parser "^4.2.0" - -postcss-custom-media@^8.0.2: - version "8.0.2" - resolved "https://registry.npmjs.org/postcss-custom-media/-/postcss-custom-media-8.0.2.tgz" - integrity sha512-7yi25vDAoHAkbhAzX9dHx2yc6ntS4jQvejrNcC+csQJAXjj15e7VcWfMgLqBNAbOvqi5uIa9huOVwdHbf+sKqg== - dependencies: - postcss-value-parser "^4.2.0" - -postcss-custom-properties@^12.1.10: - version "12.1.11" - resolved "https://registry.npmjs.org/postcss-custom-properties/-/postcss-custom-properties-12.1.11.tgz" - integrity sha512-0IDJYhgU8xDv1KY6+VgUwuQkVtmYzRwu+dMjnmdMafXYv86SWqfxkc7qdDvWS38vsjaEtv8e0vGOUQrAiMBLpQ== - dependencies: - postcss-value-parser "^4.2.0" - -postcss-custom-selectors@^6.0.3: - version "6.0.3" - resolved "https://registry.npmjs.org/postcss-custom-selectors/-/postcss-custom-selectors-6.0.3.tgz" - integrity sha512-fgVkmyiWDwmD3JbpCmB45SvvlCD6z9CG6Ie6Iere22W5aHea6oWa7EM2bpnv2Fj3I94L3VbtvX9KqwSi5aFzSg== - dependencies: - postcss-selector-parser "^6.0.4" - -postcss-dir-pseudo-class@^6.0.5: - 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== - dependencies: - postcss-selector-parser "^6.0.10" - -postcss-double-position-gradients@^3.1.2: - 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== - dependencies: - "@csstools/postcss-progressive-custom-properties" "^1.1.0" - postcss-value-parser "^4.2.0" - -postcss-env-function@^4.0.6: - 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== - dependencies: - postcss-value-parser "^4.2.0" - -postcss-focus-visible@^6.0.4: - 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== - dependencies: - postcss-selector-parser "^6.0.9" - -postcss-focus-within@^5.0.4: - 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== - dependencies: - postcss-selector-parser "^6.0.9" - -postcss-font-variant@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/postcss-font-variant/-/postcss-font-variant-5.0.0.tgz" - integrity sha512-1fmkBaCALD72CK2a9i468mA/+tr9/1cBxRRMXOUaZqO43oWPR5imcyPjXwuv7PXbCid4ndlP5zWhidQVVa3hmA== - -postcss-gap-properties@^3.0.5: - version "3.0.5" - resolved "https://registry.npmjs.org/postcss-gap-properties/-/postcss-gap-properties-3.0.5.tgz" - integrity sha512-IuE6gKSdoUNcvkGIqdtjtcMtZIFyXZhmFd5RUlg97iVEvp1BZKV5ngsAjCjrVy+14uhGBQl9tzmi1Qwq4kqVOg== - -postcss-image-set-function@^4.0.7: - 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== - dependencies: - postcss-value-parser "^4.2.0" - -postcss-import@^14.0.0: - version "14.1.0" - resolved "https://registry.npmjs.org/postcss-import/-/postcss-import-14.1.0.tgz" - integrity sha512-flwI+Vgm4SElObFVPpTIT7SU7R3qk2L7PyduMcokiaVKuWv9d/U+Gm/QAd8NDLuykTWTkcrjOeD2Pp1rMeBTGw== - dependencies: - postcss-value-parser "^4.0.0" - read-cache "^1.0.0" - resolve "^1.1.7" - -postcss-initial@^4.0.1: - 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== - -postcss-lab-function@^4.2.1: - version "4.2.1" - resolved "https://registry.npmjs.org/postcss-lab-function/-/postcss-lab-function-4.2.1.tgz" - integrity sha512-xuXll4isR03CrQsmxyz92LJB2xX9n+pZJ5jE9JgcnmsCammLyKdlzrBin+25dy6wIjfhJpKBAN80gsTlCgRk2w== - dependencies: - "@csstools/postcss-progressive-custom-properties" "^1.1.0" - postcss-value-parser "^4.2.0" - -postcss-loader@7.0.2: - version "7.0.2" - resolved "https://registry.npmjs.org/postcss-loader/-/postcss-loader-7.0.2.tgz" - integrity sha512-fUJzV/QH7NXUAqV8dWJ9Lg4aTkDCezpTS5HgJ2DvqznexTbSTxgi/dTECvTZ15BwKTtk8G/bqI/QTu2HPd3ZCg== - dependencies: - cosmiconfig "^7.0.0" - klona "^2.0.5" - semver "^7.3.8" - -postcss-loader@^4.0.3: - version "4.3.0" - resolved "https://registry.npmjs.org/postcss-loader/-/postcss-loader-4.3.0.tgz" - integrity sha512-M/dSoIiNDOo8Rk0mUqoj4kpGq91gcxCfb9PoyZVdZ76/AuhxylHDYZblNE8o+EQ9AMSASeMFEKxZf5aU6wlx1Q== - dependencies: - cosmiconfig "^7.0.0" - klona "^2.0.4" - loader-utils "^2.0.0" - schema-utils "^3.0.0" - semver "^7.3.4" - -postcss-logical@^5.0.4: - version "5.0.4" - resolved "https://registry.npmjs.org/postcss-logical/-/postcss-logical-5.0.4.tgz" - integrity sha512-RHXxplCeLh9VjinvMrZONq7im4wjWGlRJAqmAVLXyZaXwfDWP73/oq4NdIp+OZwhQUMj0zjqDfM5Fj7qby+B4g== - -postcss-media-minmax@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/postcss-media-minmax/-/postcss-media-minmax-5.0.0.tgz" - integrity sha512-yDUvFf9QdFZTuCUg0g0uNSHVlJ5X1lSzDZjPSFaiCWvjgsvu8vEVxtahPrLMinIDEEGnx6cBe6iqdx5YWz08wQ== - -postcss-modules-extract-imports@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz" - integrity sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw== - -postcss-modules-local-by-default@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.0.tgz" - integrity sha512-sT7ihtmGSF9yhm6ggikHdV0hlziDTX7oFoXtuVWeDd3hHObNkcHRo9V3yg7vCAY7cONyxJC/XXCmmiHHcvX7bQ== - dependencies: - icss-utils "^5.0.0" - postcss-selector-parser "^6.0.2" - postcss-value-parser "^4.1.0" - -postcss-modules-scope@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz" - integrity sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg== - dependencies: - postcss-selector-parser "^6.0.4" - -postcss-modules-values@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz" - integrity sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ== - dependencies: - icss-utils "^5.0.0" - -postcss-nesting@^10.2.0: - version "10.2.0" - resolved "https://registry.npmjs.org/postcss-nesting/-/postcss-nesting-10.2.0.tgz" - integrity sha512-EwMkYchxiDiKUhlJGzWsD9b2zvq/r2SSubcRrgP+jujMXFzqvANLt16lJANC+5uZ6hjI7lpRmI6O8JIl+8l1KA== - dependencies: - "@csstools/selector-specificity" "^2.0.0" - postcss-selector-parser "^6.0.10" - -postcss-opacity-percentage@^1.1.2: - 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== - -postcss-overflow-shorthand@^3.0.4: - version "3.0.4" - resolved "https://registry.npmjs.org/postcss-overflow-shorthand/-/postcss-overflow-shorthand-3.0.4.tgz" - integrity sha512-otYl/ylHK8Y9bcBnPLo3foYFLL6a6Ak+3EQBPOTR7luMYCOsiVTUk1iLvNf6tVPNGXcoL9Hoz37kpfriRIFb4A== - dependencies: - postcss-value-parser "^4.2.0" - -postcss-page-break@^3.0.4: - 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== - -postcss-place@^7.0.5: - version "7.0.5" - resolved "https://registry.npmjs.org/postcss-place/-/postcss-place-7.0.5.tgz" - integrity sha512-wR8igaZROA6Z4pv0d+bvVrvGY4GVHihBCBQieXFY3kuSuMyOmEnnfFzHl/tQuqHZkfkIVBEbDvYcFfHmpSet9g== - dependencies: - postcss-value-parser "^4.2.0" - -postcss-preset-env@^7.4.2: - version "7.8.3" - resolved "https://registry.npmjs.org/postcss-preset-env/-/postcss-preset-env-7.8.3.tgz" - integrity sha512-T1LgRm5uEVFSEF83vHZJV2z19lHg4yJuZ6gXZZkqVsqv63nlr6zabMH3l4Pc01FQCyfWVrh2GaUeCVy9Po+Aag== - 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" - -postcss-pseudo-class-any-link@^7.1.6: - 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== - dependencies: - postcss-selector-parser "^6.0.10" - -postcss-replace-overflow-wrap@^4.0.0: - 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== - -postcss-responsive-type@1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/postcss-responsive-type/-/postcss-responsive-type-1.0.0.tgz" - integrity sha512-O4kAKbc4RLnSkzcguJ6ojW67uOfeILaj+8xjsO0quLU94d8BKCqYwwFEUVRNbj0YcXA6d3uF/byhbaEATMRVig== - dependencies: - postcss "^6.0.6" - -postcss-selector-not@^6.0.1: - 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== - dependencies: - postcss-selector-parser "^6.0.10" - -postcss-selector-parser@^6.0.10, postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4, postcss-selector-parser@^6.0.9: - version "6.0.11" - resolved "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.11.tgz" - integrity sha512-zbARubNdogI9j7WY4nQJBiNqQf3sLS3wCP4WfOidu+p28LofJqDH1tcXypGrcmMHhDk2t9wGhCsYe/+szLTy1g== - dependencies: - cssesc "^3.0.0" - util-deprecate "^1.0.2" - -postcss-value-parser@^4.0.0, postcss-value-parser@^4.1.0, postcss-value-parser@^4.2.0: - version "4.2.0" - resolved "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz" - integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== - -postcss@8.4.21, postcss@^8.2.14, postcss@^8.3.11, postcss@^8.3.7, postcss@^8.4.19: - version "8.4.21" - resolved "https://registry.npmjs.org/postcss/-/postcss-8.4.21.tgz" - integrity sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg== - dependencies: - nanoid "^3.3.4" - picocolors "^1.0.0" - source-map-js "^1.0.2" - -postcss@^6.0.6: - version "6.0.23" - resolved "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz" - integrity sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag== - dependencies: - chalk "^2.4.1" - source-map "^0.6.1" - supports-color "^5.4.0" - -postcss@^7.0.14: - version "7.0.39" - resolved "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz" - integrity sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA== - dependencies: - picocolors "^0.2.1" - source-map "^0.6.1" - -postcss@^8.4: - version "8.4.23" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.23.tgz#df0aee9ac7c5e53e1075c24a3613496f9e6552ab" - integrity sha512-bQ3qMcpF6A/YjR55xtoTr0jGOlnPOKAIMdOWiv0EIT6HVPEaJiJB4NLljSbiHoC2RX7DN5Uvjtpbg1NPdwv1oA== - dependencies: - nanoid "^3.3.6" - picocolors "^1.0.0" - source-map-js "^1.0.2" - -prelude-ls@^1.2.1: - version "1.2.1" - resolved "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz" - integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== - -prelude-ls@~1.1.2: - version "1.1.2" - resolved "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz" - integrity sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w== - -pretty-bytes@^5.3.0, pretty-bytes@^5.6.0: - version "5.6.0" - resolved "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz" - integrity sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg== - -proc-log@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/proc-log/-/proc-log-3.0.0.tgz" - integrity sha512-++Vn7NS4Xf9NacaU9Xq3URUuqZETPsf8L4j5/ckhaRYsfPeRyzGw+iDjFhV/Jr3uNmTvvddEJFWh5R1gRgUH8A== - -process-nextick-args@~2.0.0: - 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== - -promise-inflight@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz" - integrity sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g== - -promise-retry@^2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz" - integrity sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g== - dependencies: - err-code "^2.0.2" - retry "^0.12.0" - -prompts@~2.4.2: - version "2.4.2" - resolved "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz" - integrity sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q== - dependencies: - kleur "^3.0.3" - sisteransi "^1.0.5" - -prop-types@^15.6.0, prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1: - 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" - -proxy-addr@~2.0.7: - 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" - -proxy-from-env@1.0.0: - 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== - -prr@~1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz" - integrity sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw== - -psl@^1.1.28, psl@^1.1.33: - version "1.9.0" - resolved "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz" - integrity sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag== - -pstree.remy@^1.1.8: - version "1.1.8" - resolved "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz" - integrity sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w== - -pump@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz" - integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== - dependencies: - end-of-stream "^1.1.0" - once "^1.3.1" - -punycode@^2.1.0, punycode@^2.1.1: - version "2.3.0" - resolved "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz" - integrity sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA== - -q@^1.4.1: - version "1.5.1" - resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" - integrity sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw== - -qjobs@^1.2.0: - 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== - -qs@6.11.0, qs@^6.11.0: - version "6.11.0" - resolved "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz" - integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q== - dependencies: - side-channel "^1.0.4" - -qs@~6.10.3: - version "6.10.4" - resolved "https://registry.npmjs.org/qs/-/qs-6.10.4.tgz" - integrity sha512-OQiU+C+Ds5qiH91qh/mg0w+8nwQuLjM4F4M/PbmhDOoYehPh+Fb0bDjtR1sOvy7YKxvj28Y/M0PhP5uVX0kB+g== - dependencies: - side-channel "^1.0.4" - -qs@~6.5.2: - version "6.5.3" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.3.tgz#3aeeffc91967ef6e35c0e488ef46fb296ab76aad" - integrity sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA== - -querystringify@^2.1.1: - version "2.2.0" - resolved "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz" - integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ== - -queue-microtask@^1.2.2: - version "1.2.3" - resolved "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz" - integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== - -raf-schd@^4.0.2: - version "4.0.3" - resolved "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.3.tgz" - integrity sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ== - -randombytes@^2.1.0: - version "2.1.0" - resolved "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz" - integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== - dependencies: - safe-buffer "^5.1.0" - -range-parser@^1.2.1, range-parser@~1.2.0, range-parser@~1.2.1: - version "1.2.1" - resolved "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz" - integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== - -raw-body@2.5.1, raw-body@^2.3.2: - version "2.5.1" - resolved "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz" - integrity sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig== - dependencies: - bytes "3.1.2" - http-errors "2.0.0" - iconv-lite "0.4.24" - unpipe "1.0.0" - -re-reselect@^4.0.0: - version "4.0.1" - resolved "https://registry.npmjs.org/re-reselect/-/re-reselect-4.0.1.tgz" - integrity sha512-xVTNGQy/dAxOolunBLmVMGZ49VUUR1s8jZUiJQb+g1sI63GAv9+a5Jas9yHvdxeUgiZkU9r3gDExDorxHzOgRA== - -re-resizable@6.9.6: - version "6.9.6" - resolved "https://registry.npmjs.org/re-resizable/-/re-resizable-6.9.6.tgz" - integrity sha512-0xYKS5+Z0zk+vICQlcZW+g54CcJTTmHluA7JUUgvERDxnKAnytylcyPsA+BSFi759s5hPlHmBRegFrwXs2FuBQ== - dependencies: - fast-memoize "^2.5.1" - -react-aria-live@^2.0.5: - 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" - -react-beautiful-dnd@^13.0.0: - 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" - -react-copy-to-clipboard@^5.0.1, react-copy-to-clipboard@^5.1.0: - 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" - -react-dnd-html5-backend@^10.0.2: - 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" - -react-dnd-multi-backend@^5.0.0: - 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" - -react-dnd-preview@^5.0.1: - 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" - -react-dnd-touch-backend@^10.0.2: - 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" - -react-dnd@^10.0.2: - 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" - -react-dom@^16.14.0: - version "16.14.0" - resolved "https://registry.npmjs.org/react-dom/-/react-dom-16.14.0.tgz" - integrity sha512-1gCeQXDLoIqMgqD3IO2Ah9bnf0w9kzhwN5q4FGnHZ67hBm9yePzB5JJAIQCc8x3pFnNlwFq4RidZggNAAkzWWw== - dependencies: - loose-envify "^1.1.0" - object-assign "^4.1.1" - prop-types "^15.6.2" - scheduler "^0.19.1" - -react-draggable@4.4.5: - version "4.4.5" - resolved "https://registry.npmjs.org/react-draggable/-/react-draggable-4.4.5.tgz" - integrity sha512-OMHzJdyJbYTZo4uQE393fHcqqPYsEtkjfMgvCHr6rejT+Ezn4OZbNyGH50vv+SunC1RMvwOTSWkEODQLzw1M9g== - dependencies: - clsx "^1.1.1" - prop-types "^15.8.1" - -react-full-screen@^0.2.4: - 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" - -react-i18next@^11.7.0: - 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" - -react-image@^4.0.1: - version "4.1.0" - resolved "https://registry.npmjs.org/react-image/-/react-image-4.1.0.tgz" - integrity sha512-qwPNlelQe9Zy14K2pGWSwoL+vHsAwmJKS6gkotekDgRpcnRuzXNap00GfibD3eEPYu3WCPlyIUUNzcyHOrLHjw== - -react-is@^16.13.1, react-is@^16.7.0: - version "16.13.1" - resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz" - integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== - -"react-is@^16.8.0 || ^17.0.0", react-is@^17.0.2: - version "17.0.2" - resolved "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz" - integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== - -react-mosaic-component@^4.0.1: - 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" - -react-redux@^7.1.0, react-redux@^7.2.0: - 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" - -react-resize-observer@^1.1.1: - 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== - -react-rnd@^10.1: - version "10.4.1" - resolved "https://registry.npmjs.org/react-rnd/-/react-rnd-10.4.1.tgz" - integrity sha512-0m887AjQZr6p2ADLNnipquqsDq4XJu/uqVqI3zuoGD19tRm6uB83HmZWydtkilNp5EWsOHbLGF4IjWMdd5du8Q== - dependencies: - re-resizable "6.9.6" - react-draggable "4.4.5" - tslib "2.3.1" - -react-sizeme@^2.6.7: - 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" - -react-transition-group@^4.4.0: - 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" - -react-virtualized-auto-sizer@^1.0.2: - version "1.0.13" - resolved "https://registry.npmjs.org/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.13.tgz" - integrity sha512-iazB2jnZz93jgjo66/rMlrshQ0M67Y7CRSGJRu/BkWH5H63OcC0YL6dBxJZ+etHDjZT6qnUm4PKRjOJx9yF51w== - -react-window@^1.8.5: - version "1.8.8" - resolved "https://registry.npmjs.org/react-window/-/react-window-1.8.8.tgz" - integrity sha512-D4IiBeRtGXziZ1n0XklnFGu7h9gU684zepqyKzgPNzrsrk7xOCxni+TCckjg2Nr/DiaEEGVVmnhYSlT2rB47dQ== - dependencies: - "@babel/runtime" "^7.0.0" - memoize-one ">=3.1.1 <6" - -react@^16.14.0: - version "16.14.0" - resolved "https://registry.npmjs.org/react/-/react-16.14.0.tgz" - integrity sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g== - dependencies: - loose-envify "^1.1.0" - object-assign "^4.1.1" - prop-types "^15.6.2" - -read-cache@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz" - integrity sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA== - dependencies: - pify "^2.3.0" - -read-package-json-fast@^3.0.0: - 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== - dependencies: - json-parse-even-better-errors "^3.0.0" - npm-normalize-package-bin "^3.0.0" - -read-package-json@^6.0.0: - version "6.0.1" - resolved "https://registry.npmjs.org/read-package-json/-/read-package-json-6.0.1.tgz" - integrity sha512-AaHqXxfAVa+fNL07x8iAghfKOds/XXsu7zoouIVsbm7PEbQ3nMWXlvjcbrNLjElnUHWQtAo4QEa0RXuvD4XlpA== - dependencies: - glob "^9.3.0" - json-parse-even-better-errors "^3.0.0" - normalize-package-data "^5.0.0" - npm-normalize-package-bin "^3.0.0" - -readable-stream@^2.0.1: - version "2.3.8" - resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz" - integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== - 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" - -readable-stream@^3.0.6, readable-stream@^3.4.0, readable-stream@^3.6.0: - version "3.6.2" - resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz" - integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== - dependencies: - inherits "^2.0.3" - string_decoder "^1.1.1" - util-deprecate "^1.0.1" - -readdirp@~3.6.0: - version "3.6.0" - resolved "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz" - integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== - dependencies: - picomatch "^2.2.1" - -rechoir@^0.7.0: - version "0.7.1" - resolved "https://registry.npmjs.org/rechoir/-/rechoir-0.7.1.tgz" - integrity sha512-/njmZ8s1wVeR6pjTZ+0nCnv8SpZNRMT2D1RLOJQESlYFDBvwpTA4KWJpZ+sBJ4+vhjILRcK7JIFdGCdxEAAitg== - dependencies: - resolve "^1.9.0" - -redux-devtools-extension@^2.13.2: - version "2.13.9" - resolved "https://registry.npmjs.org/redux-devtools-extension/-/redux-devtools-extension-2.13.9.tgz" - integrity sha512-cNJ8Q/EtjhQaZ71c8I9+BPySIBVEKssbPpskBfsXqb8HJ002A3KRVHfeRzwRo6mGPqsm7XuHTqNSNeS1Khig0A== - -redux-saga@^1.1.3: - version "1.2.3" - resolved "https://registry.npmjs.org/redux-saga/-/redux-saga-1.2.3.tgz" - integrity sha512-HDe0wTR5nhd8Xr5xjGzoyTbdAw6rjy1GDplFt3JKtKN8/MnkQSRqK/n6aQQhpw5NI4ekDVOaW+w4sdxPBaCoTQ== - dependencies: - "@redux-saga/core" "^1.2.3" - -redux-thunk@^2.3.0: - version "2.4.2" - resolved "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.2.tgz" - integrity sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q== - -redux@^4.0.0, redux@^4.0.4, redux@^4.0.5: - 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" - -reflect-metadata@^0.1.13, reflect-metadata@^0.1.2: - version "0.1.13" - resolved "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz" - integrity sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg== - -regenerate-unicode-properties@^10.1.0: - version "10.1.0" - resolved "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.0.tgz" - integrity sha512-d1VudCLoIGitcU/hEg2QqvyGZQmdC0Lf8BqdOMXGFSvJP4bNV1+XqbPQeHHLD51Jh4QJJ225dlIFvY4Ly6MXmQ== - dependencies: - regenerate "^1.4.2" - -regenerate@^1.4.2: - version "1.4.2" - resolved "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz" - integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A== - -regenerator-runtime@^0.13.11, regenerator-runtime@^0.13.4: - version "0.13.11" - resolved "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz" - integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg== - -regenerator-transform@^0.15.1: - version "0.15.1" - resolved "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.1.tgz" - integrity sha512-knzmNAcuyxV+gQCufkYcvOqX/qIIfHLv0u5x79kRxuGojfYVky1f15TzZEu2Avte8QGepvUNTnLskf8E6X6Vyg== - dependencies: - "@babel/runtime" "^7.8.4" - -regex-parser@^2.2.11: - version "2.2.11" - resolved "https://registry.npmjs.org/regex-parser/-/regex-parser-2.2.11.tgz" - integrity sha512-jbD/FT0+9MBU2XAZluI7w2OBs1RBi6p9M83nkoZayQXXU9e8Robt69FcZc7wU4eJD/YFTjn1JdCk3rbMJajz8Q== - -regexp.prototype.flags@^1.4.3: - version "1.4.3" - resolved "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz" - integrity sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.3" - functions-have-names "^1.2.2" - -regexpu-core@^5.3.1: - version "5.3.2" - resolved "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.3.2.tgz" - integrity sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ== - dependencies: - "@babel/regjsgen" "^0.8.0" - regenerate "^1.4.2" - regenerate-unicode-properties "^10.1.0" - regjsparser "^0.9.1" - unicode-match-property-ecmascript "^2.0.0" - unicode-match-property-value-ecmascript "^2.1.0" - -regjsparser@^0.9.1: - version "0.9.1" - resolved "https://registry.npmjs.org/regjsparser/-/regjsparser-0.9.1.tgz" - integrity sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ== - dependencies: - jsesc "~0.5.0" - -request-progress@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/request-progress/-/request-progress-3.0.0.tgz" - integrity sha512-MnWzEHHaxHO2iWiQuHrUPBi/1WeBf5PkxQqNyNvLl9VAYSdXkP8tQ3pBSeCPD+yw0v0Aq1zosWLz0BdeXpWwZg== - dependencies: - throttleit "^1.0.0" - -request@^2.87.0: - version "2.88.2" - resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3" - integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw== - 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 "~2.3.2" - har-validator "~5.1.3" - http-signature "~1.2.0" - is-typedarray "~1.0.0" - isstream "~0.1.2" - json-stringify-safe "~5.0.1" - mime-types "~2.1.19" - oauth-sign "~0.9.0" - performance-now "^2.1.0" - qs "~6.5.2" - safe-buffer "^5.1.2" - tough-cookie "~2.5.0" - tunnel-agent "^0.6.0" - uuid "^3.3.2" - -require-directory@^2.1.1: - 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== - -require-from-string@^2.0.2: - 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== - -requires-port@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz" - integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ== - -reselect@^4.0.0: - version "4.1.7" - resolved "https://registry.npmjs.org/reselect/-/reselect-4.1.7.tgz" - integrity sha512-Zu1xbUt3/OPwsXL46hvOOoQrap2azE7ZQbokq61BQfiXvhewsKDwhMeZjTX9sX0nvw1t/U5Audyn1I9P/m9z0A== - -resolve-cwd@^3.0.0: - 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== - dependencies: - resolve-from "^5.0.0" - -resolve-from@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz" - integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== - -resolve-from@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz" - integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== - -resolve-url-loader@5.0.0: - 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== - 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" - -resolve@1.22.1: - version "1.22.1" - resolved "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz" - integrity sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw== - dependencies: - is-core-module "^2.9.0" - path-parse "^1.0.7" - supports-preserve-symlinks-flag "^1.0.0" - -resolve@^1.1.7, resolve@^1.14.2, resolve@^1.22.1, resolve@^1.9.0: - version "1.22.2" - resolved "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz" - integrity sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g== - dependencies: - is-core-module "^2.11.0" - path-parse "^1.0.7" - supports-preserve-symlinks-flag "^1.0.0" - -resp-modifier@6.0.2: - 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== - dependencies: - debug "^2.2.0" - minimatch "^3.0.2" - -restore-cursor@^3.1.0: - version "3.1.0" - resolved "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz" - integrity sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA== - dependencies: - onetime "^5.1.0" - signal-exit "^3.0.2" - -retry@^0.12.0: - version "0.12.0" - resolved "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz" - integrity sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow== - -retry@^0.13.1: - version "0.13.1" - resolved "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz" - integrity sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg== - -reusify@^1.0.4: - version "1.0.4" - resolved "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz" - integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== - -rfdc@^1.3.0: - version "1.3.0" - resolved "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz" - integrity sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA== - -rimraf@^2.2.8, rimraf@^2.5.2, rimraf@^2.6.3: - version "2.7.1" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" - integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== - dependencies: - glob "^7.1.3" - -rimraf@^3.0.0, rimraf@^3.0.2: - version "3.0.2" - resolved "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz" - integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== - dependencies: - glob "^7.1.3" - -rtl-css-js@^1.13.1: - 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" - -run-async@^2.4.0: - version "2.4.1" - resolved "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz" - integrity sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ== - -run-parallel@^1.1.9: - version "1.2.0" - resolved "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz" - integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== - dependencies: - queue-microtask "^1.2.2" - -rx@4.1.0: - version "4.1.0" - resolved "https://registry.npmjs.org/rx/-/rx-4.1.0.tgz" - integrity sha512-CiaiuN6gapkdl+cZUr67W6I8jquN4lkak3vtIsIWCl4XIPP8ffsoyN6/+PuGXnQy8Cu8W2y9Xxh31Rq4M6wUug== - -rxjs-report-usage@^1.0.4: - 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== - 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" - -rxjs-spy@^8.0.2: - version "8.0.2" - resolved "https://registry.npmjs.org/rxjs-spy/-/rxjs-spy-8.0.2.tgz" - integrity sha512-w2yc+EiwYA8J97hxqMD+pxGZkNbRCQwxR660r4nw4Soa8kCvatsdSRc0THndYk9uk6SvZy2RNyiVcxfX39pWpw== - dependencies: - "@types/circular-json" "^0.4.0" - "@types/stacktrace-js" "^0.0.33" - circular-json "^0.5.0" - error-stack-parser "^2.0.1" - rxjs-report-usage "^1.0.4" - stacktrace-gps "^3.0.2" - -rxjs@6.6.7, rxjs@^6.5.5, rxjs@~6.6.0: - version "6.6.7" - resolved "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz" - integrity sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ== - dependencies: - tslib "^1.9.0" - -rxjs@^7.5.1, rxjs@^7.5.5, rxjs@^7.8.0: - version "7.8.0" - resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.0.tgz#90a938862a82888ff4c7359811a595e14e1e09a4" - integrity sha512-F2+gxDshqmIub1KdvZkaEfGDwLNpPvk9Fs6LD/MyQxNgMds/WH9OdDDXOmxUZpME+iSK3rQCctkL0DYyytUqMg== - dependencies: - tslib "^2.1.0" - -safe-buffer@5.1.2, safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: - version "5.1.2" - resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz" - integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== - -safe-buffer@5.2.1, safe-buffer@~5.2.0: - version "5.2.1" - resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz" - integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== - -safe-regex-test@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz" - integrity sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA== - dependencies: - call-bind "^1.0.2" - get-intrinsic "^1.1.3" - is-regex "^1.1.4" - -"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: - version "2.1.2" - resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz" - integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== - -sanitize-html@^2.10.0: - version "2.10.0" - resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-2.10.0.tgz#74d28848dfcf72c39693139131895c78900ab452" - integrity sha512-JqdovUd81dG4k87vZt6uA6YhDfWkUGruUu/aPmXLxXi45gZExnt9Bnw/qeQU8oGf82vPyaE0vO4aH0PbobB9JQ== - dependencies: - deepmerge "^4.2.2" - escape-string-regexp "^4.0.0" - htmlparser2 "^8.0.0" - is-plain-object "^5.0.0" - parse-srcset "^1.0.2" - postcss "^8.3.11" - -sass-loader@13.2.0: - version "13.2.0" - resolved "https://registry.npmjs.org/sass-loader/-/sass-loader-13.2.0.tgz" - integrity sha512-JWEp48djQA4nbZxmgC02/Wh0eroSUutulROUusYJO9P9zltRbNN80JCBHqRGzjd4cmZCa/r88xgfkjGD0TXsHg== - dependencies: - klona "^2.0.4" - neo-async "^2.6.2" - -sass-loader@^12.6.0: - version "12.6.0" - resolved "https://registry.npmjs.org/sass-loader/-/sass-loader-12.6.0.tgz" - integrity sha512-oLTaH0YCtX4cfnJZxKSLAyglED0naiYfNG1iXfU5w1LNZ+ukoA5DtyDIN5zmKVZwYNJP4KRc5Y3hkWga+7tYfA== - dependencies: - klona "^2.0.4" - neo-async "^2.6.2" - -sass-resources-loader@^2.2.5: - version "2.2.5" - resolved "https://registry.yarnpkg.com/sass-resources-loader/-/sass-resources-loader-2.2.5.tgz#75131cdb26bae51fcffc007d8155d57b5e825ca7" - integrity sha512-po8rfETH9cOQACWxubT/1CCu77KjxwRtCDm6QAXZH99aUHBydwSoxdIjC40SGp/dcS/FkSNJl0j1VEojGZqlvQ== - dependencies: - async "^3.2.3" - chalk "^4.1.0" - glob "^7.1.6" - loader-utils "^2.0.0" - -sass@1.58.1: - version "1.58.1" - resolved "https://registry.npmjs.org/sass/-/sass-1.58.1.tgz" - integrity sha512-bnINi6nPXbP1XNRaranMFEBZWUfdW/AF16Ql5+ypRxfTvCRTTKrLsMIakyDcayUt2t/RZotmL4kgJwNH5xO+bg== - dependencies: - chokidar ">=3.0.0 <4.0.0" - immutable "^4.0.0" - source-map-js ">=0.6.2 <2.0.0" - -sass@~1.62.0: - version "1.62.0" - resolved "https://registry.yarnpkg.com/sass/-/sass-1.62.0.tgz#3686b2195b93295d20765135e562366b33ece37d" - integrity sha512-Q4USplo4pLYgCi+XlipZCWUQz5pkg/ruSSgJ0WRDSb/+3z9tXUOkQ7QPYn4XrhZKYAK4HlpaQecRwKLJX6+DBg== - dependencies: - chokidar ">=3.0.0 <4.0.0" - immutable "^4.0.0" - source-map-js ">=0.6.2 <2.0.0" - -sax@>=0.6.0, sax@^1.2.4: - version "1.2.4" - resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" - integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== - -saxes@^5.0.1: - version "5.0.1" - resolved "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz" - integrity sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw== - dependencies: - xmlchars "^2.2.0" - -saxes@^6.0.0: - version "6.0.0" - resolved "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz" - integrity sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA== - dependencies: - xmlchars "^2.2.0" - -scheduler@^0.19.1: - version "0.19.1" - resolved "https://registry.npmjs.org/scheduler/-/scheduler-0.19.1.tgz" - integrity sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA== - dependencies: - loose-envify "^1.1.0" - object-assign "^4.1.1" - -schema-utils@^3.0.0, schema-utils@^3.1.0, schema-utils@^3.1.1: - version "3.1.1" - resolved "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz" - integrity sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw== - dependencies: - "@types/json-schema" "^7.0.8" - ajv "^6.12.5" - ajv-keywords "^3.5.2" - -schema-utils@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.0.tgz" - integrity sha512-1edyXKgh6XnJsJSQ8mKWXnN/BVaIbFMLpouRUrXgVq7WYne5kw3MW7UPhO44uRXQSIpTSXoJbmrR2X0w9kUTyg== - dependencies: - "@types/json-schema" "^7.0.9" - ajv "^8.8.0" - ajv-formats "^2.1.1" - ajv-keywords "^5.0.0" - -select-hose@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz" - integrity sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg== - -selfsigned@^2.1.1: - version "2.1.1" - resolved "https://registry.npmjs.org/selfsigned/-/selfsigned-2.1.1.tgz" - integrity sha512-GSL3aowiF7wa/WtSFwnUrludWFoNhftq8bUkH9pkzjpN2XSPOAYEgg6e0sS9s0rZwgJzJiQRPU18A6clnoW5wQ== - dependencies: - node-forge "^1" - -semver@7.3.8, semver@^7.3.5, semver@^7.3.8: - version "7.3.8" - resolved "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz" - integrity sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A== - dependencies: - lru-cache "^6.0.0" - -semver@^5.3.0, semver@^5.6.0, semver@^5.7.1: - version "5.7.1" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" - integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== - -semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.3.0: - version "6.3.0" - resolved "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz" - integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== - -semver@^7.0.0, semver@^7.1.1, semver@^7.3.2, semver@^7.3.4, semver@^7.3.7: - version "7.4.0" - resolved "https://registry.npmjs.org/semver/-/semver-7.4.0.tgz" - integrity sha512-RgOxM8Mw+7Zus0+zcLEUn8+JfoLpj/huFTItQy2hsM4khuC1HYRDp0cU482Ewn/Fcy6bCjufD8vAj7voC66KQw== - dependencies: - lru-cache "^6.0.0" - -semver@~7.0.0: - version "7.0.0" - resolved "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz" - integrity sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A== - -send@0.16.2: - version "0.16.2" - resolved "https://registry.npmjs.org/send/-/send-0.16.2.tgz" - integrity sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw== - dependencies: - debug "2.6.9" - depd "~1.1.2" - destroy "~1.0.4" - encodeurl "~1.0.2" - escape-html "~1.0.3" - etag "~1.8.1" - fresh "0.5.2" - http-errors "~1.6.2" - mime "1.4.1" - ms "2.0.0" - on-finished "~2.3.0" - range-parser "~1.2.0" - statuses "~1.4.0" - -send@0.18.0: - version "0.18.0" - resolved "https://registry.npmjs.org/send/-/send-0.18.0.tgz" - integrity sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg== - 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" - -serialize-javascript@^5.0.1: - version "5.0.1" - resolved "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-5.0.1.tgz" - integrity sha512-SaaNal9imEO737H2c05Og0/8LUXG7EnsZyMa8MzkmuHoELfT6txuj0cMqRj6zfPKnmQ1yasR4PCJc8x+M4JSPA== - dependencies: - randombytes "^2.1.0" - -serialize-javascript@^6.0.0, serialize-javascript@^6.0.1: - version "6.0.1" - resolved "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz" - integrity sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w== - dependencies: - randombytes "^2.1.0" - -serve-index@1.9.1, serve-index@^1.9.1: - version "1.9.1" - resolved "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz" - integrity sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw== - 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" - -serve-static@1.13.2: - version "1.13.2" - resolved "https://registry.npmjs.org/serve-static/-/serve-static-1.13.2.tgz" - integrity sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw== - dependencies: - encodeurl "~1.0.2" - escape-html "~1.0.3" - parseurl "~1.3.2" - send "0.16.2" - -serve-static@1.15.0, serve-static@^1.14.1: - version "1.15.0" - resolved "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz" - integrity sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g== - dependencies: - encodeurl "~1.0.2" - escape-html "~1.0.3" - parseurl "~1.3.3" - send "0.18.0" - -server-destroy@1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/server-destroy/-/server-destroy-1.0.1.tgz" - integrity sha512-rb+9B5YBIEzYcD6x2VKidaa+cqYBJQKnU4oe4E3ANwRRN56yk/ua1YCJT1n21NTS8w6CcOclAKNP3PhdCXKYtQ== - -set-blocking@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz" - integrity sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw== - -setprototypeof@1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz" - integrity sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ== - -setprototypeof@1.2.0: - version "1.2.0" - resolved "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz" - integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== - -shallow-clone@^3.0.0: - version "3.0.1" - resolved "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz" - integrity sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA== - dependencies: - kind-of "^6.0.2" - -shallowequal@^1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz" - integrity sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ== - -shebang-command@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz" - integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== - dependencies: - shebang-regex "^3.0.0" - -shebang-regex@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz" - integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== - -shell-quote@^1.7.3: - version "1.8.1" - resolved "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz" - integrity sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA== - -side-channel@^1.0.4: - version "1.0.4" - resolved "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz" - integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw== - dependencies: - call-bind "^1.0.0" - get-intrinsic "^1.0.2" - object-inspect "^1.9.0" - -signal-exit@^3.0.2, signal-exit@^3.0.3, signal-exit@^3.0.7: - version "3.0.7" - resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz" - integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== - -sigstore@^1.0.0: - version "1.2.0" - resolved "https://registry.npmjs.org/sigstore/-/sigstore-1.2.0.tgz" - integrity sha512-Fr9+W1nkBSIZCkJQR7jDn/zI0UXNsVpp+7mDQkCnZOIxG9p6yNXBx9xntHsfUyYHE55XDkkVV3+rYbrkzAeesA== - dependencies: - "@sigstore/protobuf-specs" "^0.1.0" - make-fetch-happen "^11.0.1" - tuf-js "^1.0.0" - -simple-update-notifier@^1.0.7: - 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== - dependencies: - semver "~7.0.0" - -sirv@^1.0.7: - version "1.0.19" - resolved "https://registry.npmjs.org/sirv/-/sirv-1.0.19.tgz" - integrity sha512-JuLThK3TnZG1TAKDwNIqNq6QA2afLOCcm+iE8D1Kj3GA40pSPsxQjjJl0J8X3tsR7T+CP1GavpzLwYkgVLWrZQ== - dependencies: - "@polka/url" "^1.0.0-next.20" - mrmime "^1.0.0" - totalist "^1.0.0" - -sisteransi@^1.0.5: - version "1.0.5" - resolved "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz" - integrity sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg== - -slash@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz" - integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== - -slash@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz" - integrity sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew== - -slice-ansi@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz" - integrity sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ== - dependencies: - ansi-styles "^4.0.0" - astral-regex "^2.0.0" - is-fullwidth-code-point "^3.0.0" - -slice-ansi@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz" - integrity sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ== - dependencies: - ansi-styles "^4.0.0" - astral-regex "^2.0.0" - is-fullwidth-code-point "^3.0.0" - -slick@^1.12.2: - version "1.12.2" - resolved "https://registry.npmjs.org/slick/-/slick-1.12.2.tgz" - integrity sha512-4qdtOGcBjral6YIBCWJ0ljFSKNLz9KkhbWtuGvUyRowl1kxfuE1x/Z/aJcaiilpb3do9bl5K7/1h9XC5wWpY/A== - -smart-buffer@^4.2.0: - version "4.2.0" - resolved "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz" - integrity sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg== - -socket.io-adapter@~2.5.2: - version "2.5.2" - resolved "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.2.tgz" - integrity sha512-87C3LO/NOMc+eMcpcxUBebGjkpMDkNBS9tf7KJqcDsmL936EChtVva71Dw2q4tQcuVC+hAUy4an2NO/sYXmwRA== - dependencies: - ws "~8.11.0" - -socket.io-client@^4.4.1: - version "4.6.1" - resolved "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.6.1.tgz" - integrity sha512-5UswCV6hpaRsNg5kkEHVcbBIXEYoVbMQaHJBXJCyEQ+CiFPV1NIOY0XOFWG4XR4GZcB8Kn6AsRs/9cy9TbqVMQ== - dependencies: - "@socket.io/component-emitter" "~3.1.0" - debug "~4.3.2" - engine.io-client "~6.4.0" - socket.io-parser "~4.2.1" - -socket.io-parser@~4.2.1: - version "4.2.3" - resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.2.3.tgz#926bcc6658e2ae0883dc9dee69acbdc76e4e3667" - integrity sha512-JMafRntWVO2DCJimKsRTh/wnqVvO4hrfwOqtO7f+uzwsQMuxO6VwImtYxaQ+ieoyshWOTJyV0fA21lccEXRPpQ== - dependencies: - "@socket.io/component-emitter" "~3.1.0" - debug "~4.3.1" - -socket.io@^4.4.1: - version "4.6.1" - resolved "https://registry.npmjs.org/socket.io/-/socket.io-4.6.1.tgz" - integrity sha512-KMcaAi4l/8+xEjkRICl6ak8ySoxsYG+gG6/XfRCPJPQ/haCRIJBTL4wIl8YCsmtaBovcAXGLOShyVWQ/FG8GZA== - dependencies: - accepts "~1.3.4" - base64id "~2.0.0" - debug "~4.3.2" - engine.io "~6.4.1" - socket.io-adapter "~2.5.2" - socket.io-parser "~4.2.1" - -sockjs@^0.3.24: - version "0.3.24" - resolved "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz" - integrity sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ== - dependencies: - faye-websocket "^0.11.3" - uuid "^8.3.2" - websocket-driver "^0.7.4" - -socks-proxy-agent@^7.0.0: - version "7.0.0" - resolved "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-7.0.0.tgz" - integrity sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww== - dependencies: - agent-base "^6.0.2" - debug "^4.3.3" - socks "^2.6.2" - -socks@^2.6.2: - version "2.7.1" - resolved "https://registry.npmjs.org/socks/-/socks-2.7.1.tgz" - integrity sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ== - dependencies: - ip "^2.0.0" - smart-buffer "^4.2.0" - -sortablejs@1.15.0: - version "1.15.0" - resolved "https://registry.yarnpkg.com/sortablejs/-/sortablejs-1.15.0.tgz#53230b8aa3502bb77a29e2005808ffdb4a5f7e2a" - integrity sha512-bv9qgVMjUMf89wAvM6AxVvS/4MX3sPeN0+agqShejLU5z5GX4C75ow1O2e5k4L6XItUyAK3gH6AxSbXrOM5e8w== - -source-list-map@^2.0.0: - 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== - -"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz" - integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== - -source-map-loader@4.0.1: - version "4.0.1" - resolved "https://registry.npmjs.org/source-map-loader/-/source-map-loader-4.0.1.tgz" - integrity sha512-oqXpzDIByKONVY8g1NUPOTQhe0UTU5bWUl32GSkqK2LjJj0HmwTMVKxcUip0RgAYhY1mqgOxjbQM48a0mmeNfA== - dependencies: - abab "^2.0.6" - iconv-lite "^0.6.3" - source-map-js "^1.0.2" - -source-map-support@0.5.21, source-map-support@^0.5.17, source-map-support@^0.5.5, source-map-support@~0.5.20: - 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== - dependencies: - buffer-from "^1.0.0" - source-map "^0.6.0" - -source-map@0.5.6: - version "0.5.6" - resolved "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz" - integrity sha512-MjZkVp0NHr5+TPihLcadqnlVoGIoWo4IBHptutGh9wI3ttUYvCG26HkSuDi+K6lsZ25syXJXcctwgyVCt//xqA== - -source-map@0.6.1, source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0, source-map@~0.6.1: - version "0.6.1" - resolved "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz" - integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== - -source-map@0.7.3: - version "0.7.3" - resolved "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz" - integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ== - -source-map@0.7.4: - version "0.7.4" - resolved "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz" - integrity sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA== - -sourcemap-codec@^1.4.4: - version "1.4.8" - resolved "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz" - integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA== - -spdx-correct@^3.0.0: - version "3.2.0" - resolved "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz" - integrity sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA== - dependencies: - spdx-expression-parse "^3.0.0" - spdx-license-ids "^3.0.0" - -spdx-exceptions@^2.1.0: - version "2.3.0" - resolved "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz" - integrity sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A== - -spdx-expression-parse@^3.0.0, spdx-expression-parse@^3.0.1: - version "3.0.1" - resolved "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz" - integrity sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q== - dependencies: - spdx-exceptions "^2.1.0" - spdx-license-ids "^3.0.0" - -spdx-license-ids@^3.0.0: - version "3.0.13" - resolved "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.13.tgz" - integrity sha512-XkD+zwiqXHikFZm4AX/7JSCXA98U5Db4AFd5XUg/+9UNtnH75+Z9KxtpYiJZx36mUDVOwH83pl7yvCer6ewM3w== - -spdy-transport@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz" - integrity sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw== - 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" - -spdy@^4.0.2: - version "4.0.2" - resolved "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz" - integrity sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA== - 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" - -speech-rule-engine@^4.0.6: - version "4.0.7" - resolved "https://registry.npmjs.org/speech-rule-engine/-/speech-rule-engine-4.0.7.tgz" - integrity sha512-sJrL3/wHzNwJRLBdf6CjJWIlxC04iYKkyXvYSVsWVOiC2DSkHmxsqOhEeMsBA9XK+CHuNcsdkbFDnoUfAsmp9g== - dependencies: - commander "9.2.0" - wicked-good-xpath "1.3.0" - xmldom-sre "0.1.31" - -sprintf-js@~1.0.2: - version "1.0.3" - resolved "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz" - integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g== - -sshpk@^1.14.1, sshpk@^1.7.0: - version "1.17.0" - resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.17.0.tgz#578082d92d4fe612b13007496e543fa0fbcbe4c5" - integrity sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ== - 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" - -ssri@^10.0.0: - version "10.0.3" - resolved "https://registry.npmjs.org/ssri/-/ssri-10.0.3.tgz" - integrity sha512-lJtX/BFPI/VEtxZmLfeh7pzisIs6micwZ3eruD3+ds9aPsXKlYpwDS2Q7omD6WC42WO9+bnUSzlMmfv8uK8meg== - dependencies: - minipass "^4.0.0" - -ssri@^8.0.1: - version "8.0.1" - resolved "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz" - integrity sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ== - dependencies: - minipass "^3.1.1" - -ssri@^9.0.0: - version "9.0.1" - resolved "https://registry.npmjs.org/ssri/-/ssri-9.0.1.tgz" - integrity sha512-o57Wcn66jMQvfHG1FlYbWeZWW/dHZhJXjpIcTfXldXEk5nz5lStPo3mK0OJQfGR3RbZUlbISexbljkJzuEj/8Q== - dependencies: - minipass "^3.1.1" - -stackframe@^1.3.4: - version "1.3.4" - resolved "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz" - integrity sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw== - -stacktrace-gps@^3.0.2: - version "3.1.2" - resolved "https://registry.npmjs.org/stacktrace-gps/-/stacktrace-gps-3.1.2.tgz" - integrity sha512-GcUgbO4Jsqqg6RxfyTHFiPxdPqF+3LFmQhm7MgCuYQOYuWyqxo5pwRPz5d/u6/WYJdEnWfK4r+jGbyD8TSggXQ== - dependencies: - source-map "0.5.6" - stackframe "^1.3.4" - -statuses@2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz" - integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== - -"statuses@>= 1.4.0 < 2", statuses@~1.4.0: - version "1.4.0" - resolved "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz" - integrity sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew== - -statuses@~1.3.1: - version "1.3.1" - resolved "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz" - integrity sha512-wuTCPGlJONk/a1kqZ4fQM2+908lC7fa7nPYpTC1EhnvqLX/IICbeP1OZGDtA374trpSq68YubKUMo8oRhN46yg== - -statuses@~1.5.0: - version "1.5.0" - resolved "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz" - integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== - -stop-iteration-iterator@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz" - integrity sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ== - dependencies: - internal-slot "^1.0.4" - -stream-throttle@^0.1.3: - version "0.1.3" - resolved "https://registry.npmjs.org/stream-throttle/-/stream-throttle-0.1.3.tgz" - integrity sha512-889+B9vN9dq7/vLbGyuHeZ6/ctf5sNuGWsDy89uNxkFTAgzy0eK7+w5fL3KLNRTkLle7EgZGvHUphZW0Q26MnQ== - dependencies: - commander "^2.2.0" - limiter "^1.0.5" - -streamroller@^3.1.5: - version "3.1.5" - resolved "https://registry.npmjs.org/streamroller/-/streamroller-3.1.5.tgz" - integrity sha512-KFxaM7XT+irxvdqSP1LGLgNWbYN7ay5owZ3r/8t77p+EtSUAfUgtl7be3xtqtOmGUl9K9YPO2ca8133RlTjvKw== - dependencies: - date-format "^4.0.14" - debug "^4.3.4" - fs-extra "^8.1.0" - -"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: - 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" - -string.prototype.trim@^1.2.7: - version "1.2.7" - resolved "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.7.tgz" - integrity sha512-p6TmeT1T3411M8Cgg9wBTMRtY2q9+PNy9EV1i2lIXUN/btt763oIfxwN3RR8VU6wHX8j/1CFy0L+YuThm6bgOg== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.4" - es-abstract "^1.20.4" - -string.prototype.trimend@^1.0.6: - version "1.0.6" - resolved "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz" - integrity sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.4" - es-abstract "^1.20.4" - -string.prototype.trimstart@^1.0.6: - version "1.0.6" - resolved "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.6.tgz" - integrity sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.4" - es-abstract "^1.20.4" - -string_decoder@^1.1.1: - 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== - dependencies: - safe-buffer "~5.2.0" - -string_decoder@~1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz" - integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== - dependencies: - safe-buffer "~5.1.0" - -strip-ansi@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" - integrity sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg== - dependencies: - ansi-regex "^2.0.0" - -strip-ansi@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz" - integrity sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow== - dependencies: - ansi-regex "^3.0.0" - -strip-ansi@^6.0.0, strip-ansi@^6.0.1: - 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" - -strip-bom@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz" - integrity sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA== - -strip-final-newline@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz" - integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== - -strip-json-comments@3.1.1, strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: - 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== - -supports-color@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" - integrity sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g== - -supports-color@^5.3.0, supports-color@^5.4.0, supports-color@^5.5.0: - 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" - -supports-color@^7.1.0: - 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" - -supports-color@^8.0.0, supports-color@^8.1.1: - version "8.1.1" - resolved "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz" - integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== - dependencies: - has-flag "^4.0.0" - -supports-preserve-symlinks-flag@^1.0.0: - 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== - -symbol-observable@4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz" - integrity sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ== - -symbol-tree@^3.2.4: - version "3.2.4" - resolved "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz" - integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw== - -tapable@^2.1.1, tapable@^2.2.0: - version "2.2.1" - resolved "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz" - integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== - -tar@^6.0.2, tar@^6.1.11, tar@^6.1.2: - version "6.1.13" - resolved "https://registry.npmjs.org/tar/-/tar-6.1.13.tgz" - integrity sha512-jdIBIN6LTIe2jqzay/2vtYLlBHa3JF42ot3h1dW8Q0PaAG4v8rm0cvpVePtau5C6OKXGGcgO9q2AMNSWxiLqKw== - dependencies: - chownr "^2.0.0" - fs-minipass "^2.0.0" - minipass "^4.0.0" - minizlib "^2.1.1" - mkdirp "^1.0.3" - yallist "^4.0.0" - -terser-webpack-plugin@^5.1.3: - version "5.3.7" - resolved "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.7.tgz" - integrity sha512-AfKwIktyP7Cu50xNjXF/6Qb5lBNzYaWpU6YfoX3uZicTx0zTy0stDDCsvjDapKsSDvOeWo5MEq4TmdBy2cNoHw== - dependencies: - "@jridgewell/trace-mapping" "^0.3.17" - jest-worker "^27.4.5" - schema-utils "^3.1.1" - serialize-javascript "^6.0.1" - terser "^5.16.5" - -terser@5.16.3: - version "5.16.3" - resolved "https://registry.npmjs.org/terser/-/terser-5.16.3.tgz" - integrity sha512-v8wWLaS/xt3nE9dgKEWhNUFP6q4kngO5B8eYFUuebsu7Dw/UNAnpUod6UHo04jSSkv8TzKHjZDSd7EXdDQAl8Q== - dependencies: - "@jridgewell/source-map" "^0.3.2" - acorn "^8.5.0" - commander "^2.20.0" - source-map-support "~0.5.20" - -terser@^5.16.5: - version "5.16.9" - resolved "https://registry.npmjs.org/terser/-/terser-5.16.9.tgz" - integrity sha512-HPa/FdTB9XGI2H1/keLFZHxl6WNvAI4YalHGtDQTlMnJcoqSab1UwL4l1hGEhs6/GmLHBZIg/YgB++jcbzoOEg== - dependencies: - "@jridgewell/source-map" "^0.3.2" - acorn "^8.5.0" - commander "^2.20.0" - source-map-support "~0.5.20" - -test-exclude@^6.0.0: - 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== - dependencies: - "@istanbuljs/schema" "^0.1.2" - glob "^7.1.4" - minimatch "^3.0.4" - -text-table@0.2.0, text-table@^0.2.0: - version "0.2.0" - resolved "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz" - integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== - -throttle-debounce@^2.1.0: - version "2.3.0" - resolved "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-2.3.0.tgz" - integrity sha512-H7oLPV0P7+jgvrk+6mwwwBDmxTaxnu9HMXmloNLXwnNO0ZxZ31Orah2n8lU1eMPvsaowP2CX+USCgyovXfdOFQ== - -throttleit@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/throttleit/-/throttleit-1.0.0.tgz" - integrity sha512-rkTVqu6IjfQ/6+uNuuc3sZek4CEYxTJom3IktzgdSxcZqdARuebbA/f4QmAxMQIxqq9ZLEUkSYqvuk1I6VKq4g== - -through@^2.3.6, through@^2.3.8: - version "2.3.8" - resolved "https://registry.npmjs.org/through/-/through-2.3.8.tgz" - integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg== - -thunky@^1.0.2: - version "1.1.0" - resolved "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz" - integrity sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA== - -tiny-invariant@^1.0.6: - version "1.3.1" - resolved "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.1.tgz" - integrity sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw== - -tiny-warning@^1.0.2: - version "1.0.3" - resolved "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz" - integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA== - -tmp@0.2.1, tmp@^0.2.1, tmp@~0.2.1: - version "0.2.1" - resolved "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz" - integrity sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ== - dependencies: - rimraf "^3.0.0" - -tmp@^0.0.33: - version "0.0.33" - resolved "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz" - integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw== - dependencies: - os-tmpdir "~1.0.2" - -to-fast-properties@^2.0.0: - 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== - -to-regex-range@^5.0.1: - 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" - -toggle-selection@^1.0.6: - version "1.0.6" - resolved "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz" - integrity sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ== - -toidentifier@1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz" - integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== - -totalist@^1.0.0: - version "1.1.0" - resolved "https://registry.npmjs.org/totalist/-/totalist-1.1.0.tgz" - integrity sha512-gduQwd1rOdDMGxFG1gEvhV88Oirdo2p+KjoYFU7k2g+i7n6AFFbDQ5kMPUsW0pNbfQsB/cwXvT1i4Bue0s9g5g== - -touch@^3.1.0: - version "3.1.0" - resolved "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz" - integrity sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA== - dependencies: - nopt "~1.0.10" - -tough-cookie@^4.0.0, tough-cookie@^4.1.2: - version "4.1.2" - resolved "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.2.tgz" - integrity sha512-G9fqXWoYFZgTc2z8Q5zaHy/vJMjm+WV0AkAeHxVCQiEB1b+dGvWzFW6QV07cY5jQ5gRkeid2qIkzkxUnmoQZUQ== - dependencies: - psl "^1.1.33" - punycode "^2.1.1" - universalify "^0.2.0" - url-parse "^1.5.3" - -tough-cookie@~2.5.0: - version "2.5.0" - resolved "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz" - integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g== - dependencies: - psl "^1.1.28" - punycode "^2.1.1" - -tr46@^2.1.0: - version "2.1.0" - resolved "https://registry.npmjs.org/tr46/-/tr46-2.1.0.tgz" - integrity sha512-15Ih7phfcdP5YxqiB+iDtLoaTz4Nd35+IiAv0kQ5FNKHzXgdWqPoTIqEDDJmXceQt4JZk6lVPT8lnDlPpGDppw== - dependencies: - punycode "^2.1.1" - -tr46@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz" - integrity sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA== - dependencies: - punycode "^2.1.1" - -tr46@~0.0.3: - version "0.0.3" - resolved "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz" - integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== - -tree-kill@1.2.2, tree-kill@^1.2.2: - version "1.2.2" - resolved "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz" - integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A== - -ts-node@10.2.1, ts-node@^10.0.0: - version "10.2.1" - resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.2.1.tgz#4cc93bea0a7aba2179497e65bb08ddfc198b3ab5" - integrity sha512-hCnyOyuGmD5wHleOQX6NIjJtYVIO8bPP8F2acWkB4W06wdlkgyvJtubO/I9NkI88hCFECbsEgoLc0VNkYmcSfw== - dependencies: - "@cspotcode/source-map-support" "0.6.1" - "@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" - yn "3.1.1" - -ts-node@^8.10.2: - version "8.10.2" - resolved "https://registry.npmjs.org/ts-node/-/ts-node-8.10.2.tgz" - integrity sha512-ISJJGgkIpDdBhWVu3jufsWpK3Rzo7bdiIXJjQc0ynKxVOVcg2oIrf2H2cejminGrptVc6q6/uynAHNCuWGbpVA== - dependencies: - arg "^4.1.0" - diff "^4.0.1" - make-error "^1.1.1" - source-map-support "^0.5.17" - yn "3.1.1" - -tsconfig-paths@^3.14.1: - version "3.14.2" - resolved "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz" - integrity sha512-o/9iXgCYc5L/JxCHPe3Hvh8Q/2xm5Z+p18PESBU6Ff33695QnCHBEjcytY2q19ua7Mbl/DavtBOLq+oG0RCL+g== - dependencies: - "@types/json5" "^0.0.29" - json5 "^1.0.2" - minimist "^1.2.6" - strip-bom "^3.0.0" - -tsconfig-paths@^4.1.0: - version "4.2.0" - resolved "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz" - integrity sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg== - dependencies: - json5 "^2.2.2" - minimist "^1.2.6" - strip-bom "^3.0.0" - -tslib@2.3.1: - version "2.3.1" - resolved "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz" - integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw== - -tslib@2.5.0, tslib@^2.0.0, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.2.0, tslib@^2.3.0, tslib@^2.3.1: - version "2.5.0" - resolved "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz" - integrity sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg== - -tslib@^1.8.1, tslib@^1.9.0: - version "1.14.1" - resolved "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz" - integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== - -tsutils@^3.21.0: - version "3.21.0" - resolved "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz" - integrity sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA== - dependencies: - tslib "^1.8.1" - -tuf-js@^1.0.0: - version "1.1.3" - resolved "https://registry.npmjs.org/tuf-js/-/tuf-js-1.1.3.tgz" - integrity sha512-jGYi5nG/kqgfTFQSdoN6PW9eIn+XRZqdXku+fSwNk6UpWIsWaV7pzAqPgFr85edOPhoyJDyBqCS+DCnHroMvrw== - dependencies: - "@tufjs/models" "1.0.2" - make-fetch-happen "^11.0.1" - -tunnel-agent@^0.6.0: - version "0.6.0" - resolved "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz" - integrity sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w== - dependencies: - safe-buffer "^5.0.1" - -tweetnacl@^0.14.3, tweetnacl@~0.14.0: - version "0.14.5" - resolved "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz" - integrity sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA== - -type-check@^0.4.0, type-check@~0.4.0: - version "0.4.0" - resolved "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz" - integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew== - dependencies: - prelude-ls "^1.2.1" - -type-check@~0.3.2: - version "0.3.2" - resolved "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz" - integrity sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg== - dependencies: - prelude-ls "~1.1.2" - -type-fest@^0.20.2: - 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== - -type-fest@^0.21.3: - 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== - -type-is@~1.6.18: - 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" - -typed-array-length@^1.0.4: - version "1.0.4" - resolved "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz" - integrity sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng== - dependencies: - call-bind "^1.0.2" - for-each "^0.3.3" - is-typed-array "^1.1.9" - -typed-assert@^1.0.8: - version "1.0.9" - resolved "https://registry.npmjs.org/typed-assert/-/typed-assert-1.0.9.tgz" - integrity sha512-KNNZtayBCtmnNmbo5mG47p1XsCyrx6iVqomjcZnec/1Y5GGARaxPs6r49RnSPeUP3YjNYiU9sQHAtY4BBvnZwg== - -typescript-compare@^0.0.2: - 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" - -typescript-logic@^0.0.0: - version "0.0.0" - resolved "https://registry.npmjs.org/typescript-logic/-/typescript-logic-0.0.0.tgz" - integrity sha512-zXFars5LUkI3zP492ls0VskH3TtdeHCqu0i7/duGt60i5IGPIpAHE/DWo5FqJ6EjQ15YKXrt+AETjv60Dat34Q== - -typescript-tuple@^2.2.1: - 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" - -typescript@^2.5.0: - version "2.9.2" - resolved "https://registry.npmjs.org/typescript/-/typescript-2.9.2.tgz" - integrity sha512-Gr4p6nFNaoufRIY4NMdpQRNmgxVIGMs4Fcu/ujdYk3nAZqk7supzBE9idmvfZIlH/Cuj//dvi+019qEue9lV0w== - -typescript@~4.8.4: - version "4.8.4" - resolved "https://registry.npmjs.org/typescript/-/typescript-4.8.4.tgz" - integrity sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ== - -ua-parser-js@^0.7.30: - version "0.7.35" - resolved "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.35.tgz" - integrity sha512-veRf7dawaj9xaWEu9HoTVn5Pggtc/qj+kqTOFvNiN1l0YdxwC1kvel57UCjThjGa3BHBihE8/UJAHI+uQHmd/g== - -ua-parser-js@^1.0.33: - version "1.0.35" - resolved "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.35.tgz" - integrity sha512-fKnGuqmTBnIE+/KXSzCn4db8RTigUzw1AN0DmdU6hJovUTbYJKyqj+8Mt1c4VfRDnOVJnENmfYkIPZ946UrSAA== - -uc.micro@^1.0.1, uc.micro@^1.0.5: - version "1.0.6" - resolved "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz" - integrity sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA== - -unbox-primitive@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz" - integrity sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw== - dependencies: - call-bind "^1.0.2" - has-bigints "^1.0.2" - has-symbols "^1.0.3" - which-boxed-primitive "^1.0.2" - -undefsafe@^2.0.5: - version "2.0.5" - resolved "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz" - integrity sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA== - -unfetch@^4.2.0: - version "4.2.0" - resolved "https://registry.npmjs.org/unfetch/-/unfetch-4.2.0.tgz" - integrity sha512-F9p7yYCn6cIW9El1zi0HI6vqpeIvBsr3dSuRO6Xuppb1u5rXpCPmMvLSyECLhybr9isec8Ohl0hPekMVrEinDA== - -unicode-canonical-property-names-ecmascript@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz" - integrity sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ== - -unicode-match-property-ecmascript@^2.0.0: - 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== - dependencies: - unicode-canonical-property-names-ecmascript "^2.0.0" - unicode-property-aliases-ecmascript "^2.0.0" - -unicode-match-property-value-ecmascript@^2.1.0: - version "2.1.0" - resolved "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz" - integrity sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA== - -unicode-property-aliases-ecmascript@^2.0.0: - 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== - -unique-filename@^1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz" - integrity sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ== - dependencies: - unique-slug "^2.0.0" - -unique-filename@^2.0.0: - version "2.0.1" - resolved "https://registry.npmjs.org/unique-filename/-/unique-filename-2.0.1.tgz" - integrity sha512-ODWHtkkdx3IAR+veKxFV+VBkUMcN+FaqzUUd7IZzt+0zhDZFPFxhlqwPF3YQvMHx1TD0tdgYl+kuPnJ8E6ql7A== - dependencies: - unique-slug "^3.0.0" - -unique-filename@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/unique-filename/-/unique-filename-3.0.0.tgz" - integrity sha512-afXhuC55wkAmZ0P18QsVE6kp8JaxrEokN2HGIoIVv2ijHQd419H0+6EigAFcIzXeMIkcIkNBpB3L/DXB3cTS/g== - dependencies: - unique-slug "^4.0.0" - -unique-slug@^2.0.0: - version "2.0.2" - resolved "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz" - integrity sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w== - dependencies: - imurmurhash "^0.1.4" - -unique-slug@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/unique-slug/-/unique-slug-3.0.0.tgz" - integrity sha512-8EyMynh679x/0gqE9fT9oilG+qEt+ibFyqjuVTsZn1+CMxH+XLlpvr2UZx4nVcCwTpx81nICr2JQFkM+HPLq4w== - dependencies: - imurmurhash "^0.1.4" - -unique-slug@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/unique-slug/-/unique-slug-4.0.0.tgz" - integrity sha512-WrcA6AyEfqDX5bWige/4NQfPZMtASNVxdmWR76WESYQVAACSgWcR6e9i0mofqqBxYFtL4oAxPIptY73/0YE1DQ== - dependencies: - imurmurhash "^0.1.4" - -universalify@^0.1.0: - version "0.1.2" - resolved "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz" - integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== - -universalify@^0.2.0: - version "0.2.0" - resolved "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz" - integrity sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg== - -universalify@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz" - integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== - -unpipe@1.0.0, unpipe@~1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz" - integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== - -untildify@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz" - integrity sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw== - -update-browserslist-db@^1.0.10: - version "1.0.10" - resolved "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz" - integrity sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ== - dependencies: - escalade "^3.1.1" - picocolors "^1.0.0" - -uri-js@^4.2.2: - version "4.4.1" - resolved "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz" - integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== - dependencies: - punycode "^2.1.0" - -url-parse@^1.5.3: - version "1.5.10" - resolved "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz" - integrity sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ== - dependencies: - querystringify "^2.1.1" - requires-port "^1.0.0" - -use-memo-one@^1.1.1: - 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== - -util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1: - version "1.0.2" - resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" - integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== - -utils-merge@1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz" - integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== - -uuid@^3.2.1, uuid@^3.3.2: - version "3.4.0" - resolved "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz" - integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== - -uuid@^8.1.0, uuid@^8.3.2: - version "8.3.2" - resolved "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz" - integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== - -valid-data-url@^3.0.0: - version "3.0.1" - resolved "https://registry.npmjs.org/valid-data-url/-/valid-data-url-3.0.1.tgz" - integrity sha512-jOWVmzVceKlVVdwjNSenT4PbGghU0SBIizAev8ofZVgivk/TVHXSbNL8LP6M3spZvkR9/QolkyJavGSX5Cs0UA== - -validate-npm-package-license@^3.0.4: - 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== - dependencies: - spdx-correct "^3.0.0" - spdx-expression-parse "^3.0.0" - -validate-npm-package-name@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-5.0.0.tgz" - integrity sha512-YuKoXDAhBYxY7SfOKxHBDoSyENFeW5VvIIQp2TGQuit8gpK6MnWaQelBKxso72DoxTZfZdcP3W90LqpSkgPzLQ== - dependencies: - builtins "^5.0.0" - -vary@^1, vary@~1.1.2: - version "1.1.2" - resolved "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz" - integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== - -verror@1.10.0: - version "1.10.0" - resolved "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz" - integrity sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw== - dependencies: - assert-plus "^1.0.0" - core-util-is "1.0.2" - extsprintf "^1.2.0" - -void-elements@3.1.0: - 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== - -void-elements@^2.0.0: - version "2.0.1" - resolved "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz" - integrity sha512-qZKX4RnBzH2ugr8Lxa7x+0V6XD9Sb/ouARtiasEQCHB1EVU4NXtmHsDDrx1dO4ne5fc3J6EW05BP1Dl0z0iung== - -w3c-hr-time@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz" - integrity sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ== - dependencies: - browser-process-hrtime "^1.0.0" - -w3c-xmlserializer@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-2.0.0.tgz" - integrity sha512-4tzD0mF8iSiMiNs30BiLO3EpfGLZUT2MSX/G+o7ZywDzliWQ3OPtTZ0PTC3B3ca1UAf4cJMHB+2Bf56EriJuRA== - dependencies: - xml-name-validator "^3.0.0" - -w3c-xmlserializer@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz" - integrity sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw== - dependencies: - xml-name-validator "^4.0.0" - -watchpack@^2.4.0: - version "2.4.0" - resolved "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz" - integrity sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg== - dependencies: - glob-to-regexp "^0.4.1" - graceful-fs "^4.1.2" - -wbuf@^1.1.0, wbuf@^1.7.3: - version "1.7.3" - resolved "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz" - integrity sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA== - dependencies: - minimalistic-assert "^1.0.0" - -wcwidth@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz" - integrity sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg== - dependencies: - defaults "^1.0.3" - -web-resource-inliner@^6.0.1: - version "6.0.1" - resolved "https://registry.npmjs.org/web-resource-inliner/-/web-resource-inliner-6.0.1.tgz" - integrity sha512-kfqDxt5dTB1JhqsCUQVFDj0rmY+4HLwGQIsLPbyrsN9y9WV/1oFDSx3BQ4GfCv9X+jVeQ7rouTqwK53rA/7t8A== - dependencies: - ansi-colors "^4.1.1" - escape-goat "^3.0.0" - htmlparser2 "^5.0.0" - mime "^2.4.6" - node-fetch "^2.6.0" - valid-data-url "^3.0.0" - -webdriver-manager@^12.1.8: - version "12.1.9" - resolved "https://registry.yarnpkg.com/webdriver-manager/-/webdriver-manager-12.1.9.tgz#8d83543b92711b7217b39fef4cda958a4703d2df" - integrity sha512-Yl113uKm8z4m/KMUVWHq1Sjtla2uxEBtx2Ue3AmIlnlPAKloDn/Lvmy6pqWCUersVISpdMeVpAaGbNnvMuT2LQ== - dependencies: - adm-zip "^0.5.2" - chalk "^1.1.1" - del "^2.2.0" - glob "^7.0.3" - ini "^1.3.4" - minimist "^1.2.0" - q "^1.4.1" - request "^2.87.0" - rimraf "^2.5.2" - semver "^5.3.0" - xml2js "^0.4.17" - -webfontloader@1.6.28: - version "1.6.28" - resolved "https://registry.npmjs.org/webfontloader/-/webfontloader-1.6.28.tgz" - integrity sha512-Egb0oFEga6f+nSgasH3E0M405Pzn6y3/9tOVanv/DLfa1YBIgcv90L18YyWnvXkRbIM17v5Kv6IT2N6g1x5tvQ== - -webidl-conversions@^3.0.0: - version "3.0.1" - resolved "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz" - integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== - -webidl-conversions@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz" - integrity sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA== - -webidl-conversions@^6.1.0: - version "6.1.0" - resolved "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz" - integrity sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w== - -webidl-conversions@^7.0.0: - version "7.0.0" - resolved "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz" - integrity sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g== - -webpack-bundle-analyzer@^4.8.0: - version "4.8.0" - resolved "https://registry.yarnpkg.com/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.8.0.tgz#951b8aaf491f665d2ae325d8b84da229157b1d04" - integrity sha512-ZzoSBePshOKhr+hd8u6oCkZVwpVaXgpw23ScGLFpR6SjYI7+7iIWYarjN6OEYOfRt8o7ZyZZQk0DuMizJ+LEIg== - dependencies: - "@discoveryjs/json-ext" "0.5.7" - acorn "^8.0.4" - acorn-walk "^8.0.0" - chalk "^4.1.0" - commander "^7.2.0" - gzip-size "^6.0.0" - lodash "^4.17.20" - opener "^1.5.2" - sirv "^1.0.7" - ws "^7.3.1" - -webpack-cli@^4.2.0: - version "4.10.0" - resolved "https://registry.npmjs.org/webpack-cli/-/webpack-cli-4.10.0.tgz" - integrity sha512-NLhDfH/h4O6UOy+0LSso42xvYypClINuMNBVVzX4vX98TmTaTUxwRbXdhucbFMd2qLaCTcLq/PdYrvi8onw90w== - dependencies: - "@discoveryjs/json-ext" "^0.5.0" - "@webpack-cli/configtest" "^1.2.0" - "@webpack-cli/info" "^1.5.0" - "@webpack-cli/serve" "^1.7.0" - colorette "^2.0.14" - commander "^7.0.0" - cross-spawn "^7.0.3" - fastest-levenshtein "^1.0.12" - import-local "^3.0.2" - interpret "^2.2.0" - rechoir "^0.7.0" - webpack-merge "^5.7.3" - -webpack-dev-middleware@6.0.1: - version "6.0.1" - resolved "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-6.0.1.tgz" - integrity sha512-PZPZ6jFinmqVPJZbisfggDiC+2EeGZ1ZByyMP5sOFJcPPWSexalISz+cvm+j+oYPT7FIJyxT76esjnw9DhE5sw== - dependencies: - colorette "^2.0.10" - memfs "^3.4.12" - mime-types "^2.1.31" - range-parser "^1.2.1" - schema-utils "^4.0.0" - -webpack-dev-middleware@^5.3.1: - version "5.3.3" - resolved "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.3.tgz" - integrity sha512-hj5CYrY0bZLB+eTO+x/j67Pkrquiy7kWepMHmUMoPsmcUaeEnQJqFzHJOyxgWlq746/wUuA64p9ta34Kyb01pA== - dependencies: - colorette "^2.0.10" - memfs "^3.4.3" - mime-types "^2.1.31" - range-parser "^1.2.1" - schema-utils "^4.0.0" - -webpack-dev-server@4.11.1: - version "4.11.1" - resolved "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.11.1.tgz" - integrity sha512-lILVz9tAUy1zGFwieuaQtYiadImb5M3d+H+L1zDYalYoDl0cksAB1UNyuE5MMWJrG6zR1tXkCP2fitl7yoUJiw== - 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.1" - 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" - 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.4.2" - -webpack-dev-server@^4.13.3: - version "4.13.3" - resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-4.13.3.tgz#9feb740b8b56b886260bae1360286818a221bae8" - integrity sha512-KqqzrzMRSRy5ePz10VhjyL27K2dxqwXQLP5rAKwRJBPUahe7Z2bBWzHw37jeb8GCPKxZRO79ZdQUAPesMh/Nug== - 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.1" - 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" - -webpack-merge@5.8.0, webpack-merge@^5.7.3: - version "5.8.0" - resolved "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.8.0.tgz" - integrity sha512-/SaI7xY0831XwP6kzuwhKWVKDP9t1QY1h65lAFLbZqMPIuYcD9QAW4u9STIbU9kaJbPBB/geU/gLr1wDjOhQ+Q== - dependencies: - clone-deep "^4.0.1" - wildcard "^2.0.0" - -webpack-sources@^1.4.3: - version "1.4.3" - resolved "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz" - integrity sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ== - dependencies: - source-list-map "^2.0.0" - source-map "~0.6.1" - -webpack-sources@^3.0.0, webpack-sources@^3.2.3: - 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== - -webpack-subresource-integrity@5.1.0: - 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== - dependencies: - typed-assert "^1.0.8" - -webpack@5.76.1: - version "5.76.1" - resolved "https://registry.npmjs.org/webpack/-/webpack-5.76.1.tgz" - integrity sha512-4+YIK4Abzv8172/SGqObnUjaIHjLEuUasz9EwQj/9xmPPkYJy2Mh03Q/lJfSD3YLzbxy5FeTq5Uw0323Oh6SJQ== - dependencies: - "@types/eslint-scope" "^3.7.3" - "@types/estree" "^0.0.51" - "@webassemblyjs/ast" "1.11.1" - "@webassemblyjs/wasm-edit" "1.11.1" - "@webassemblyjs/wasm-parser" "1.11.1" - acorn "^8.7.1" - acorn-import-assertions "^1.7.6" - browserslist "^4.14.5" - chrome-trace-event "^1.0.2" - enhanced-resolve "^5.10.0" - es-module-lexer "^0.9.0" - eslint-scope "5.1.1" - events "^3.2.0" - glob-to-regexp "^0.4.1" - graceful-fs "^4.2.9" - 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.1.0" - tapable "^2.1.1" - terser-webpack-plugin "^5.1.3" - watchpack "^2.4.0" - webpack-sources "^3.2.3" - -websocket-driver@>=0.5.1, websocket-driver@^0.7.4: - version "0.7.4" - resolved "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz" - integrity sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg== - dependencies: - http-parser-js ">=0.5.1" - safe-buffer ">=5.1.0" - websocket-extensions ">=0.1.1" - -websocket-extensions@>=0.1.1: - version "0.1.4" - resolved "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz" - integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg== - -whatwg-encoding@^1.0.5: - version "1.0.5" - resolved "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz" - integrity sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw== - dependencies: - iconv-lite "0.4.24" - -whatwg-encoding@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz" - integrity sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg== - dependencies: - iconv-lite "0.6.3" - -whatwg-mimetype@^2.3.0: - version "2.3.0" - resolved "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz" - integrity sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g== - -whatwg-mimetype@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz" - integrity sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q== - -whatwg-url@^11.0.0: - version "11.0.0" - resolved "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz" - integrity sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ== - dependencies: - tr46 "^3.0.0" - webidl-conversions "^7.0.0" - -whatwg-url@^5.0.0: - 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" - -whatwg-url@^8.0.0, whatwg-url@^8.5.0: - version "8.7.0" - resolved "https://registry.npmjs.org/whatwg-url/-/whatwg-url-8.7.0.tgz" - integrity sha512-gAojqb/m9Q8a5IV96E3fHJM70AzCkgt4uXYX2O7EmuyOnLrViCQlsEBmF9UQIu3/aeAIp2U17rtbpZWNntQqdg== - dependencies: - lodash "^4.7.0" - tr46 "^2.1.0" - webidl-conversions "^6.1.0" - -which-boxed-primitive@^1.0.2: - version "1.0.2" - resolved "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz" - integrity sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg== - 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" - -which-collection@^1.0.1: - version "1.0.1" - resolved "https://registry.npmjs.org/which-collection/-/which-collection-1.0.1.tgz" - integrity sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A== - dependencies: - is-map "^2.0.1" - is-set "^2.0.1" - is-weakmap "^2.0.1" - is-weakset "^2.0.1" - -which-typed-array@^1.1.9: - version "1.1.9" - resolved "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.9.tgz" - integrity sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA== - dependencies: - available-typed-arrays "^1.0.5" - call-bind "^1.0.2" - for-each "^0.3.3" - gopd "^1.0.1" - has-tostringtag "^1.0.0" - is-typed-array "^1.1.10" - -which@^1.2.1: - version "1.3.1" - resolved "https://registry.npmjs.org/which/-/which-1.3.1.tgz" - integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== - dependencies: - isexe "^2.0.0" - -which@^2.0.1, which@^2.0.2: - 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" - -which@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/which/-/which-3.0.0.tgz" - integrity sha512-nla//68K9NU6yRiwDY/Q8aU6siKlSs64aEC7+IV56QoAuyQT2ovsJcgGYGyqMOmI/CGN1BOR6mM5EN0FBO+zyQ== - dependencies: - isexe "^2.0.0" - -wicked-good-xpath@1.3.0: - version "1.3.0" - resolved "https://registry.npmjs.org/wicked-good-xpath/-/wicked-good-xpath-1.3.0.tgz" - integrity sha512-Gd9+TUn5nXdwj/hFsPVx5cuHHiF5Bwuc30jZ4+ronF1qHK5O7HD0sgmXWSEgwKquT3ClLoKPVbO6qGwVwLzvAw== - -wide-align@^1.1.5: - version "1.1.5" - resolved "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz" - integrity sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg== - dependencies: - string-width "^1.0.2 || 2 || 3 || 4" - -wildcard@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/wildcard/-/wildcard-2.0.0.tgz" - integrity sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw== - -word-wrap@^1.2.3, word-wrap@~1.2.3: - version "1.2.3" - resolved "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz" - integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== - -wrap-ansi@^6.2.0: - version "6.2.0" - resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz" - integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - -wrap-ansi@^7.0.0: - 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" - -wrappy@1: - version "1.0.2" - resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" - integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== - -ws@^7.3.1, ws@^7.4.6: - version "7.5.9" - resolved "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz" - integrity sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q== - -ws@^8.11.0, ws@^8.13.0, ws@^8.4.2: - version "8.13.0" - resolved "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz" - integrity sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA== - -ws@~8.11.0: - version "8.11.0" - resolved "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz" - integrity sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg== - -xhr2@^0.2.0: - version "0.2.1" - resolved "https://registry.npmjs.org/xhr2/-/xhr2-0.2.1.tgz" - integrity sha512-sID0rrVCqkVNUn8t6xuv9+6FViXjUVXq8H5rWOH2rz9fDNQEd4g0EA2XlcEdJXRz5BMEn4O1pJFdT+z4YHhoWw== - -xml-name-validator@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz" - integrity sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw== - -xml-name-validator@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz" - integrity sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw== - -xml2js@^0.4.17: - version "0.4.23" - resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.23.tgz#a0c69516752421eb2ac758ee4d4ccf58843eac66" - integrity sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug== - dependencies: - sax ">=0.6.0" - xmlbuilder "~11.0.0" - -xmlbuilder@~11.0.0: - version "11.0.1" - resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-11.0.1.tgz#be9bae1c8a046e76b31127726347d0ad7002beb3" - integrity sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA== - -xmlchars@^2.2.0: - version "2.2.0" - resolved "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz" - integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw== - -xmldom-sre@0.1.31: - version "0.1.31" - resolved "https://registry.npmjs.org/xmldom-sre/-/xmldom-sre-0.1.31.tgz" - integrity sha512-f9s+fUkX04BxQf+7mMWAp5zk61pciie+fFLC9hX9UVvCeJQfNHRHXpeo5MPcR0EUf57PYLdt+ZO4f3Ipk2oZUw== - -xmlhttprequest-ssl@~2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz" - integrity sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A== - -y18n@^5.0.5: - version "5.0.8" - resolved "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz" - integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== - -yallist@^3.0.2: - version "3.1.1" - resolved "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz" - integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== - -yallist@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz" - integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== - -yaml@^1.10.0: - version "1.10.2" - resolved "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz" - integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== - -yargs-parser@^20.2.2: - version "20.2.9" - resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz" - integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== - -yargs-parser@^21.1.1: - version "21.1.1" - resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz" - integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== - -yargs@17.1.1: - version "17.1.1" - resolved "https://registry.npmjs.org/yargs/-/yargs-17.1.1.tgz" - integrity sha512-c2k48R0PwKIqKhPMWjeiF6y2xY/gPMUlro0sgxqXpbOIohWiLNXWslsootttv7E1e73QPAMQSg5FeySbVcpsPQ== - 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" - -yargs@17.6.2: - version "17.6.2" - resolved "https://registry.npmjs.org/yargs/-/yargs-17.6.2.tgz" - integrity sha512-1/9UrdHjDZc0eOU0HxOHoS78C69UD3JRMvzlJ7S79S2nTaWRA/whGCTV8o9e/N/1Va9YIV7Q4sOxD8VV4pCWOw== - 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" - -yargs@^16.1.1: - version "16.2.0" - resolved "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz" - integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== - 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" - -yargs@^17.2.1, yargs@^17.3.1: - version "17.7.1" - resolved "https://registry.npmjs.org/yargs/-/yargs-17.7.1.tgz" - integrity sha512-cwiTb08Xuv5fqF4AovYacTFNxk62th7LKJ6BL9IGUpTJrWoU7/7WdQGTP2SjKf1dUNBGzDd28p/Yfs/GI6JrLw== - 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" - -yauzl@^2.10.0: - version "2.10.0" - resolved "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz" - integrity sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g== - dependencies: - buffer-crc32 "~0.2.3" - fd-slicer "~1.1.0" - -yn@3.1.1: - version "3.1.1" - resolved "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz" - integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== - -yocto-queue@^0.1.0: - version "0.1.0" - resolved "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz" - integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== - -zone.js@~0.11.5: - version "0.11.8" - resolved "https://registry.npmjs.org/zone.js/-/zone.js-0.11.8.tgz" - integrity sha512-82bctBg2hKcEJ21humWIkXRlLBBmrc3nN7DFh5LGGhcyycO2S7FN8NmdvlcKaGFDNVL4/9kFLmwmInTavdJERA== - dependencies: - tslib "^2.3.0"