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 50a9be3d59b..5fb4c121717 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -11,7 +11,13 @@ "eslint-plugin-jsonc", "eslint-plugin-rxjs", "eslint-plugin-simple-import-sort", - "eslint-plugin-import-newlines" + "eslint-plugin-import-newlines", + "eslint-plugin-jsonc", + "dspace-angular-ts", + "dspace-angular-html" + ], + "ignorePatterns": [ + "lint/test/fixture" ], "overrides": [ { @@ -21,7 +27,8 @@ "parserOptions": { "project": [ "./tsconfig.json", - "./cypress/tsconfig.json" + "./cypress/tsconfig.json", + "./lint/tsconfig.json" ], "createDefaultProgram": true }, @@ -38,7 +45,10 @@ "error", 2, { - "SwitchCase": 1 + "SwitchCase": 1, + "ignoredNodes": [ + "ClassBody.body > PropertyDefinition[decorators.length > 0] > .key" + ] } ], "max-classes-per-file": [ @@ -155,6 +165,7 @@ "@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", @@ -212,6 +223,15 @@ "@typescript-eslint/no-unsafe-return": "off", "@typescript-eslint/restrict-template-expressions": "off", "@typescript-eslint/require-await": "off", + "@typescript-eslint/no-base-to-string": [ + "error", + { + "ignoredTypeNames": [ + "ResourceType", + "Error" + ] + } + ], "deprecation/deprecation": "warn", @@ -238,7 +258,12 @@ "method" ], - "rxjs/no-nested-subscribe": "off" // todo: go over _all_ cases + "rxjs/no-nested-subscribe": "off", // todo: go over _all_ cases + + // Custom DSpace Angular rules + "dspace-angular-ts/themed-component-classes": "error", + "dspace-angular-ts/themed-component-selectors": "error", + "dspace-angular-ts/themed-component-usages": "error" } }, { @@ -253,7 +278,10 @@ "createDefaultProgram": true }, "rules": { - "prefer-const": "off" + "prefer-const": "off", + + // Custom DSpace Angular rules + "dspace-angular-ts/themed-component-usages": "error" } }, { @@ -262,7 +290,11 @@ ], "extends": [ "plugin:@angular-eslint/template/recommended" - ] + ], + "rules": { + // Custom DSpace Angular rules + "dspace-angular-html/themed-component-usages": "error" + } }, { "files": [ diff --git a/.gitattributes b/.gitattributes index 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/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 f6ffa5e004b..ab03cb38850 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,7 +7,8 @@ name: Build on: [push, pull_request] permissions: - contents: read # to fetch code (actions/checkout) + contents: read # to fetch code (actions/checkout) + packages: read # to fetch private images from GitHub Container Registry (GHCR) jobs: tests: @@ -35,6 +36,9 @@ jobs: NODE_OPTIONS: '--max-old-space-size=4096' # Project name to use when running "docker compose" prior to e2e tests COMPOSE_PROJECT_NAME: 'ci' + # Docker Registry to use for Docker compose scripts below. + # We use GitHub's Container Registry to avoid aggressive rate limits at DockerHub. + DOCKER_REGISTRY: ghcr.io strategy: # Create a matrix of Node versions to test against (in parallel) matrix: @@ -69,33 +73,39 @@ 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 + # 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) @@ -108,6 +118,14 @@ jobs: path: 'coverage/dspace-angular/lcov.info' retention-days: 14 + # Login to our Docker registry, so that we can access private Docker images using "docker compose" below. + - name: Login to ${{ env.DOCKER_REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.DOCKER_REGISTRY }} + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + # Using "docker compose" start backend using CI configuration # and load assetstore from a cached copy - name: Start DSpace REST Backend via Docker (for e2e tests) @@ -125,7 +143,7 @@ jobs: # Run tests in Chrome, headless mode (default) browser: chrome # Start app before running tests (will be stopped automatically after tests finish) - start: yarn run serve:ssr + start: npm run serve:ssr # Wait for backend & frontend to be available # NOTE: We use the 'sites' REST endpoint to also ensure the database is ready wait-on: http://127.0.0.1:8080/server/api/core/sites, http://127.0.0.1:4000 @@ -161,7 +179,7 @@ jobs: # Start up the app with SSR enabled (run in background) - name: Start app in SSR (server-side rendering) mode run: | - nohup yarn run serve:ssr & + nohup npm run serve:ssr & printf 'Waiting for app to start' until curl --output /dev/null --silent --head --fail http://127.0.0.1:4000/home; do printf '.' diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index d0b4cd0939f..bae8c013005 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -16,7 +16,8 @@ on: pull_request: permissions: - contents: read # to fetch code (actions/checkout) + contents: read # to fetch code (actions/checkout) + packages: write # to write images to GitHub Container Registry (GHCR) jobs: ############################################################# diff --git a/.gitignore b/.gitignore index ce44f6b3fbe..7af424c50f3 100644 --- a/.gitignore +++ b/.gitignore @@ -28,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..a2a78413961 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ # This image will be published as dspace/dspace-angular # See https://github.com/DSpace/dspace-angular/tree/main/docker for usage details -FROM node:18-alpine +FROM docker.io/node:18-alpine # Ensure Python and other build tools are available # These are needed to install some node modules, especially on linux/arm64 @@ -11,9 +11,7 @@ WORKDIR /app ADD . /app/ EXPOSE 4000 -# We run yarn install with an increased network timeout (5min) to avoid "ESOCKETTIMEDOUT" errors from hub.docker.com -# See, for example https://github.com/yarnpkg/yarn/issues/5540 -RUN yarn install --network-timeout 300000 +RUN npm install # When running in dev mode, 4GB of memory is required to build & launch the app. # This default setting can be overridden as needed in your shell, via an env file or in docker-compose. @@ -24,5 +22,5 @@ ENV NODE_OPTIONS="--max_old_space_size=4096" # Listen / accept connections from all IP addresses. # NOTE: At this time it is only possible to run Docker container in Production mode # if you have a public URL. See https://github.com/DSpace/dspace-angular/issues/1485 -ENV NODE_ENV development -CMD yarn serve --host 0.0.0.0 +ENV NODE_ENV=development +CMD npm run serve -- --host 0.0.0.0 diff --git a/Dockerfile.dist b/Dockerfile.dist index e4b467ae26c..9bb7b1519ba 100644 --- a/Dockerfile.dist +++ b/Dockerfile.dist @@ -4,18 +4,18 @@ # Test build: # docker build -f Dockerfile.dist -t dspace/dspace-angular:latest-dist . -FROM node:18-alpine as build +FROM docker.io/node:18-alpine AS build # Ensure Python and other build tools are available # These are needed to install some node modules, especially on linux/arm64 RUN apk add --update python3 make g++ && rm -rf /var/cache/apk/* WORKDIR /app -COPY package.json yarn.lock ./ -RUN yarn install --network-timeout 300000 +COPY package.json package-lock.json ./ +RUN npm install ADD . /app/ -RUN yarn build:prod +RUN npm run build:prod FROM node:18-alpine RUN npm install --global pm2 @@ -26,6 +26,6 @@ COPY --chown=node:node docker/dspace-ui.json /app/dspace-ui.json WORKDIR /app USER node -ENV NODE_ENV production +ENV NODE_ENV=production EXPOSE 4000 CMD pm2-runtime start dspace-ui.json --json diff --git a/README.md b/README.md index ebc24f8b918..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 @@ -202,7 +202,7 @@ import { environment } from '../environment.ts'; Running the app --------------- -After you have installed all dependencies you can now run the app. Run `yarn run start:dev` to start a local server which will watch for changes, rebuild the code, and reload the server for you. You can visit it at `http://localhost:4000`. +After you have installed all dependencies you can now run the app. Run `npm run start:dev` to start a local server which will watch for changes, rebuild the code, and reload the server for you. You can visit it at `http://localhost:4000`. ### Running in production mode @@ -211,20 +211,20 @@ When building for production we're using Ahead of Time (AoT) compilation. With A To build the app for production and start the server (in one command) run: ```bash -yarn start +npm start ``` This will run the application in an instance of the Express server, which is included. If you only want to build for production, without starting, run: ```bash -yarn run build:prod +npm run build:prod ``` This will build the application and put the result in the `dist` folder. You can copy this folder to wherever you need it for your application server. If you will be using the built-in Express server, you'll also need a copy of the `node_modules` folder tucked inside your copy of `dist`. After building the app for production, it can be started by running: ```bash -yarn run serve:ssr +npm run serve:ssr ``` ### Running the application with Docker @@ -238,14 +238,14 @@ Cleaning -------- ```bash -# clean everything, including node_modules. You'll need to run yarn install again afterwards. -yarn run clean +# clean everything, including node_modules. You'll need to run npm install again afterwards. +npm run clean # clean files generated by the production build (.ngfactory files, css files, etc) -yarn run clean:prod +npm run clean:prod # cleans the distribution directory -yarn run clean:dist +npm run clean:dist ``` @@ -259,9 +259,9 @@ If you would like to contribute by testing a Pull Request (PR), here's how to do 1. Pull down the branch that the Pull Request was built from. Easy instructions for doing so can be found on the Pull Request itself. * Next to the "Merge" button, you'll see a link that says "command line instructions". * Click it, and follow "Step 1" of those instructions to checkout the pull down the PR branch. -2. `yarn run clean` (This resets your local dependencies to ensure you are up-to-date with this PR) -3. `yarn install` (Updates your local dependencies to those in the PR) -4. `yarn start` (Rebuilds the project, and deploys to localhost:4000, by default) +2. `npm run clean` (This resets your local dependencies to ensure you are up-to-date with this PR) +3. `npm install` (Updates your local dependencies to those in the PR) +4. `npm start` (Rebuilds the project, and deploys to localhost:4000, by default) 5. At this point, the code from the PR will be deployed to http://localhost:4000. Test it out, and ensure that it does what is described in the PR (or fixes the bug described in the ticket linked to the PR). Once you have tested the Pull Request, please add a comment and/or approval to the PR to let us know whether you found it to be successful (or not). Thanks! @@ -271,13 +271,13 @@ Once you have tested the Pull Request, please add a comment and/or approval to t Unit tests use the [Jasmine test framework](https://jasmine.github.io/), and are run via [Karma](https://karma-runner.github.io/). -You can find the Karma configuration file at the same level of this README file:`./karma.conf.js` If you are going to use a remote test environment you need to edit the `./karma.conf.js`. Follow the instructions you will find inside it. To executing tests whenever any file changes you can modify the 'autoWatch' option to 'true' and 'singleRun' option to 'false'. A coverage report is also available at: http://localhost:9876/ after you run: `yarn run coverage`. +You can find the Karma configuration file at the same level of this README file:`./karma.conf.js` If you are going to use a remote test environment you need to edit the `./karma.conf.js`. Follow the instructions you will find inside it. To executing tests whenever any file changes you can modify the 'autoWatch' option to 'true' and 'singleRun' option to 'false'. A coverage report is also available at: http://localhost:9876/ after you run: `npm run coverage`. The default browser is Google Chrome. Place your tests in the same location of the application source code files that they test, e.g. ending with `*.component.spec.ts` -and run: `yarn test` +and run: `npm test` If you run into odd test errors, see the Angular guide to debugging tests: https://angular.io/guide/test-debugging @@ -330,9 +330,9 @@ All E2E tests must be created under the `./cypress/integration/` folder, and mus * In the [Cypress Test Runner](https://docs.cypress.io/guides/core-concepts/test-runner), you'll Cypress automatically visit the page. This first test will succeed, as all you are doing is making sure the _page exists_. * From here, you can use the [Selector Playground](https://docs.cypress.io/guides/core-concepts/test-runner#Selector-Playground) in the Cypress Test Runner window to determine how to tell Cypress to interact with a specific HTML element on that page. * Most commands start by telling Cypress to [get()](https://docs.cypress.io/api/commands/get) a specific element, using a CSS or jQuery style selector - * It's generally best not to rely on attributes like `class` and `id` in tests, as those are likely to change later on. Instead, you can add a `data-test` attribute to makes it clear that it's required for a test. + * It's generally best not to rely on attributes like `class` and `id` in tests, as those are likely to change later on. Instead, you can add a `data-test` attribute to makes it clear that it's required for a test. * Cypress can then do actions like [click()](https://docs.cypress.io/api/commands/click) an element, or [type()](https://docs.cypress.io/api/commands/type) text in an input field, etc. - * When running with server-side rendering enabled, the client first receives HTML without the JS; only once the page is rendered client-side do some elements (e.g. a button that toggles a Bootstrap dropdown) become fully interactive. This can trip up Cypress in some cases as it may try to `click` or `type` in an element that's not fully loaded yet, causing tests to fail. + * When running with server-side rendering enabled, the client first receives HTML without the JS; only once the page is rendered client-side do some elements (e.g. a button that toggles a Bootstrap dropdown) become fully interactive. This can trip up Cypress in some cases as it may try to `click` or `type` in an element that's not fully loaded yet, causing tests to fail. * To work around this issue, define the attributes you use for Cypress selectors as `[attr.data-test]="'button' | ngBrowserOnly"`. This will only show the attribute in CSR HTML, forcing Cypress to wait until CSR is complete before interacting with the element. * Cypress can also validate that something occurs, using [should()](https://docs.cypress.io/api/commands/should) assertions. * Any time you save your test file, the Cypress Test Runner will reload & rerun it. This allows you can see your results quickly as you write the tests & correct any broken tests rapidly. @@ -357,14 +357,14 @@ Some UI specific configuration documentation is also found in the [`./docs`](doc To build the code documentation we use [TYPEDOC](http://typedoc.org). TYPEDOC is a documentation generator for TypeScript projects. It extracts information from properly formatted comments that can be written within the code files. Follow the instructions [here](http://typedoc.org/guides/doccomments/) to know how to make those comments. -Run:`yarn run docs` to produce the documentation that will be available in the 'doc' folder. +Run:`npm run docs` to produce the documentation that will be available in the 'doc' folder. Other commands -------------- There are many more commands in the `scripts` section of `package.json`. Most of these are executed by one of the commands mentioned above. -A command with a name that starts with `pre` or `post` will be executed automatically before or after the script with the matching name. e.g. if you type `yarn run start` the `prestart` script will run first, then the `start` script will trigger. +A command with a name that starts with `pre` or `post` will be executed automatically before or after the script with the matching name. e.g. if you type `npm run start` the `prestart` script will run first, then the `start` script will trigger. Recommended Editors/IDEs ------------------------ @@ -456,6 +456,7 @@ dspace-angular ├── LICENSES_THIRD_PARTY * ├── nodemon.json * Nodemon (https://nodemon.io/) configuration ├── package.json * This file describes the npm package for this project, its dependencies, scripts, etc. +├── package-lock.json * npm lockfile (https://docs.npmjs.com/cli/v10/configuring-npm/package-lock-json) ├── postcss.config.js * PostCSS (http://postcss.org/) configuration ├── README.md * This document ├── SECURITY.md * @@ -466,30 +467,29 @@ dspace-angular ├── tsconfig.spec.json * TypeScript config for tests ├── tsconfig.ts-node.json * TypeScript config for using ts-node directly ├── tslint.json * TSLint (https://palantir.github.io/tslint/) configuration -├── typedoc.json * TYPEDOC configuration -└── yarn.lock * Yarn lockfile (https://yarnpkg.com/en/docs/yarn-lock) +└── typedoc.json * TYPEDOC configuration ``` -Managing Dependencies (via yarn) +Managing Dependencies (via npm) ------------- -This project makes use of [`yarn`](https://yarnpkg.com/en/) to ensure that the exact same dependency versions are used every time you install it. +This project makes use of [`npm`](https://docs.npmjs.com/about-npm) to ensure that the exact same dependency versions are used every time you install it. -* `yarn` creates a [`yarn.lock`](https://yarnpkg.com/en/docs/yarn-lock) to track those versions. That file is updated automatically by whenever dependencies are added/updated/removed via yarn. -* **Adding new dependencies**: To install/add a new dependency (third party library), use [`yarn add`](https://yarnpkg.com/en/docs/cli/add). For example: `yarn add some-lib`. - * If you are adding a new build tool dependency (to `devDependencies`), use `yarn add some-lib --dev` -* **Upgrading existing dependencies**: To upgrade existing dependencies, you can use [`yarn upgrade`](https://yarnpkg.com/en/docs/cli/upgrade). For example: `yarn upgrade some-lib` or `yarn upgrade some-lib@version` -* **Removing dependencies**: If a dependency is no longer needed, or replaced, use [`yarn remove`](https://yarnpkg.com/en/docs/cli/remove) to remove it. +* `npm` creates a [`package-lock.json`](https://docs.npmjs.com/cli/v10/configuring-npm/package-lock-json) to track those versions. That file is updated automatically by whenever dependencies are added/updated/removed via npm. +* **Adding new dependencies**: To install/add a new dependency (third party library), use [`npm install`](https://docs.npmjs.com/cli/v10/commands/npm-install). For example: `npm install some-lib`. + * If you are adding a new build tool dependency (to `devDependencies`), use `npm install some-lib --save--dev` +* **Upgrading existing dependencies**: To upgrade existing dependencies, you can use [`npm update`](https://docs.npmjs.com/cli/v10/commands/npm-update). For example: `npm update some-lib` or `npm update some-lib@version` +* **Removing dependencies**: If a dependency is no longer needed, or replaced, use [`npm uninstall`](https://docs.npmjs.com/cli/v10/commands/npm-uninstall) to remove it. -As you can see above, using `yarn` commandline tools means that you should never need to modify the `package.json` manually. *We recommend always using `yarn` to keep dependencies updated / in sync.* +As you can see above, using `npm` commandline tools means that you should never need to modify the `package.json` manually. *We recommend always using `npm` to keep dependencies updated / in sync.* ### Adding Typings for libraries -If the library does not include typings, you can install them using yarn: +If the library does not include typings, you can install them using npm: ```bash -yarn add d3 -yarn add @types/d3 --dev +npm install d3 +npm install @types/d3 --save-dev ``` If the library doesn't have typings available at `@types/`, you can still use it by manually adding typings for it: @@ -527,13 +527,13 @@ Frequently asked questions - What are the naming conventions for Angular? - See [the official angular style guide](https://angular.io/styleguide) - Why is the size of my app larger in development? - - The production build uses a whole host of techniques (ahead-of-time compilation, rollup to remove unreachable code, minification, etc.) to reduce the size, that aren't used during development in the intrest of build speed. -- node-pre-gyp ERR in yarn install (Windows) + - The production build uses a whole host of techniques (ahead-of-time compilation, rollup to remove unreachable code, minification, etc.) to reduce the size, that aren't used during development in the interest of build speed. +- node-pre-gyp ERR in npm install (Windows) - install Python x86 version between 2.5 and 3.0 on windows. See [this issue](https://github.com/AngularClass/angular2-webpack-starter/issues/626) -- How do I handle merge conflicts in yarn.lock? - - first check out the yarn.lock file from the branch you're merging in to yours: e.g. `git checkout --theirs yarn.lock` - - now run `yarn install` again. Yarn will create a new lockfile that contains both sets of changes. - - then run `git add yarn.lock` to stage the lockfile for commit +- How do I handle merge conflicts in package-lock.json? + - first check out the package-lock.json file from the branch you're merging in to yours: e.g. `git checkout --theirs package-lock.json` + - now run `npm install` again. NPM will create a new lockfile that contains both sets of changes. + - then run `git add package-lock.json` to stage the lockfile for commit - and `git commit` to conclude the merge Getting Help diff --git a/angular.json b/angular.json index 98463fa732e..02fd69b1e1b 100644 --- a/angular.json +++ b/angular.json @@ -30,7 +30,6 @@ "lodash", "jwt-decode", "uuid", - "webfontloader", "zone.js" ], "outputPath": "dist/browser", @@ -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 82c061dab22..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,12 @@ ui: # Trust X-FORWARDED-* headers from proxies (default = true) useProxies: true -universal: - # Whether to inline "critical" styles into the server-side rendered HTML. - # Determining which styles are critical is a relatively expensive operation; - # this option can be disabled to boost server performance at the expense of - # loading smoothness. - inlineCriticalCss: 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 @@ -59,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. @@ -202,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 @@ -211,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 @@ -244,8 +262,8 @@ languages: - code: sr-lat label: Srpski (lat) active: true - - code: fi - label: Suomi + - code: sr-cyr + label: Српски active: true - code: sv label: Svenska @@ -253,27 +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: sr-cyr - label: Српски - active: true - code: uk label: Yкраї́нська active: true + - code: vi + label: Tiếng Việt + active: true # Browse-By Pages @@ -500,6 +503,16 @@ notifyMetrics: description: 'admin-notify-dashboard.NOTIFY.outgoing.delivered.description' - - - +# Live Region configuration +# Live Region as defined by w3c, https://www.w3.org/TR/wai-aria-1.1/#terms: +# Live regions are perceivable regions of a web page that are typically updated as a +# result of an external event when user focus may be elsewhere. +# +# The DSpace live region is a component present at the bottom of all pages that is invisible by default, but is useful +# for screen readers. Any message pushed to the live region will be announced by the screen reader. These messages +# usually contain information about changes on the page that might not be in focus. +liveRegion: + # The duration after which messages disappear from the live region in milliseconds + messageTimeOutDurationMs: 30000 + # The visibility of the live region. Setting this to true is only useful for debugging purposes. + isVisible: false diff --git a/cypress.config.ts b/cypress.config.ts index 458b035a484..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', @@ -18,6 +19,7 @@ export default defineConfig({ // Admin account used for administrative tests DSPACE_TEST_ADMIN_USER: 'dspacedemo+admin@gmail.com', + DSPACE_TEST_ADMIN_USER_UUID: '335647b6-8a52-4ecb-a8c1-7ebabb199bda', DSPACE_TEST_ADMIN_PASSWORD: 'dspace', // Community/collection/publication used for view/edit tests DSPACE_TEST_COMMUNITY: '0958c910-2037-42a9-81c7-dca80e3892b4', @@ -33,6 +35,8 @@ export default defineConfig({ // Account used to test basic submission process DSPACE_TEST_SUBMIT_USER: 'dspacedemo+submit@gmail.com', DSPACE_TEST_SUBMIT_USER_PASSWORD: 'dspace', + // Administrator users group + DSPACE_ADMINISTRATOR_GROUP: 'e59f5659-bff9-451e-b28f-439e7bd467e4' }, e2e: { // Setup our plugins for e2e tests diff --git a/cypress/e2e/admin-add-new-modals.cy.ts b/cypress/e2e/admin-add-new-modals.cy.ts new file mode 100644 index 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 index be1c9d4ef27..318bf2b27e2 100644 --- a/cypress/e2e/admin-sidebar.cy.ts +++ b/cypress/e2e/admin-sidebar.cy.ts @@ -10,7 +10,7 @@ describe('Admin Sidebar', () => { it('should be pinnable and pass accessibility tests', () => { // Pin the sidebar open - cy.get('#sidebar-collapse-toggle').click(); + 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 }); 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/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/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/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/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 043d67dd2b9..1471e5ae6c5 100644 --- a/cypress/e2e/header.cy.ts +++ b/cypress/e2e/header.cy.ts @@ -10,4 +10,29 @@ describe('Header', () => { // Analyze for accessibility testA11y('ds-header'); }); + + it('should allow for changing language to German (for example)', () => { + cy.visit('/'); + + // Click the language switcher (globe) in header + cy.get('a[data-test="lang-switch"]').click(); + // Click on the "Deusch" language in dropdown + cy.get('#language-menu-list li').contains('Deutsch').click(); + + // HTML "lang" attribute should switch to "de" + cy.get('html').invoke('attr', 'lang').should('eq', 'de'); + + // Login menu should now be in German + cy.get('a[data-test="login-menu"]').contains('Anmelden'); + + // Change back to English from language switcher + cy.get('a[data-test="lang-switch"]').click(); + cy.get('#language-menu-list li').contains('English').click(); + + // HTML "lang" attribute should switch to "en" + cy.get('html').invoke('attr', 'lang').should('eq', 'en'); + + // Login menu should now be in English + cy.get('a[data-test="login-menu"]').contains('Log In'); + }); }); diff --git a/cypress/e2e/health-page.cy.ts b/cypress/e2e/health-page.cy.ts new file mode 100644 index 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 f9642c0c831..0e0fca3c5bb 100644 --- a/cypress/e2e/homepage-statistics.cy.ts +++ b/cypress/e2e/homepage-statistics.cy.ts @@ -17,7 +17,7 @@ describe('Site Statistics Page', () => { cy.visit('/statistics'); - // tag must be visable + // 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 diff --git a/cypress/e2e/item-edit.cy.ts b/cypress/e2e/item-edit.cy.ts index b13d5a46958..604aab9d042 100644 --- a/cypress/e2e/item-edit.cy.ts +++ b/cypress/e2e/item-edit.cy.ts @@ -15,9 +15,17 @@ 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'); }); @@ -28,6 +36,9 @@ 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'); @@ -41,6 +52,9 @@ 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'); @@ -65,6 +79,9 @@ 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'); @@ -78,6 +95,9 @@ 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'); @@ -91,6 +111,9 @@ 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'); @@ -104,6 +127,9 @@ 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'); @@ -117,6 +143,9 @@ 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'); 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 190f3ff9271..80d36a03099 100644 --- a/cypress/e2e/login-modal.cy.ts +++ b/cypress/e2e/login-modal.cy.ts @@ -3,31 +3,31 @@ import { testA11y } from 'cypress/support/utils'; const page = { openLoginMenu() { // Click the "Log In" dropdown menu in header - cy.get('ds-themed-header [data-test="login-menu"]').click(); + cy.get('[data-test="login-menu"]').click(); }, openUserMenu() { // Once logged in, click the User menu in header - cy.get('ds-themed-header [data-test="user-menu"]').click(); + cy.get('[data-test="user-menu"]').click(); }, submitLoginAndPasswordByPressingButton(email, password) { // Enter email - cy.get('ds-themed-header [data-test="email"]').type(email); + cy.get('[data-test="email"]').type(email); // Enter password - cy.get('ds-themed-header [data-test="password"]').type(password); + cy.get('[data-test="password"]').type(password); // Click login button - cy.get('ds-themed-header [data-test="login-button"]').click(); + cy.get('[data-test="login-button"]').click(); }, submitLoginAndPasswordByPressingEnter(email, password) { // In opened Login modal, fill out email & password, then click Enter - cy.get('ds-themed-header [data-test="email"]').type(email); - cy.get('ds-themed-header [data-test="password"]').type(password); - cy.get('ds-themed-header [data-test="password"]').type('{enter}'); + 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('ds-themed-header [data-test="logout-button"]').click(); + 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'); @@ -67,7 +67,7 @@ describe('Login Modal', () => { // Login, and the tag should no longer exist page.submitLoginAndPasswordByPressingEnter(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD')); - cy.get('.form-login').should('not.exist'); + cy.get('ds-log-in').should('not.exist'); // Verify we are still on homepage cy.url().should('include', '/home'); @@ -102,10 +102,10 @@ describe('Login Modal', () => { page.openLoginMenu(); // Registration link should be visible - cy.get('ds-themed-header [data-test="register"]').should('be.visible'); + cy.get('ds-header [data-test="register"]').should('be.visible'); // Click registration link & you should go to registration page - cy.get('ds-themed-header [data-test="register"]').click(); + cy.get('ds-header [data-test="register"]').click(); cy.location('pathname').should('eq', '/register'); cy.get('ds-register-email').should('exist'); @@ -119,10 +119,10 @@ describe('Login Modal', () => { page.openLoginMenu(); // Forgot password link should be visible - cy.get('ds-themed-header [data-test="forgot"]').should('be.visible'); + cy.get('ds-header [data-test="forgot"]').should('be.visible'); // Click link & you should go to Forgot Password page - cy.get('ds-themed-header [data-test="forgot"]').click(); + cy.get('ds-header [data-test="forgot"]').click(); cy.location('pathname').should('eq', '/forgot'); cy.get('ds-forgot-email').should('exist'); @@ -142,7 +142,7 @@ describe('Login Modal', () => { page.submitLoginAndPasswordByPressingButton(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD')); cy.get('ds-log-in').should('not.exist'); - // Open user menu, verify user menu accesibility + // 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/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 968ae2747b5..5f84a22df9c 100644 --- a/cypress/e2e/pagenotfound.cy.ts +++ b/cypress/e2e/pagenotfound.cy.ts @@ -1,7 +1,7 @@ import { testA11y } from 'cypress/support/utils'; describe('PageNotFound', () => { - it('should contain element ds-pagenotfound when navigating to page that doesnt 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'); 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 b1682199161..0613e5e7124 100644 --- a/cypress/e2e/search-navbar.cy.ts +++ b/cypress/e2e/search-navbar.cy.ts @@ -1,15 +1,15 @@ const page = { fillOutQueryInNavBar(query) { // Click the magnifying glass - cy.get('ds-themed-header [data-test="header-search-icon"]').click(); + cy.get('ds-header [data-test="header-search-icon"]').click(); // Fill out a query in input that appears - cy.get('ds-themed-header [data-test="header-search-box"]').type(query); + cy.get('ds-header [data-test="header-search-box"]').type(query); }, submitQueryByPressingEnter() { - cy.get('ds-themed-header [data-test="header-search-box"]').type('{enter}'); + cy.get('ds-header [data-test="header-search-box"]').type('{enter}'); }, submitQueryByPressingIcon() { - cy.get('ds-themed-header [data-test="header-search-icon"]').click(); + cy.get('ds-header [data-test="header-search-icon"]').click(); }, }; diff --git a/cypress/e2e/submission.cy.ts b/cypress/e2e/submission.cy.ts index 7123d841345..ebdabbde2e0 100644 --- a/cypress/e2e/submission.cy.ts +++ b/cypress/e2e/submission.cy.ts @@ -34,7 +34,7 @@ describe('New Submission page', () => { // Author & Subject fields have invalid "aria-multiline" attrs. // See https://github.com/DSpace/dspace-angular/issues/1272 'aria-allowed-attr': { enabled: false }, - // All panels are accordians & fail "aria-required-children" and "nested-interactive". + // 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 }, @@ -137,7 +137,7 @@ describe('New Submission page', () => { // 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', { + cy.get('div.ds-document-drop-zone').selectFile('src/assets/images/dspace-logo.svg', { action: 'drag-drop', }); @@ -192,7 +192,7 @@ describe('New Submission page', () => { testA11y('ds-submission-edit', { rules: { - // All panels are accordians & fail "aria-required-children" and "nested-interactive". + // 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 }, 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/support/commands.ts b/cypress/support/commands.ts index b3e3b9630bb..8cc2c5c721b 100644 --- a/cypress/support/commands.ts +++ b/cypress/support/commands.ts @@ -101,11 +101,11 @@ Cypress.Commands.add('login', login); */ function loginViaForm(email: string, password: string): void { // Enter email - cy.get('ds-log-in [data-test="email"]').type(email); + cy.get('[data-test="email"]').type(email); // Enter password - cy.get('ds-log-in [data-test="password"]').type(password); + cy.get('[data-test="password"]').type(password); // Click login button - cy.get('ds-log-in [data-test="login-button"]').click(); + cy.get('[data-test="login-button"]').click(); } // Add as a Cypress command (i.e. assign to 'cy.loginViaForm') Cypress.Commands.add('loginViaForm', loginViaForm); diff --git a/cypress/support/e2e.ts b/cypress/support/e2e.ts index 73d3c76a990..48985e79115 100644 --- a/cypress/support/e2e.ts +++ b/cypress/support/e2e.ts @@ -54,9 +54,9 @@ before(() => { // Runs once before the first test in each "block" beforeEach(() => { - // Pre-agree to all Klaro cookies by setting the klaro-anonymous cookie + // 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('klaro-anonymous', '{%22authentication%22:true%2C%22preferences%22:true%2C%22acknowledgement%22:true%2C%22google-analytics%22:true%2C%22google-recaptcha%22:true}'); + 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); diff --git a/docker/README.md b/docker/README.md index d0cee3f52a5..3dc5fd50550 100644 --- a/docker/README.md +++ b/docker/README.md @@ -20,7 +20,7 @@ the Docker compose scripts in this 'docker' folder. ### Dockerfile -This Dockerfile is used to build a *development* DSpace 7 Angular UI image, published as 'dspace/dspace-angular' +This Dockerfile is used to build a *development* DSpace Angular UI image, published as 'dspace/dspace-angular' ``` docker build -t dspace/dspace-angular:latest . @@ -46,11 +46,11 @@ A default/demo version of this image is built *automatically*. ## 'docker' directory - docker-compose.yml - - Starts DSpace Angular with Docker Compose from the current branch. This file assumes that a DSpace 7 REST instance will also be started in Docker. + - Starts DSpace Angular with Docker Compose from the current branch. This file assumes that a DSpace REST instance will also be started in Docker. - docker-compose-rest.yml - - Runs a published instance of the DSpace 7 REST API - persists data in Docker volumes + - Runs a published instance of the DSpace REST API - persists data in Docker volumes - docker-compose-ci.yml - - Runs a published instance of the DSpace 7 REST API for CI testing. The database is re-populated from a SQL dump on each startup. + - Runs a published instance of the DSpace REST API for CI testing. The database is re-populated from a SQL dump on each startup. - cli.yml - Docker compose file that provides a DSpace CLI container to work with a running DSpace REST container. - cli.assetstore.yml @@ -59,19 +59,19 @@ A default/demo version of this image is built *automatically*. ## To refresh / pull DSpace images from Dockerhub ``` -docker-compose -f docker/docker-compose.yml pull +docker compose -f docker/docker-compose.yml pull ``` ## To build DSpace images using code in your branch ``` -docker-compose -f docker/docker-compose.yml build +docker compose -f docker/docker-compose.yml build ``` ## To start DSpace (REST and Angular) from your branch This command provides a quick way to start both the frontend & backend from this single codebase ``` -docker-compose -p d7 -f docker/docker-compose.yml -f docker/docker-compose-rest.yml up -d +docker compose -p d8 -f docker/docker-compose.yml -f docker/docker-compose-rest.yml up -d ``` Keep in mind, you may also start the backend by cloning the 'DSpace/DSpace' GitHub repository separately. See the next section. @@ -86,14 +86,14 @@ _The system will be started in 2 steps. Each step shares the same docker network From 'DSpace/DSpace' clone (build first as needed): ``` -docker-compose -p d7 up -d +docker compose -p d8 up -d ``` NOTE: More detailed instructions on starting the backend via Docker can be found in the [Docker Compose instructions for the Backend](https://github.com/DSpace/DSpace/blob/main/dspace/src/main/docker-compose/README.md). From 'DSpace/dspace-angular' clone (build first as needed) ``` -docker-compose -p d7 -f docker/docker-compose.yml up -d +docker compose -p d8 -f docker/docker-compose.yml up -d ``` At this point, you should be able to access the UI from http://localhost:4000, @@ -105,21 +105,21 @@ This allows you to run the Angular UI in *production* mode, pointing it at the d (https://demo.dspace.org/server/ or https://sandbox.dspace.org/server/). ``` -docker-compose -f docker/docker-compose-dist.yml pull -docker-compose -f docker/docker-compose-dist.yml build -docker-compose -p d7 -f docker/docker-compose-dist.yml up -d +docker compose -f docker/docker-compose-dist.yml pull +docker compose -f docker/docker-compose-dist.yml build +docker compose -p d8 -f docker/docker-compose-dist.yml up -d ``` ## Ingest test data from AIPDIR Create an administrator ``` -docker-compose -p d7 -f docker/cli.yml run --rm dspace-cli create-administrator -e test@test.edu -f admin -l user -p admin -c en +docker compose -p d8 -f docker/cli.yml run --rm dspace-cli create-administrator -e test@test.edu -f admin -l user -p admin -c en ``` Load content from AIP files ``` -docker-compose -p d7 -f docker/cli.yml -f ./docker/cli.ingest.yml run --rm dspace-cli +docker compose -p d8 -f docker/cli.yml -f ./docker/cli.ingest.yml run --rm dspace-cli ``` ## Alternative Ingest - Use Entities dataset @@ -127,12 +127,12 @@ _Delete your docker volumes or use a unique project (-p) name_ Start DSpace with Database Content from a database dump ``` -docker-compose -p d7 -f docker/docker-compose.yml -f docker/docker-compose-rest.yml -f docker/db.entities.yml up -d +docker compose -p d8 -f docker/docker-compose.yml -f docker/docker-compose-rest.yml -f docker/db.entities.yml up -d ``` Load assetstore content and trigger a re-index of the repository ``` -docker-compose -p d7 -f docker/cli.yml -f docker/cli.assetstore.yml run --rm dspace-cli +docker compose -p d8 -f docker/cli.yml -f docker/cli.assetstore.yml run --rm dspace-cli ``` ## End to end testing of the REST API (runs in GitHub Actions CI). @@ -140,5 +140,5 @@ _In this instance, only the REST api runs in Docker using the Entities dataset. This command is only really useful for testing our Continuous Integration process. ``` -docker-compose -p d7ci -f docker/docker-compose-ci.yml up -d +docker compose -p d8ci -f docker/docker-compose-ci.yml up -d ``` diff --git a/docker/cli.assetstore.yml b/docker/cli.assetstore.yml index 31bc53f64d8..98f74148610 100644 --- a/docker/cli.assetstore.yml +++ b/docker/cli.assetstore.yml @@ -12,8 +12,6 @@ # https://github.com/DSpace/DSpace/blob/main/dspace/src/main/docker-compose/cli.assetstore.yml # # Therefore, it should be kept in sync with that file -version: "3.7" - services: dspace-cli: environment: diff --git a/docker/cli.ingest.yml b/docker/cli.ingest.yml index 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 cc266b186f9..bbb9bd5619a 100644 --- a/docker/cli.yml +++ b/docker/cli.yml @@ -12,7 +12,6 @@ # https://github.com/DSpace/DSpace/blob/main/docker-compose-cli.yml # # Therefore, it should be kept in sync with that file -version: "3.7" networks: # Default to using network named 'dspacenet' from docker-compose-rest.yml. # Its full name will be prepended with the project name (e.g. "-p d7" means it will be named "d7_dspacenet") @@ -22,7 +21,7 @@ networks: external: true services: dspace-cli: - image: "${DOCKER_OWNER:-dspace}/dspace-cli:${DSPACE_VER:-latest}" + image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace-cli:${DSPACE_VER:-latest}" container_name: dspace-cli environment: # Below syntax may look odd, but it is how to override dspace.cfg settings via env variables. diff --git a/docker/db.entities.yml b/docker/db.entities.yml index 6473bf2e385..c532d158579 100644 --- a/docker/db.entities.yml +++ b/docker/db.entities.yml @@ -12,11 +12,9 @@ # https://github.com/DSpace/DSpace/blob/main/dspace/src/main/docker-compose/db.entities.yml # # # Therefore, it should be kept in sync with that file -version: "3.7" - services: dspacedb: - image: dspace/dspace-postgres-pgcrypto:loadsql + image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace-postgres-pgcrypto:${DSPACE_VER:-latest}-loadsql" environment: # This LOADSQL should be kept in sync with the URL in DSpace/DSpace # This SQL is available from https://github.com/DSpace-Labs/AIP-Files/releases/tag/demo-entities-data @@ -29,23 +27,11 @@ services: # 3. (Custom for Entities) enable Entity-specific collection submission mappings in item-submission.xml # This 'sed' command inserts the sample configurations specific to the Entities data set, see: # https://github.com/DSpace/DSpace/blob/main/dspace/config/item-submission.xml#L36-L49 - # 4. Finally, start Tomcat + # 4. Finally, start DSpace entrypoint: - /bin/bash - '-c' - | while (! /dev/null 2>&1; do sleep 1; done; /dspace/bin/dspace database migrate ignored - sed -i '/name-map collection-handle="default".*/a \\n \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - \ - ' /dspace/config/item-submission.xml - catalina.sh run \ No newline at end of file + java -jar /dspace/webapps/server-boot.jar --dspace.dir=/dspace diff --git a/docker/docker-compose-ci.yml b/docker/docker-compose-ci.yml index 07993e20c62..98825605d35 100644 --- a/docker/docker-compose-ci.yml +++ b/docker/docker-compose-ci.yml @@ -10,7 +10,6 @@ # This is used by our GitHub CI at .github/workflows/build.yml # It is based heavily on the Backend's Docker Compose: # https://github.com/DSpace/DSpace/blob/main/docker-compose.yml -version: '3.7' networks: dspacenet: services: @@ -33,7 +32,8 @@ services: # Tell Statistics to commit all views immediately instead of waiting on Solr's autocommit. # This allows us to generate statistics in e2e tests so that statistics pages can be tested thoroughly. solr__D__statistics__P__autoCommit: 'false' - image: "${DOCKER_OWNER:-dspace}/dspace:${DSPACE_VER:-latest-test}" + LOGGING_CONFIG: /dspace/config/log4j2-container.xml + image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace:${DSPACE_VER:-latest-test}" depends_on: - dspacedb networks: @@ -48,27 +48,31 @@ services: # Ensure that the database is ready BEFORE starting tomcat # 1. While a TCP connection to dspacedb port 5432 is not available, continue to sleep # 2. Then, run database migration to init database tables (including any out-of-order ignored migrations, if any) - # 3. Finally, start Tomcat + # 3. Finally, start DSpace entrypoint: - /bin/bash - '-c' - | while (! /dev/null 2>&1; do sleep 1; done; /dspace/bin/dspace database migrate ignored - catalina.sh run + java -jar /dspace/webapps/server-boot.jar --dspace.dir=/dspace # DSpace database container # NOTE: This is customized to use our loadsql image, so that we are using a database with existing test data dspacedb: container_name: dspacedb + image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace-postgres-pgcrypto:${DSPACE_VER:-latest}-loadsql" environment: # This LOADSQL should be kept in sync with the LOADSQL in # https://github.com/DSpace/DSpace/blob/main/dspace/src/main/docker-compose/db.entities.yml # This SQL is available from https://github.com/DSpace-Labs/AIP-Files/releases/tag/demo-entities-data LOADSQL: https://github.com/DSpace-Labs/AIP-Files/releases/download/demo-entities-data/dspace7-entities-data.sql PGDATA: /pgdata - image: dspace/dspace-postgres-pgcrypto:loadsql + POSTGRES_PASSWORD: dspace networks: - dspacenet + ports: + - published: 5432 + target: 5432 stdin_open: true tty: true volumes: @@ -77,7 +81,7 @@ services: # DSpace Solr container dspacesolr: container_name: dspacesolr - image: "${DOCKER_OWNER:-dspace}/dspace-solr:${DSPACE_VER:-latest}" + image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace-solr:${DSPACE_VER:-latest}" networks: - dspacenet ports: @@ -105,6 +109,8 @@ services: cp -r /opt/solr/server/solr/configsets/statistics/* statistics precreate-core qaevent /opt/solr/server/solr/configsets/qaevent cp -r /opt/solr/server/solr/configsets/qaevent/* qaevent + precreate-core suggestion /opt/solr/server/solr/configsets/suggestion + cp -r /opt/solr/server/solr/configsets/suggestion/* suggestion exec solr -f volumes: assetstore: diff --git a/docker/docker-compose-dist.yml b/docker/docker-compose-dist.yml index 38278085cd0..5b06faa6a21 100644 --- a/docker/docker-compose-dist.yml +++ b/docker/docker-compose-dist.yml @@ -8,7 +8,6 @@ # Docker Compose for running the DSpace Angular UI dist build # for previewing with the DSpace Demo site backend -version: '3.7' networks: dspacenet: services: @@ -27,7 +26,7 @@ services: DSPACE_REST_HOST: sandbox.dspace.org DSPACE_REST_PORT: 443 DSPACE_REST_NAMESPACE: /server - image: dspace/dspace-angular:${DSPACE_VER:-latest}-dist + image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace-angular:${DSPACE_VER:-latest}-dist" build: context: .. dockerfile: Dockerfile.dist diff --git a/docker/docker-compose-rest.yml b/docker/docker-compose-rest.yml index e1577ec8375..e650f09eb50 100644 --- a/docker/docker-compose-rest.yml +++ b/docker/docker-compose-rest.yml @@ -10,7 +10,6 @@ # This is based heavily on the docker-compose.yml that is available in the DSpace/DSpace # (Backend) at: # https://github.com/DSpace/DSpace/blob/main/docker-compose.yml -version: '3.7' networks: dspacenet: ipam: @@ -29,8 +28,9 @@ services: # __D__ => "-" (e.g. google__D__metadata => google-metadata) # dspace.dir, dspace.server.url, dspace.ui.url and dspace.name dspace__P__dir: /dspace - dspace__P__server__P__url: http://localhost:8080/server - dspace__P__ui__P__url: http://localhost:4000 + # Uncomment to set a non-default value for dspace.server.url or dspace.ui.url + # dspace__P__server__P__url: http://localhost:8080/server + # dspace__P__ui__P__url: http://localhost:4000 dspace__P__name: 'DSpace Started with Docker Compose' # db.url: Ensure we are using the 'dspacedb' image for our database db__P__url: 'jdbc:postgresql://dspacedb:5432/dspace' @@ -39,7 +39,8 @@ services: # proxies.trusted.ipranges: This setting is required for a REST API running in Docker to trust requests # from the host machine. This IP range MUST correspond to the 'dspacenet' subnet defined above. proxies__P__trusted__P__ipranges: '172.23.0' - image: "${DOCKER_OWNER:-dspace}/dspace:${DSPACE_VER:-latest-test}" + LOGGING_CONFIG: /dspace/config/log4j2-container.xml + image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace:${DSPACE_VER:-latest-test}" depends_on: - dspacedb networks: @@ -50,24 +51,27 @@ services: stdin_open: true tty: true volumes: + # Keep DSpace assetstore directory between reboots - assetstore:/dspace/assetstore # Ensure that the database is ready BEFORE starting tomcat # 1. While a TCP connection to dspacedb port 5432 is not available, continue to sleep # 2. Then, run database migration to init database tables - # 3. Finally, start Tomcat + # 3. Finally, start DSpace entrypoint: - /bin/bash - '-c' - | while (! /dev/null 2>&1; do sleep 1; done; /dspace/bin/dspace database migrate - catalina.sh run + java -jar /dspace/webapps/server-boot.jar --dspace.dir=/dspace # DSpace database container dspacedb: container_name: dspacedb + # Uses a custom Postgres image with pgcrypto installed + image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace-postgres-pgcrypto:${DSPACE_VER:-latest}" environment: PGDATA: /pgdata - image: "${DOCKER_OWNER:-dspace}/dspace-postgres-pgcrypto:${DSPACE_VER:-latest}" + POSTGRES_PASSWORD: dspace networks: - dspacenet ports: @@ -81,7 +85,7 @@ services: # DSpace Solr container dspacesolr: container_name: dspacesolr - image: "${DOCKER_OWNER:-dspace}/dspace-solr:${DSPACE_VER:-latest}" + image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace-solr:${DSPACE_VER:-latest}" networks: - dspacenet ports: @@ -97,7 +101,7 @@ services: # * First, run precreate-core to create the core (if it doesn't yet exist). If exists already, this is a no-op # * Second, copy configsets to this core: # Updates to Solr configs require the container to be rebuilt/restarted: - # `docker-compose -p d7 -f docker/docker-compose.yml -f docker/docker-compose-rest.yml up -d --build dspacesolr` + # `docker compose -p d7 -f docker/docker-compose.yml -f docker/docker-compose-rest.yml up -d --build dspacesolr` entrypoint: - /bin/bash - '-c' @@ -113,6 +117,8 @@ services: cp -r /opt/solr/server/solr/configsets/statistics/* statistics precreate-core qaevent /opt/solr/server/solr/configsets/qaevent cp -r /opt/solr/server/solr/configsets/qaevent/* qaevent + precreate-core suggestion /opt/solr/server/solr/configsets/suggestion + cp -r /opt/solr/server/solr/configsets/suggestion/* suggestion exec solr -f volumes: assetstore: diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 1071b8d6ce2..b13ab505f15 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -9,7 +9,6 @@ # Docker Compose for running the DSpace Angular UI for testing/development # Requires also running a REST API backend (either locally or remotely), # for example via 'docker-compose-rest.yml' -version: '3.7' networks: dspacenet: services: @@ -24,7 +23,7 @@ services: DSPACE_REST_HOST: localhost DSPACE_REST_PORT: 8080 DSPACE_REST_NAMESPACE: /server - image: dspace/dspace-angular:${DSPACE_VER:-latest} + image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace-angular:${DSPACE_VER:-latest}" build: context: .. dockerfile: Dockerfile diff --git a/docs/Configuration.md b/docs/Configuration.md index 01fd83c94d1..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): 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/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/themes/custom/app/shared/comcol-page-browse-by/comcol-page-browse-by.component.html b/lint/test/fixture/src/test.ts similarity index 100% rename from src/themes/custom/app/shared/comcol-page-browse-by/comcol-page-browse-by.component.html 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..5d9f49d1f6b --- /dev/null +++ b/package-lock.json @@ -0,0 +1,23310 @@ +{ + "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.39.0", + "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.16.0", + "cypress-axe": "^1.5.0", + "deep-freeze": "0.0.1", + "eslint": "^8.39.0", + "eslint-plugin-deprecation": "^1.4.1", + "eslint-plugin-dspace-angular-html": "file:./lint/dist/src/rules/html", + "eslint-plugin-dspace-angular-ts": "file:./lint/dist/src/rules/ts", + "eslint-plugin-import": "^2.31.0", + "eslint-plugin-import-newlines": "^1.3.1", + "eslint-plugin-jsdoc": "^45.0.0", + "eslint-plugin-jsonc": "^2.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.6", + "resolved": "https://registry.npmjs.org/@cypress/request/-/request-3.0.6.tgz", + "integrity": "sha512-fi0eVdCOtKu5Ed6+E8mYxUF6ZTFJDZvHogCBelM0xVXmrDEkyM22gRArQzq1YcHPm1V47Vf/iAD+WgVdUlJCGg==", + "dev": true, + "dependencies": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~4.0.0", + "http-signature": "~1.4.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "performance-now": "^2.1.0", + "qs": "6.13.0", + "safe-buffer": "^5.1.2", + "tough-cookie": "^5.0.0", + "tunnel-agent": "^0.6.0", + "uuid": "^8.3.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@cypress/schematic": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@cypress/schematic/-/schematic-1.7.0.tgz", + "integrity": "sha512-CouQrVlZ+uHVVBQtmNoMYU9LyoSAmQTOLDpVjrdTdMPpJH1mWnHCL5OCMt+FZLR+43KRiWEvDUjNqSza11oGsQ==", + "dev": true, + "dependencies": { + "@angular-devkit/architect": "^0.1202.10", + "@angular-devkit/core": "^12.2.17", + "@angular-devkit/schematics": "^12.2.17", + "@schematics/angular": "^12.2.17", + "jsonc-parser": "^3.0.0", + "rxjs": "~6.6.0" + }, + "peerDependencies": { + "@angular/cli": ">=12", + "@angular/core": ">=12" + } + }, + "node_modules/@cypress/schematic/node_modules/@angular-devkit/architect": { + "version": "0.1202.18", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1202.18.tgz", + "integrity": "sha512-C4ASKe+xBjl91MJyHDLt3z7ICPF9FU6B0CeJ1phwrlSHK9lmFG99WGxEj/Tc82+vHyPhajqS5XJ38KyVAPBGzA==", + "dev": true, + "dependencies": { + "@angular-devkit/core": "12.2.18", + "rxjs": "6.6.7" + }, + "engines": { + "node": "^12.14.1 || >=14.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@cypress/schematic/node_modules/@angular-devkit/core": { + "version": "12.2.18", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-12.2.18.tgz", + "integrity": "sha512-GDLHGe9HEY5SRS+NrKr14C8aHsRCiBFkBFSSbeohgLgcgSXzZHFoU84nDWrl3KZNP8oqcUSv5lHu6dLcf2fnww==", + "dev": true, + "dependencies": { + "ajv": "8.6.2", + "ajv-formats": "2.1.0", + "fast-json-stable-stringify": "2.1.0", + "magic-string": "0.25.7", + "rxjs": "6.6.7", + "source-map": "0.7.3" + }, + "engines": { + "node": "^12.14.1 || >=14.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@cypress/schematic/node_modules/@angular-devkit/schematics": { + "version": "12.2.18", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-12.2.18.tgz", + "integrity": "sha512-bZ9NS5PgoVfetRC6WeQBHCY5FqPZ9y2TKHUo12sOB2YSL3tgWgh1oXyP8PtX34gasqsLjNULxEQsAQYEsiX/qQ==", + "dev": true, + "dependencies": { + "@angular-devkit/core": "12.2.18", + "ora": "5.4.1", + "rxjs": "6.6.7" + }, + "engines": { + "node": "^12.14.1 || >=14.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@cypress/schematic/node_modules/@schematics/angular": { + "version": "12.2.18", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-12.2.18.tgz", + "integrity": "sha512-niRS9Ly9y8uI0YmTSbo8KpdqCCiZ/ATMZWeS2id5M8JZvfXbngwiqJvojdSol0SWU+n1W4iA+lJBdt4gSKlD5w==", + "dev": true, + "dependencies": { + "@angular-devkit/core": "12.2.18", + "@angular-devkit/schematics": "12.2.18", + "jsonc-parser": "3.0.0" + }, + "engines": { + "node": "^12.14.1 || >=14.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@cypress/schematic/node_modules/ajv": { + "version": "8.6.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.6.2.tgz", + "integrity": "sha512-9807RlWAgT564wT+DjeyU5OFMPjmzxVobvDFmNAhY+5zD6A2ly3jDp6sgnfyDtlIQ+7H97oc/DGCzzfu9rjw9w==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@cypress/schematic/node_modules/ajv-formats": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.0.tgz", + "integrity": "sha512-USH2jBb+C/hIpwD2iRjp0pe0k+MvzG0mlSn/FIdCgQhUb9ALPRjt2KIQdfZDS9r0ZIeUAg7gOu9KL0PFqGqr5Q==", + "dev": true, + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/@cypress/schematic/node_modules/jsonc-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.0.0.tgz", + "integrity": "sha512-fQzRfAbIBnR0IQvftw9FJveWiHp72Fg20giDrHz6TdfB12UH/uue0D3hm57UB5KgAVuniLMCaS8P1IMj9NR7cA==", + "dev": true + }, + "node_modules/@cypress/schematic/node_modules/magic-string": { + "version": "0.25.7", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz", + "integrity": "sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA==", + "dev": true, + "dependencies": { + "sourcemap-codec": "^1.4.4" + } + }, + "node_modules/@cypress/schematic/node_modules/rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "dev": true, + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" + } + }, + "node_modules/@cypress/schematic/node_modules/source-map": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", + "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@cypress/schematic/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "node_modules/@cypress/xvfb": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@cypress/xvfb/-/xvfb-1.2.4.tgz", + "integrity": "sha512-skbBzPggOVYCbnGgV+0dmBdW/s77ZkAOXIC1knS8NagwDjBrNC1LuXtQJeiN6l+m7lzmHtaoUw/ctJKdqkG57Q==", + "dev": true, + "dependencies": { + "debug": "^3.1.0", + "lodash.once": "^4.1.1" + } + }, + "node_modules/@cypress/xvfb/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/@discoveryjs/json-ext": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", + "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", + "dev": true, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@edsilv/http-status-codes": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@edsilv/http-status-codes/-/http-status-codes-1.0.3.tgz", + "integrity": "sha512-HLK2FS5sZqxPqD53D6hhZxC6C8THTVwlyZDZ7J0iWsrB8JmMA69m/CQuNKZc1kki9WSVeck2fXna26NL0SE7cg==" + }, + "node_modules/@emotion/hash": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz", + "integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==" + }, + "node_modules/@es-joy/jsdoccomment": { + "version": "0.39.4", + "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.39.4.tgz", + "integrity": "sha512-Jvw915fjqQct445+yron7Dufix9A+m9j1fCJYlCo1FWlRvTxa3pjJelxdSTdaLWcTwRU6vbL+NYjO4YuNIS5Qg==", + "dev": true, + "dependencies": { + "comment-parser": "1.3.1", + "esquery": "^1.5.0", + "jsdoc-type-pratt-parser": "~4.0.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.1.tgz", + "integrity": "sha512-m55cpeupQ2DbuRGQMMZDzbv9J9PgVelPjlcmM5kxHnrBdBx6REaEd7LamYV7Dm8N7rCyR/XwU6rVP8ploKtIkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.1.tgz", + "integrity": "sha512-4j0+G27/2ZXGWR5okcJi7pQYhmkVgb4D7UKwxcqrjhvp5TKWx3cUjgB1CGj1mfdmJBQ9VnUGgUhign+FPF2Zgw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.1.tgz", + "integrity": "sha512-hCnXNF0HM6AjowP+Zou0ZJMWWa1VkD77BXe959zERgGJBBxB+sV+J9f/rcjeg2c5bsukD/n17RKWXGFCO5dD5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.1.tgz", + "integrity": "sha512-MSfZMBoAsnhpS+2yMFYIQUPs8Z19ajwfuaSZx+tSl09xrHZCjbeXXMsUF/0oq7ojxYEpsSo4c0SfjxOYXRbpaA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.1.tgz", + "integrity": "sha512-Ylk6rzgMD8klUklGPzS414UQLa5NPXZD5tf8JmQU8GQrj6BrFA/Ic9tb2zRe1kOZyCbGl+e8VMbDRazCEBqPvA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.1.tgz", + "integrity": "sha512-pFIfj7U2w5sMp52wTY1XVOdoxw+GDwy9FsK3OFz4BpMAjvZVs0dT1VXs8aQm22nhwoIWUmIRaE+4xow8xfIDZA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.1.tgz", + "integrity": "sha512-UyW1WZvHDuM4xDz0jWun4qtQFauNdXjXOtIy7SYdf7pbxSWWVlqhnR/T2TpX6LX5NI62spt0a3ldIIEkPM6RHw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.1.tgz", + "integrity": "sha512-itPwCw5C+Jh/c624vcDd9kRCCZVpzpQn8dtwoYIt2TJF3S9xJLiRohnnNrKwREvcZYx0n8sCSbvGH349XkcQeg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.1.tgz", + "integrity": "sha512-LojC28v3+IhIbfQ+Vu4Ut5n3wKcgTu6POKIHN9Wpt0HnfgUGlBuyDDQR4jWZUZFyYLiz4RBBBmfU6sNfn6RhLw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.1.tgz", + "integrity": "sha512-cX8WdlF6Cnvw/DO9/X7XLH2J6CkBnz7Twjpk56cshk9sjYVcuh4sXQBy5bmTwzBjNVZze2yaV1vtcJS04LbN8w==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.1.tgz", + "integrity": "sha512-4H/sQCy1mnnGkUt/xszaLlYJVTz3W9ep52xEefGtd6yXDQbz/5fZE5dFLUgsPdbUOQANcVUa5iO6g3nyy5BJiw==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.1.tgz", + "integrity": "sha512-c0jgtB+sRHCciVXlyjDcWb2FUuzlGVRwGXgI+3WqKOIuoo8AmZAddzeOHeYLtD+dmtHw3B4Xo9wAUdjlfW5yYA==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.1.tgz", + "integrity": "sha512-TgFyCfIxSujyuqdZKDZ3yTwWiGv+KnlOeXXitCQ+trDODJ+ZtGOzLkSWngynP0HZnTsDyBbPy7GWVXWaEl6lhA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.1.tgz", + "integrity": "sha512-b+yuD1IUeL+Y93PmFZDZFIElwbmFfIKLKlYI8M6tRyzE6u7oEP7onGk0vZRh8wfVGC2dZoy0EqX1V8qok4qHaw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.1.tgz", + "integrity": "sha512-wpDlpE0oRKZwX+GfomcALcouqjjV8MIX8DyTrxfyCfXxoKQSDm45CZr9fanJ4F6ckD4yDEPT98SrjvLwIqUCgg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.1.tgz", + "integrity": "sha512-5BepC2Au80EohQ2dBpyTquqGCES7++p7G+7lXe1bAIvMdXm4YYcEfZtQrP4gaoZ96Wv1Ute61CEHFU7h4FMueQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.1.tgz", + "integrity": "sha512-5gRPk7pKuaIB+tmH+yKd2aQTRpqlf1E4f/mC+tawIm/CGJemZcHZpp2ic8oD83nKgUPMEd0fNanrnFljiruuyA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.1.tgz", + "integrity": "sha512-4fL68JdrLV2nVW2AaWZBv3XEm3Ae3NZn/7qy2KGAt3dexAgSVT+Hc97JKSZnqezgMlv9x6KV0ZkZY7UO5cNLCg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.1.tgz", + "integrity": "sha512-GhRuXlvRE+twf2ES+8REbeCb/zeikNqwD3+6S5y5/x+DYbAQUNl0HNBs4RQJqrechS4v4MruEr8ZtAin/hK5iw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.1.tgz", + "integrity": "sha512-ZnWEyCM0G1Ex6JtsygvC3KUUrlDXqOihw8RicRuQAzw+c4f1D66YlPNNV3rkjVW90zXVsHwZYWbJh3v+oQFM9Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.1.tgz", + "integrity": "sha512-QZ6gXue0vVQY2Oon9WyLFCdSuYbXSoxaZrPuJ4c20j6ICedfsDilNPYfHLlMH7vGfU5DQR0czHLmJvH4Nzis/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.1.tgz", + "integrity": "sha512-HzcJa1NcSWTAU0MJIxOho8JftNp9YALui3o+Ny7hCh0v5f90nprly1U3Sj1Ldj/CvKKdvvFsCRvDkpsEMp4DNw==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.1.tgz", + "integrity": "sha512-0MBh53o6XtI6ctDnRMeQ+xoCN8kD2qI1rY1KgF/xdWQwoFeKou7puvDfV8/Wv4Ctx2rRpET/gGdz3YlNtNACSA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.0.tgz", + "integrity": "sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/eslintrc/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", + "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@fortawesome/fontawesome-free": { + "version": "6.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": "4.1.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.1.0.tgz", + "integrity": "sha512-HutrvTNsF48wnxkzERIXOe5/mlcfFcbfCmwcg6CJnizbSue78AbDt+1cgl26zwn61WFxhcPykPfZrbqjGmBb4A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "engines": { + "node": ">=8" + } + }, + "node_modules/classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==" + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-progress": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/cli-progress/-/cli-progress-3.12.0.tgz", + "integrity": "sha512-tRkV3HJ1ASwm19THiiLIXLO7Im7wlTuKnvkYaTkyoAPefqjNg7W7DHKUlGRxy9vxDvbyCYQkQozvptuMkGCg8A==", + "dependencies": { + "string-width": "^4.2.3" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cli-spinners": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.6.1.tgz", + "integrity": "sha512-x/5fWmGMnbKQAaNwN+UZlV79qBLM9JFnJuJ03gIi5whrob0xV0ofNVHy9DhwGdsMJQc2OKv0oGmLzvaqvAVv+g==", + "dev": true, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-table3": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.5.tgz", + "integrity": "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0" + }, + "engines": { + "node": "10.* || >= 12.*" + }, + "optionalDependencies": { + "@colors/colors": "1.5.0" + } + }, + "node_modules/cli-truncate": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", + "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", + "dev": true, + "dependencies": { + "slice-ansi": "^3.0.0", + "string-width": "^4.2.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "dev": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cliui/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/cliui/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "dev": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dev": true, + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/clone-deep/node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true + }, + "node_modules/colors": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", + "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", + "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/comment-parser": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.3.1.tgz", + "integrity": "sha512-B52sN2VNghyq5ofvUsqZjmk6YkihBX5vMSChmSK9v4ShjKf3Vk5Xcmgpw4o+iIgtrnM/u5FiMpz9VKb8lpBveA==", + "dev": true, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/common-path-prefix": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/common-path-prefix/-/common-path-prefix-3.0.0.tgz", + "integrity": "sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==", + "dev": true + }, + "node_modules/common-tags": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz", + "integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==", + "dev": true, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "dev": true + }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.5.tgz", + "integrity": "sha512-bQJ0YRck5ak3LgtnpKkiabX5pNF7tMUh1BSy2ZBOTh0Dim0BUu6aPPwByIns6/A5Prh8PufSPerMDUklpzes2Q==", + "dependencies": { + "bytes": "3.1.2", + "compressible": "~2.0.18", + "debug": "2.6.9", + "negotiator": "~0.6.4", + "on-headers": "~1.0.2", + "safe-buffer": "5.2.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/compression-webpack-plugin": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/compression-webpack-plugin/-/compression-webpack-plugin-9.2.0.tgz", + "integrity": "sha512-R/Oi+2+UHotGfu72fJiRoVpuRifZT0tTC6UqFD/DUo+mv8dbOow9rVOuTvDv5nPPm3GZhHL/fKkwxwIHnJ8Nyw==", + "dev": true, + "dependencies": { + "schema-utils": "^4.0.0", + "serialize-javascript": "^6.0.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + } + }, + "node_modules/compression/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/compression/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/compression/node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, + "node_modules/connect": { + "version": "3.6.6", + "resolved": "https://registry.npmjs.org/connect/-/connect-3.6.6.tgz", + "integrity": "sha512-OO7axMmPpu/2XuX1+2Yrg0ddju31B6xLZMWkJ5rYBu4YRmRVlOjvlY6kw2FJKiAzyxGwnrDUAG4s1Pf0sbBMCQ==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "debug": "2.6.9", + "finalhandler": "1.1.0", + "parseurl": "~1.3.2", + "utils-merge": "1.0.1" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/connect-history-api-fallback": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz", + "integrity": "sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg==", + "dev": true, + "optional": true, + "peer": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/connect/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/connect/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "optional": true, + "peer": true + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-disposition/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==" + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-parser": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz", + "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==", + "dependencies": { + "cookie": "0.7.2", + "cookie-signature": "1.0.6" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + }, + "node_modules/copy-anything": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-2.0.6.tgz", + "integrity": "sha512-1j20GZTsvKNkc4BY3NpMOM8tt///wY3FpIzozTOFO2ffuZcV61nojHXVKIy3WM+7ADCy5FVhdZYHYDdgTU0yJw==", + "dev": true, + "dependencies": { + "is-what": "^3.14.1" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/copy-to-clipboard": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz", + "integrity": "sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==", + "dependencies": { + "toggle-selection": "^1.0.6" + } + }, + "node_modules/copy-webpack-plugin": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-6.4.1.tgz", + "integrity": "sha512-MXyPCjdPVx5iiWyl40Va3JGh27bKzOTNY3NjUTrosD2q7dR/cLD0013uqJ3BpFbUjyONINjb6qI7nDIJujrMbA==", + "dev": true, + "dependencies": { + "cacache": "^15.0.5", + "fast-glob": "^3.2.4", + "find-cache-dir": "^3.3.1", + "glob-parent": "^5.1.1", + "globby": "^11.0.1", + "loader-utils": "^2.0.0", + "normalize-path": "^3.0.0", + "p-limit": "^3.0.2", + "schema-utils": "^3.0.0", + "serialize-javascript": "^5.0.1", + "webpack-sources": "^1.4.3" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.37.0 || ^5.0.0" + } + }, + "node_modules/copy-webpack-plugin/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/copy-webpack-plugin/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/copy-webpack-plugin/node_modules/find-cache-dir": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", + "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", + "dev": true, + "dependencies": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/avajs/find-cache-dir?sponsor=1" + } + }, + "node_modules/copy-webpack-plugin/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/copy-webpack-plugin/node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dev": true, + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/copy-webpack-plugin/node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/copy-webpack-plugin/node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/copy-webpack-plugin/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/copy-webpack-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/copy-webpack-plugin/node_modules/serialize-javascript": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-5.0.1.tgz", + "integrity": "sha512-SaaNal9imEO737H2c05Og0/8LUXG7EnsZyMa8MzkmuHoELfT6txuj0cMqRj6zfPKnmQ1yasR4PCJc8x+M4JSPA==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/core-js": { + "version": "3.39.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.39.0.tgz", + "integrity": "sha512-raM0ew0/jJUqkJ0E6e8UDtl+y/7ktFivgWvqw8dNSQeNWoSDLvQ1H/RN3aPXB9tBd4/FhyR4RDPGhsNIMsAn7g==", + "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.16.0", + "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.16.0.tgz", + "integrity": "sha512-g6XcwqnvzXrqiBQR/5gN+QsyRmKRhls1y5E42fyOvsmU7JuY+wM6uHJWj4ZPttjabzbnRvxcik2WemR8+xT6FA==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "@cypress/request": "^3.0.6", + "@cypress/xvfb": "^1.2.4", + "@types/sinonjs__fake-timers": "8.1.1", + "@types/sizzle": "^2.3.2", + "arch": "^2.2.0", + "blob-util": "^2.0.2", + "bluebird": "^3.7.2", + "buffer": "^5.7.1", + "cachedir": "^2.3.0", + "chalk": "^4.1.0", + "check-more-types": "^2.24.0", + "ci-info": "^4.0.0", + "cli-cursor": "^3.1.0", + "cli-table3": "~0.6.1", + "commander": "^6.2.1", + "common-tags": "^1.8.0", + "dayjs": "^1.10.4", + "debug": "^4.3.4", + "enquirer": "^2.3.6", + "eventemitter2": "6.4.7", + "execa": "4.1.0", + "executable": "^4.1.1", + "extract-zip": "2.0.1", + "figures": "^3.2.0", + "fs-extra": "^9.1.0", + "getos": "^3.2.1", + "is-installed-globally": "~0.4.0", + "lazy-ass": "^1.6.0", + "listr2": "^3.8.3", + "lodash": "^4.17.21", + "log-symbols": "^4.0.0", + "minimist": "^1.2.8", + "ospath": "^1.2.2", + "pretty-bytes": "^5.6.0", + "process": "^0.11.10", + "proxy-from-env": "1.0.0", + "request-progress": "^3.0.0", + "semver": "^7.5.3", + "supports-color": "^8.1.1", + "tmp": "~0.2.3", + "tree-kill": "1.2.2", + "untildify": "^4.0.0", + "yauzl": "^2.10.0" + }, + "bin": { + "cypress": "bin/cypress" + }, + "engines": { + "node": "^16.0.0 || ^18.0.0 || >=20.0.0" + } + }, + "node_modules/cypress-axe": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/cypress-axe/-/cypress-axe-1.5.0.tgz", + "integrity": "sha512-Hy/owCjfj+25KMsecvDgo4fC/781ccL+e8p+UUYoadGVM2ogZF9XIKbiM6KI8Y3cEaSreymdD6ZzccbI2bY0lQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "axe-core": "^3 || ^4", + "cypress": "^10 || ^11 || ^12 || ^13" + } + }, + "node_modules/cypress/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cypress/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/cypress/node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cypress/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/cypress/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/cypress/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cypress/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/cypress/node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/cypress/node_modules/proxy-from-env": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.0.0.tgz", + "integrity": "sha512-F2JHgJQ1iqwnHDcQjVBsq3n/uoaFL+iPW/eAeL7kVxy/2RrWaN4WroKjjvbsoRtv0ftelNyC01bjRhn/bhcf4A==", + "dev": true + }, + "node_modules/cypress/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/cypress/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", + "dev": true, + "dependencies": { + "assert-plus": "^1.0.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/data-view-buffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz", + "integrity": "sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz", + "integrity": "sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz", + "integrity": "sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/date-fns": { + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", + "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", + "dependencies": { + "@babel/runtime": "^7.21.0" + }, + "engines": { + "node": ">=0.11" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/date-fns" + } + }, + "node_modules/date-fns-tz": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/date-fns-tz/-/date-fns-tz-1.3.8.tgz", + "integrity": "sha512-qwNXUFtMHTTU6CFSFjoJ80W8Fzzp24LntbjFFBgL/faqds4e5mo9mftoRLgr3Vi1trISsg4awSpYVsOQCRnapQ==", + "peerDependencies": { + "date-fns": ">=2.0.0" + } + }, + "node_modules/date-format": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/date-format/-/date-format-4.0.14.tgz", + "integrity": "sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/dayjs": { + "version": "1.11.11", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.11.tgz", + "integrity": "sha512-okzr3f11N6WuqYtZSvm+F776mB41wRZMhKP+hc34YdW+KmtYYK9iqvHSwo2k9FEH3fhGXvOPV6yz2IcSrfRUDg==", + "dev": true + }, + "node_modules/debug": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-5.0.1.tgz", + "integrity": "sha512-VfxadyCECXgQlkoEAjeghAr5gY3Hf+IKjKb+X8tGVDtveCjN+USwprd2q3QXBR9T1+x2DG0XZF5/w+7HAtSaXA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-freeze": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/deep-freeze/-/deep-freeze-0.0.1.tgz", + "integrity": "sha512-Z+z8HiAvsGwmjqlphnHW5oz6yWlOwu6EQfFTjmeTWlDeda3FS2yv3jhq35TX/ewmsnqB+RX2IdsIOyjJCQN5tg==", + "dev": true + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/default-gateway": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-6.0.3.tgz", + "integrity": "sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg==", + "dev": true, + "dependencies": { + "execa": "^5.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/default-gateway/node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/default-gateway/node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-gateway/node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "dev": true, + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delay": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/delay/-/delay-5.0.0.tgz", + "integrity": "sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "dev": true, + "optional": true, + "bin": { + "detect-libc": "bin/detect-libc.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "dev": true + }, + "node_modules/dev-ip": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dev-ip/-/dev-ip-1.0.1.tgz", + "integrity": "sha512-LmVkry/oDShEgSZPNgqCIp2/TlqtExeGmymru3uCELnfyjY11IzpAproLYs+1X88fXO6DBoYP3ul2Xo2yz2j6A==", + "dev": true, + "optional": true, + "peer": true, + "bin": { + "dev-ip": "lib/dev-ip.js" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/di": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/di/-/di-0.0.1.tgz", + "integrity": "sha512-uJaamHkagcZtHPqCIHZxnFrXlunQXgBOsZSUOWwFw31QJCAbyTBoHMW75YOTur5ZNx8pIeAKgf6GWIgaqqiLhA==", + "dev": true + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dnd-core": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/dnd-core/-/dnd-core-10.0.2.tgz", + "integrity": "sha512-PrxEjxF0+6Y1n1n1Z9hSWZ1tvnDXv9syL+BccV1r1RC08uWNsyetf8AnWmUF3NgYPwy0HKQJwTqGkZK+1NlaFA==", + "dependencies": { + "@react-dnd/asap": "^4.0.0", + "@react-dnd/invariant": "^2.0.0", + "redux": "^4.0.4" + } + }, + "node_modules/dnd-multi-backend": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/dnd-multi-backend/-/dnd-multi-backend-5.0.1.tgz", + "integrity": "sha512-BAOj6fIeGfZPA1LreehaV8mUD7Ww0EpaKqSDRrZ5mZXLWLIxFPPT3bIvpJhHUCt0+lTrOqkXu11Hudh+gHXNUA==" + }, + "node_modules/dns-packet": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz", + "integrity": "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==", + "dev": true, + "dependencies": { + "@leichtgewicht/ip-codec": "^2.0.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, + "node_modules/dom-helpers/node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" + }, + "node_modules/dom-serialize": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/dom-serialize/-/dom-serialize-2.2.1.tgz", + "integrity": "sha512-Yra4DbvoW7/Z6LBN560ZwXMjoNOSAN2wRsKFGc4iBeso+mpIA6qj1vfdf9HpMaKAqG6wXTy+1SYEzmNpKXOSsQ==", + "dev": true, + "dependencies": { + "custom-event": "~1.0.0", + "ent": "~2.2.0", + "extend": "^3.0.0", + "void-elements": "^2.0.0" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ] + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/dompurify": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.5.6.tgz", + "integrity": "sha512-zUTaUBO8pY4+iJMPE1B9XlO2tXVYIcEA4SNGtvDELzTSCQO7RzH+j7S180BmhmJId78lqGU2z19vgVx2Sxs/PQ==" + }, + "node_modules/domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, + "node_modules/easy-extender": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/easy-extender/-/easy-extender-2.3.4.tgz", + "integrity": "sha512-8cAwm6md1YTiPpOvDULYJL4ZS6WfM5/cTeVVh4JsvyYZAoqlRVUpHL9Gr5Fy7HA6xcSZicUia3DeAgO3Us8E+Q==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "lodash": "^4.17.10" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/eazy-logger": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/eazy-logger/-/eazy-logger-4.0.1.tgz", + "integrity": "sha512-2GSFtnnC6U4IEKhEI7+PvdxrmjJ04mdsj3wHZTFiw0tUtG4HCWzTr13ZYTk8XOGnA1xQMaDljoBOYlk3D/MMSw==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "chalk": "4.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/eazy-logger/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/eazy-logger/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/eazy-logger/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/eazy-logger/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "optional": true, + "peer": true + }, + "node_modules/eazy-logger/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "optional": true, + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/eazy-logger/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", + "dev": true, + "dependencies": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "node_modules/ecc-jsbn/node_modules/jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==", + "dev": true + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.38", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.38.tgz", + "integrity": "sha512-VbeVexmZ1IFh+5EfrYz1I0HTzHVIlJa112UEWhciPyeOcKJGeTv6N8WnG4wsQB81DGCaVEGhpSb6o6a8WYFXXg==" + }, + "node_modules/element-resize-detector": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/element-resize-detector/-/element-resize-detector-1.2.4.tgz", + "integrity": "sha512-Fl5Ftk6WwXE0wqCgNoseKWndjzZlDCwuPTcoVZfCP9R3EHQF8qUtr3YUPNETegRBOKqQKPW3n4kiIWngGi8tKg==", + "dependencies": { + "batch-processor": "1.0.0" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dev": true, + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/engine.io": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.2.tgz", + "integrity": "sha512-gmNvsYi9C8iErnZdVcJnvCpSKbWTt1E8+JZo8b+daLninywUWi5NQ5STSHZ9rFjFO7imNcvb8Pc5pe/wMR5xEw==", + "dev": true, + "dependencies": { + "@types/cookie": "^0.4.1", + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.7.2", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-client": { + "version": "6.6.1", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.1.tgz", + "integrity": "sha512-aYuoak7I+R83M/BBPIOs2to51BmFIpC1wZe6zZzMrT2llVsHy5cvcmdsJgP2Qz6smHu+sD9oexiSUAVd8OfBPw==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1", + "xmlhttprequest-ssl": "~2.1.1" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.2.tgz", + "integrity": "sha512-RcyUFKA93/CXH20l4SoVvzZfrSDMOTUS3bWVpTt2FuFP+XYrL8i8oonHP7WInRyVHXh0n/ORtoeiE1os+8qkSw==", + "dev": true, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.17.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz", + "integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/enquirer": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", + "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==", + "dev": true, + "dependencies": { + "ansi-colors": "^4.1.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/ent": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.1.tgz", + "integrity": "sha512-QHuXVeZx9d+tIQAz/XztU0ZwZf2Agg9CcXcgE1rurqvdBeDBrpSwjl8/6XUqMg7tw2Y7uAdKb2sRv+bSEFqQ5A==", + "dev": true, + "dependencies": { + "punycode": "^1.4.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/envinfo": { + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.13.0.tgz", + "integrity": "sha512-cvcaMr7KqXVh4nyzGTVqTum+gAiL265x5jUWQIDLq//zOGbW+gSW/C+OWLleY/rs9Qole6AZLMXPbtIFQbqu+Q==", + "dev": true, + "bin": { + "envinfo": "dist/cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "dev": true + }, + "node_modules/errno": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", + "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", + "dev": true, + "optional": true, + "dependencies": { + "prr": "~1.0.1" + }, + "bin": { + "errno": "cli.js" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-abstract": { + "version": "1.23.3", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.3.tgz", + "integrity": "sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "arraybuffer.prototype.slice": "^1.0.3", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "data-view-buffer": "^1.0.1", + "data-view-byte-length": "^1.0.1", + "data-view-byte-offset": "^1.0.0", + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-set-tostringtag": "^2.0.3", + "es-to-primitive": "^1.2.1", + "function.prototype.name": "^1.1.6", + "get-intrinsic": "^1.2.4", + "get-symbol-description": "^1.0.2", + "globalthis": "^1.0.3", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.0.3", + "has-symbols": "^1.0.3", + "hasown": "^2.0.2", + "internal-slot": "^1.0.7", + "is-array-buffer": "^3.0.4", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.1", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.3", + "is-string": "^1.0.7", + "is-typed-array": "^1.1.13", + "is-weakref": "^1.0.2", + "object-inspect": "^1.13.1", + "object-keys": "^1.1.1", + "object.assign": "^4.1.5", + "regexp.prototype.flags": "^1.5.2", + "safe-array-concat": "^1.1.2", + "safe-regex-test": "^1.0.3", + "string.prototype.trim": "^1.2.9", + "string.prototype.trimend": "^1.0.8", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.2", + "typed-array-byte-length": "^1.0.1", + "typed-array-byte-offset": "^1.0.2", + "typed-array-length": "^1.0.6", + "unbox-primitive": "^1.0.2", + "which-typed-array": "^1.1.15" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.4.tgz", + "integrity": "sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==", + "dev": true + }, + "node_modules/es-object-atoms": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", + "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz", + "integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.4", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz", + "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==", + "dev": true, + "dependencies": { + "hasown": "^2.0.0" + } + }, + "node_modules/es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "dependencies": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es6-promisify": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-7.0.0.tgz", + "integrity": "sha512-ginqzK3J90Rd4/Yz7qRrqUeIpe3TwSXTPPZtPne7tGBPeAaQiU8qt4fpKApnxHcq1AwtUdHVg5P77x/yrggG8Q==", + "engines": { + "node": ">=6" + } + }, + "node_modules/esbuild": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.1.tgz", + "integrity": "sha512-OJwEgrpWm/PCMsLVWXKqvcjme3bHNpOgN7Tb6cQnR5n0TPbQx1/Xrn7rqM+wn17bYeT6MGB5sn1Bh5YiGi70nA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.20.1", + "@esbuild/android-arm": "0.20.1", + "@esbuild/android-arm64": "0.20.1", + "@esbuild/android-x64": "0.20.1", + "@esbuild/darwin-arm64": "0.20.1", + "@esbuild/darwin-x64": "0.20.1", + "@esbuild/freebsd-arm64": "0.20.1", + "@esbuild/freebsd-x64": "0.20.1", + "@esbuild/linux-arm": "0.20.1", + "@esbuild/linux-arm64": "0.20.1", + "@esbuild/linux-ia32": "0.20.1", + "@esbuild/linux-loong64": "0.20.1", + "@esbuild/linux-mips64el": "0.20.1", + "@esbuild/linux-ppc64": "0.20.1", + "@esbuild/linux-riscv64": "0.20.1", + "@esbuild/linux-s390x": "0.20.1", + "@esbuild/linux-x64": "0.20.1", + "@esbuild/netbsd-x64": "0.20.1", + "@esbuild/openbsd-x64": "0.20.1", + "@esbuild/sunos-x64": "0.20.1", + "@esbuild/win32-arm64": "0.20.1", + "@esbuild/win32-ia32": "0.20.1", + "@esbuild/win32-x64": "0.20.1" + } + }, + "node_modules/esbuild-wasm": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/esbuild-wasm/-/esbuild-wasm-0.20.1.tgz", + "integrity": "sha512-6v/WJubRsjxBbQdz6izgvx7LsVFvVaGmSdwrFHmEzoVgfXL89hkKPoQHsnVI2ngOkcBUQT9kmAM1hVL1k/Av4A==", + "dev": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/escalade": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/eslint": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", + "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.0", + "@humanwhocodes/config-array": "^0.11.14", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-compat-utils": { + "version": "0.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-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/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true + }, + "node_modules/pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", + "dev": true + }, + "node_modules/qjobs": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/qjobs/-/qjobs-1.2.0.tgz", + "integrity": "sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg==", + "dev": true, + "engines": { + "node": ">=0.9" + } + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/raf-schd": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.3.tgz", + "integrity": "sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==" + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/re-reselect": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/re-reselect/-/re-reselect-4.0.1.tgz", + "integrity": "sha512-xVTNGQy/dAxOolunBLmVMGZ49VUUR1s8jZUiJQb+g1sI63GAv9+a5Jas9yHvdxeUgiZkU9r3gDExDorxHzOgRA==", + "peerDependencies": { + "reselect": ">1.0.0" + } + }, + "node_modules/re-resizable": { + "version": "6.9.17", + "resolved": "https://registry.npmjs.org/re-resizable/-/re-resizable-6.9.17.tgz", + "integrity": "sha512-OBqd1BwVXpEJJn/yYROG+CbeqIDBWIp6wathlpB0kzZWWZIY1gPTsgK2yJEui5hOvkCdC2mcexF2V3DZVfLq2g==", + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.13.1 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/react": { + "version": "16.14.0", + "resolved": "https://registry.npmjs.org/react/-/react-16.14.0.tgz", + "integrity": "sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g==", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1", + "prop-types": "^15.6.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-aria-live": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/react-aria-live/-/react-aria-live-2.0.5.tgz", + "integrity": "sha512-rXiH1HNKJrr/UfVeGwA2aKY43r5WbjLs+AYB6/kJF1qny2hwxzQc1qewQmUpdQ5h8HAOTD8O/XlGcEHjqlCl0g==", + "dependencies": { + "uuid": "^3.2.1" + }, + "peerDependencies": { + "react": "^16.3.x" + } + }, + "node_modules/react-aria-live/node_modules/uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "bin": { + "uuid": "bin/uuid" + } + }, + "node_modules/react-beautiful-dnd": { + "version": "13.1.1", + "resolved": "https://registry.npmjs.org/react-beautiful-dnd/-/react-beautiful-dnd-13.1.1.tgz", + "integrity": "sha512-0Lvs4tq2VcrEjEgDXHjT98r+63drkKEgqyxdA7qD3mvKwga6a5SscbdLPO2IExotU1jW8L0Ksdl0Cj2AF67nPQ==", + "dependencies": { + "@babel/runtime": "^7.9.2", + "css-box-model": "^1.2.0", + "memoize-one": "^5.1.1", + "raf-schd": "^4.0.2", + "react-redux": "^7.2.0", + "redux": "^4.0.4", + "use-memo-one": "^1.1.1" + }, + "peerDependencies": { + "react": "^16.8.5 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.5 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/react-copy-to-clipboard": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/react-copy-to-clipboard/-/react-copy-to-clipboard-5.1.0.tgz", + "integrity": "sha512-k61RsNgAayIJNoy9yDsYzDe/yAZAzEbEgcz3DZMhF686LEyukcE1hzurxe85JandPUG+yTfGVFzuEw3xt8WP/A==", + "dependencies": { + "copy-to-clipboard": "^3.3.1", + "prop-types": "^15.8.1" + }, + "peerDependencies": { + "react": "^15.3.0 || 16 || 17 || 18" + } + }, + "node_modules/react-dnd": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/react-dnd/-/react-dnd-10.0.2.tgz", + "integrity": "sha512-SC2Ymvntynhoqtf5zaFhZscm9xenCoMofilxPdlwUlaelAzmbl9fw82C4ZJ//+lNm3kWAKXjGDZg2/aWjKEAtg==", + "dependencies": { + "@react-dnd/shallowequal": "^2.0.0", + "@types/hoist-non-react-statics": "^3.3.1", + "dnd-core": "^10.0.2", + "hoist-non-react-statics": "^3.3.0" + }, + "peerDependencies": { + "react": ">= 16.8", + "react-dom": ">= 16.8" + } + }, + "node_modules/react-dnd-html5-backend": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/react-dnd-html5-backend/-/react-dnd-html5-backend-10.0.2.tgz", + "integrity": "sha512-ny17gUdInZ6PIGXdzfwPhoztRdNVVvjoJMdG80hkDBamJBeUPuNF2Wv4D3uoQJLjXssX1+i9PhBqc7EpogClwQ==", + "dependencies": { + "dnd-core": "^10.0.2" + } + }, + "node_modules/react-dnd-multi-backend": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/react-dnd-multi-backend/-/react-dnd-multi-backend-5.0.1.tgz", + "integrity": "sha512-E45T5xVl4CLt9QFV9ZMjWCGLyF16+B6B6FgbeZE9J5o0rvLHaCqyAJTelRDXtbSiSBPaDlN1STO86jkiD31AHA==", + "dependencies": { + "dnd-multi-backend": "^5.0.1", + "prop-types": "^15.7.2", + "react-dnd-preview": "^5.0.1" + }, + "peerDependencies": { + "react": "^16.13", + "react-dnd-html5-backend": "^10.0.2", + "react-dnd-touch-backend": "^10.0.2" + } + }, + "node_modules/react-dnd-preview": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/react-dnd-preview/-/react-dnd-preview-5.0.1.tgz", + "integrity": "sha512-wwUgk8jWuO4WMOoa+GviHoZyYAiXer6vsSW2aEhpz0gsde08O+4cI+j7DG7q9vYlBnljNutdQ1WjDV0VskZ4jQ==", + "dependencies": { + "prop-types": "^15.7.2" + }, + "peerDependencies": { + "react": "^16.13.1", + "react-dnd": "^10.0.2" + } + }, + "node_modules/react-dnd-touch-backend": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/react-dnd-touch-backend/-/react-dnd-touch-backend-10.0.2.tgz", + "integrity": "sha512-+lW/Ern0dKyHToD0oP+Wc/ZD6l7qJazosLqbjzL7OnPlig6WxdlrHkJylOLkeAdZj41fIJJ551Lb57pIL0CcPw==", + "dependencies": { + "@react-dnd/invariant": "^2.0.0", + "dnd-core": "^10.0.2" + } + }, + "node_modules/react-dom": { + "version": "16.14.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.14.0.tgz", + "integrity": "sha512-1gCeQXDLoIqMgqD3IO2Ah9bnf0w9kzhwN5q4FGnHZ67hBm9yePzB5JJAIQCc8x3pFnNlwFq4RidZggNAAkzWWw==", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1", + "prop-types": "^15.6.2", + "scheduler": "^0.19.1" + }, + "peerDependencies": { + "react": "^16.14.0" + } + }, + "node_modules/react-draggable": { + "version": "4.4.6", + "resolved": "https://registry.npmjs.org/react-draggable/-/react-draggable-4.4.6.tgz", + "integrity": "sha512-LtY5Xw1zTPqHkVmtM3X8MUOxNDOUhv/khTgBgrUvwaS064bwVvxT+q5El0uUFNx5IEPKXuRejr7UqLwBIg5pdw==", + "dependencies": { + "clsx": "^1.1.1", + "prop-types": "^15.8.1" + }, + "peerDependencies": { + "react": ">= 16.3.0", + "react-dom": ">= 16.3.0" + } + }, + "node_modules/react-full-screen": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/react-full-screen/-/react-full-screen-0.2.5.tgz", + "integrity": "sha512-LNkxjLWmiR+AwemSVdn/miUcBy8tHA6mDVS1qz1AM/DHNEtQbzkh5ok9A6g99502OqutQq1zBvCBGLV8rsB2tw==", + "dependencies": { + "@types/react": "*", + "fscreen": "^1.0.1" + }, + "peerDependencies": { + "prop-types": "^15.5", + "react": "^15.6 || ^16" + } + }, + "node_modules/react-i18next": { + "version": "11.18.6", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-11.18.6.tgz", + "integrity": "sha512-yHb2F9BiT0lqoQDt8loZ5gWP331GwctHz9tYQ8A2EIEUu+CcEdjBLQWli1USG3RdWQt3W+jqQLg/d4rrQR96LA==", + "dependencies": { + "@babel/runtime": "^7.14.5", + "html-parse-stringify": "^3.0.1" + }, + "peerDependencies": { + "i18next": ">= 19.0.0", + "react": ">= 16.8.0" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, + "node_modules/react-image": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/react-image/-/react-image-4.1.0.tgz", + "integrity": "sha512-qwPNlelQe9Zy14K2pGWSwoL+vHsAwmJKS6gkotekDgRpcnRuzXNap00GfibD3eEPYu3WCPlyIUUNzcyHOrLHjw==", + "peerDependencies": { + "@babel/runtime": ">=7", + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" + }, + "node_modules/react-lifecycles-compat": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", + "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" + }, + "node_modules/react-modal": { + "version": "3.16.1", + "resolved": "https://registry.npmjs.org/react-modal/-/react-modal-3.16.1.tgz", + "integrity": "sha512-VStHgI3BVcGo7OXczvnJN7yT2TWHJPDXZWyI/a0ssFNhGZWsPmB8cF0z33ewDXq4VfYMO1vXgiv/g8Nj9NDyWg==", + "dependencies": { + "exenv": "^1.2.0", + "prop-types": "^15.7.2", + "react-lifecycles-compat": "^3.0.0", + "warning": "^4.0.3" + }, + "engines": { + "node": ">=8" + }, + "peerDependencies": { + "react": "^0.14.0 || ^15.0.0 || ^16 || ^17 || ^18", + "react-dom": "^0.14.0 || ^15.0.0 || ^16 || ^17 || ^18" + } + }, + "node_modules/react-mosaic-component": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/react-mosaic-component/-/react-mosaic-component-4.1.1.tgz", + "integrity": "sha512-HVlLvfYQ/AKmoKvw95Orx3Qyc7SNuS/QlAy+SkAVit1g9ipzXBGYoBg7RMXP5sF5w47CgYxA+1gT+fYRVf73jA==", + "dependencies": { + "classnames": "^2.2.6", + "immutability-helper": "^3.0.1", + "lodash": "^4.17.11", + "prop-types": "^15.7.2", + "react-dnd": "^10.0.2", + "react-dnd-html5-backend": "^10.0.2", + "react-dnd-multi-backend": "^5.0.0", + "react-dnd-touch-backend": "^10.0.2", + "uuid": "^3.3.2" + }, + "peerDependencies": { + "react": "^16.3.0" + } + }, + "node_modules/react-mosaic-component/node_modules/uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "bin": { + "uuid": "bin/uuid" + } + }, + "node_modules/react-redux": { + "version": "7.2.9", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.9.tgz", + "integrity": "sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==", + "dependencies": { + "@babel/runtime": "^7.15.4", + "@types/react-redux": "^7.1.20", + "hoist-non-react-statics": "^3.3.2", + "loose-envify": "^1.4.0", + "prop-types": "^15.7.2", + "react-is": "^17.0.2" + }, + "peerDependencies": { + "react": "^16.8.3 || ^17 || ^18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, + "node_modules/react-resize-observer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/react-resize-observer/-/react-resize-observer-1.1.1.tgz", + "integrity": "sha512-3R+90Hou90Mr3wJYc+unsySC8Pn91V4nmjO32NKvUvjphRUbq9HisyLg7bDyGBE7xlMrrM6Fax7iNQaFdc/FYA==", + "peerDependencies": { + "react": ">=0.14" + } + }, + "node_modules/react-rnd": { + "version": "10.4.11", + "resolved": "https://registry.npmjs.org/react-rnd/-/react-rnd-10.4.11.tgz", + "integrity": "sha512-XTfNGNcS0ad2vo3to7qNTB0BkFML9k1csIUI0Nlj44M6Uuh7yP/2h8WXiXcV3v3bxxVJck1C9K6FS1LrLH0E0Q==", + "dependencies": { + "re-resizable": "6.9.17", + "react-draggable": "4.4.6", + "tslib": "2.6.2" + }, + "peerDependencies": { + "react": ">=16.3.0", + "react-dom": ">=16.3.0" + } + }, + "node_modules/react-sizeme": { + "version": "2.6.12", + "resolved": "https://registry.npmjs.org/react-sizeme/-/react-sizeme-2.6.12.tgz", + "integrity": "sha512-tL4sCgfmvapYRZ1FO2VmBmjPVzzqgHA7kI8lSJ6JS6L78jXFNRdOZFpXyK6P1NBZvKPPCZxReNgzZNUajAerZw==", + "dependencies": { + "element-resize-detector": "^1.2.1", + "invariant": "^2.2.4", + "shallowequal": "^1.1.0", + "throttle-debounce": "^2.1.0" + }, + "peerDependencies": { + "react": "^0.14.0 || ^15.0.0-0 || ^16.0.0", + "react-dom": "^0.14.0 || ^15.0.0-0 || ^16.0.0" + } + }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, + "node_modules/react-virtualized-auto-sizer": { + "version": "1.0.24", + "resolved": "https://registry.npmjs.org/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.24.tgz", + "integrity": "sha512-3kCn7N9NEb3FlvJrSHWGQ4iVl+ydQObq2fHMn12i5wbtm74zHOPhz/i64OL3c1S1vi9i2GXtZqNqUJTQ+BnNfg==", + "peerDependencies": { + "react": "^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0", + "react-dom": "^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/react-window": { + "version": "1.8.10", + "resolved": "https://registry.npmjs.org/react-window/-/react-window-1.8.10.tgz", + "integrity": "sha512-Y0Cx+dnU6NLa5/EvoHukUD0BklJ8qITCtVEPY1C/nL8wwoZ0b5aEw8Ff1dOVHw7fCzMt55XfJDd8S8W8LCaUCg==", + "dependencies": { + "@babel/runtime": "^7.0.0", + "memoize-one": ">=3.1.1 <6" + }, + "engines": { + "node": ">8.0.0" + }, + "peerDependencies": { + "react": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/read-package-json": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/read-package-json/-/read-package-json-7.0.1.tgz", + "integrity": "sha512-8PcDiZ8DXUjLf687Ol4BR8Bpm2umR7vhoZOzNRt+uxD9GpBh/K+CAAALVIiYFknmvlmyg7hM7BSNUXPaCCqd0Q==", + "deprecated": "This package is no longer supported. Please use @npmcli/package-json instead.", + "dev": true, + "dependencies": { + "glob": "^10.2.2", + "json-parse-even-better-errors": "^3.0.0", + "normalize-package-data": "^6.0.0", + "npm-normalize-package-bin": "^3.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/read-package-json-fast": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/read-package-json-fast/-/read-package-json-fast-3.0.2.tgz", + "integrity": "sha512-0J+Msgym3vrLOUB3hzQCuZHII0xkNGCtz/HJH9xZshwv9DbDwkw1KaE3gx/e2J5rpEY5rtOy6cyhKOPrkP7FZw==", + "dev": true, + "dependencies": { + "json-parse-even-better-errors": "^3.0.0", + "npm-normalize-package-bin": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/read-package-json/node_modules/glob": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.3.tgz", + "integrity": "sha512-Q38SGlYRpVtDBPSWEylRyctn7uDeTp4NQERTLiCT1FqA9JXPYWqAVmQU6qh4r/zMM5ehxTcbaO8EjhWnvEhmyg==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/read-package-json/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/readdirp/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/rechoir": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", + "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", + "dev": true, + "dependencies": { + "resolve": "^1.20.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/redux": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", + "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", + "dependencies": { + "@babel/runtime": "^7.9.2" + } + }, + "node_modules/redux-devtools-extension": { + "version": "2.13.9", + "resolved": "https://registry.npmjs.org/redux-devtools-extension/-/redux-devtools-extension-2.13.9.tgz", + "integrity": "sha512-cNJ8Q/EtjhQaZ71c8I9+BPySIBVEKssbPpskBfsXqb8HJ002A3KRVHfeRzwRo6mGPqsm7XuHTqNSNeS1Khig0A==", + "deprecated": "Package moved to @redux-devtools/extension.", + "peerDependencies": { + "redux": "^3.1.0 || ^4.0.0" + } + }, + "node_modules/redux-saga": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/redux-saga/-/redux-saga-1.3.0.tgz", + "integrity": "sha512-J9RvCeAZXSTAibFY0kGw6Iy4EdyDNW7k6Q+liwX+bsck7QVsU78zz8vpBRweEfANxnnlG/xGGeOvf6r8UXzNJQ==", + "dependencies": { + "@redux-saga/core": "^1.3.0" + } + }, + "node_modules/redux-thunk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.2.tgz", + "integrity": "sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q==", + "peerDependencies": { + "redux": "^4" + } + }, + "node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==" + }, + "node_modules/regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", + "dev": true + }, + "node_modules/regenerate-unicode-properties": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.0.tgz", + "integrity": "sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA==", + "dev": true, + "dependencies": { + "regenerate": "^1.4.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" + }, + "node_modules/regenerator-transform": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz", + "integrity": "sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.8.4" + } + }, + "node_modules/regex-parser": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/regex-parser/-/regex-parser-2.3.0.tgz", + "integrity": "sha512-TVILVSz2jY5D47F4mA4MppkBrafEaiUWJO/TcZHEIuI13AqoZMkK1WMA4Om1YkYbTx+9Ki1/tSUXbceyr9saRg==", + "dev": true + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz", + "integrity": "sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.6", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "set-function-name": "^2.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexpu-core": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.1.1.tgz", + "integrity": "sha512-k67Nb9jvwJcJmVpw0jPttR1/zVfnKf8Km0IPatrU/zJ5XeG3+Slx0xLXs9HByJSzXzrlz5EDvN6yLNMDc2qdnw==", + "dev": true, + "dependencies": { + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.2.0", + "regjsgen": "^0.8.0", + "regjsparser": "^0.11.0", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==", + "dev": true + }, + "node_modules/regjsparser": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.11.1.tgz", + "integrity": "sha512-1DHODs4B8p/mQHU9kr+jv8+wIC9mtG4eBHxWxIq5mhjE3D5oORhCc6deRKzTjs9DcfRFmj9BHSDguZklqCGFWQ==", + "dev": true, + "dependencies": { + "jsesc": "~3.0.2" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/regjsparser/node_modules/jsesc": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", + "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/request-progress": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/request-progress/-/request-progress-3.0.0.tgz", + "integrity": "sha512-MnWzEHHaxHO2iWiQuHrUPBi/1WeBf5PkxQqNyNvLl9VAYSdXkP8tQ3pBSeCPD+yw0v0Aq1zosWLz0BdeXpWwZg==", + "dev": true, + "dependencies": { + "throttleit": "^1.0.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/requireindex": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/requireindex/-/requireindex-1.2.0.tgz", + "integrity": "sha512-L9jEkOi3ASd9PYit2cwRfyppc9NoABujTP8/5gFcbERmo5jUoAKovIC3fsF17pkTnGsrByysqX+Kxd2OTNI1ww==", + "dev": true, + "engines": { + "node": ">=0.10.5" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" + }, + "node_modules/reselect": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.1.8.tgz", + "integrity": "sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ==" + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-url-loader": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-url-loader/-/resolve-url-loader-5.0.0.tgz", + "integrity": "sha512-uZtduh8/8srhBoMx//5bwqjQ+rfYOUq8zC9NrMUGtjBiGTtFJM42s58/36+hTqeqINcnYe08Nj3LkK9lW4N8Xg==", + "dev": true, + "dependencies": { + "adjust-sourcemap-loader": "^4.0.0", + "convert-source-map": "^1.7.0", + "loader-utils": "^2.0.0", + "postcss": "^8.2.14", + "source-map": "0.6.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/resolve-url-loader/node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dev": true, + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/resolve-url-loader/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resp-modifier": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/resp-modifier/-/resp-modifier-6.0.2.tgz", + "integrity": "sha512-U1+0kWC/+4ncRFYqQWTx/3qkfE6a4B/h3XXgmXypfa0SPZ3t7cbbaFk297PjQS/yov24R18h6OZe6iZwj3NSLw==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "debug": "^2.2.0", + "minimatch": "^3.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/resp-modifier/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/resp-modifier/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/resp-modifier/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/resp-modifier/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "optional": true, + "peer": true + }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/roarr": { + "version": "7.21.1", + "resolved": "https://registry.npmjs.org/roarr/-/roarr-7.21.1.tgz", + "integrity": "sha512-3niqt5bXFY1InKU8HKWqqYTYjtrBaxBMnXELXCXUYgtNYGUtZM5rB46HIC430AyacL95iEniGf7RgqsesykLmQ==", + "dependencies": { + "fast-printf": "^1.6.9", + "safe-stable-stringify": "^2.4.3", + "semver-compare": "^1.0.0" + }, + "engines": { + "node": ">=18.0" + } + }, + "node_modules/rollup": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.24.0.tgz", + "integrity": "sha512-DOmrlGSXNk1DM0ljiQA+i+o0rSLhtii1je5wgk60j49d1jHT5YYttBv1iWOnYSTG+fZZESUOSNiAl89SIet+Cg==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.6" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.24.0", + "@rollup/rollup-android-arm64": "4.24.0", + "@rollup/rollup-darwin-arm64": "4.24.0", + "@rollup/rollup-darwin-x64": "4.24.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.24.0", + "@rollup/rollup-linux-arm-musleabihf": "4.24.0", + "@rollup/rollup-linux-arm64-gnu": "4.24.0", + "@rollup/rollup-linux-arm64-musl": "4.24.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.24.0", + "@rollup/rollup-linux-riscv64-gnu": "4.24.0", + "@rollup/rollup-linux-s390x-gnu": "4.24.0", + "@rollup/rollup-linux-x64-gnu": "4.24.0", + "@rollup/rollup-linux-x64-musl": "4.24.0", + "@rollup/rollup-win32-arm64-msvc": "4.24.0", + "@rollup/rollup-win32-ia32-msvc": "4.24.0", + "@rollup/rollup-win32-x64-msvc": "4.24.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/rtl-css-js": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/rtl-css-js/-/rtl-css-js-1.16.1.tgz", + "integrity": "sha512-lRQgou1mu19e+Ya0LsTvKrVJ5TYUbqCVPAiImX3UfLTenarvPUl1QFdvu5Z3PYmHT9RCcwIfbjRQBntExyj3Zg==", + "dependencies": { + "@babel/runtime": "^7.1.2" + } + }, + "node_modules/run-async": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-3.0.0.tgz", + "integrity": "sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/rx": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/rx/-/rx-4.1.0.tgz", + "integrity": "sha512-CiaiuN6gapkdl+cZUr67W6I8jquN4lkak3vtIsIWCl4XIPP8ffsoyN6/+PuGXnQy8Cu8W2y9Xxh31Rq4M6wUug==", + "dev": true, + "optional": true, + "peer": true + }, + "node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/rxjs-report-usage": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/rxjs-report-usage/-/rxjs-report-usage-1.0.6.tgz", + "integrity": "sha512-omv1DIv5z1kV+zDAEjaDjWSkx8w5TbFp5NZoPwUipwzYVcor/4So9ZU3bUyQ1c8lxY5Q0Es/ztWW7PGjY7to0Q==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.10.3", + "@babel/traverse": "^7.10.3", + "@babel/types": "^7.10.3", + "bent": "~7.3.6", + "chalk": "~4.1.0", + "glob": "~7.2.0", + "prompts": "~2.4.2" + }, + "bin": { + "rxjs-report-usage": "bin/rxjs-report-usage" + } + }, + "node_modules/rxjs-report-usage/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/rxjs-report-usage/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/rxjs-report-usage/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/rxjs-report-usage/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/rxjs-report-usage/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/rxjs-report-usage/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/safe-array-concat": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.2.tgz", + "integrity": "sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "get-intrinsic": "^1.2.4", + "has-symbols": "^1.0.3", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/safe-regex-test": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz", + "integrity": "sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-regex": "^1.1.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-stable-stringify": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz", + "integrity": "sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==", + "engines": { + "node": ">=10" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/sass": { + "version": "1.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/tldts": { + "version": "6.1.65", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.65.tgz", + "integrity": "sha512-xU9gLTfAGsADQ2PcWee6Hg8RFAv0DnjMGVJmDnUmI8a9+nYmapMQix4afwrdaCtT+AqP4MaxEzu7cCrYmBPbzQ==", + "dev": true, + "dependencies": { + "tldts-core": "^6.1.65" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.65", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.65.tgz", + "integrity": "sha512-Uq5t0N0Oj4nQSbU8wFN1YYENvMthvwU13MQrMJRspYCGLSAZjAfoBOJki5IQpnBM/WFskxxC/gIOTwaedmHaSg==", + "dev": true + }, + "node_modules/tmp": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", + "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==", + "dev": true, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toggle-selection": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz", + "integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==" + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/tough-cookie": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.0.0.tgz", + "integrity": "sha512-FRKsF7cz96xIIeMZ82ehjC3xW2E+O2+v11udrDYewUbszngYhsGa8z6YUMMzO9QJZzzyd0nGGXnML/TReX6W8Q==", + "dev": true, + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/ts-api-utils": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", + "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", + "dev": true, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "node_modules/ts-node": { + "version": "8.10.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-8.10.2.tgz", + "integrity": "sha512-ISJJGgkIpDdBhWVu3jufsWpK3Rzo7bdiIXJjQc0ynKxVOVcg2oIrf2H2cejminGrptVc6q6/uynAHNCuWGbpVA==", + "dev": true, + "dependencies": { + "arg": "^4.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "source-map-support": "^0.5.17", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "engines": { + "node": ">=6.0.0" + }, + "peerDependencies": { + "typescript": ">=2.7" + } + }, + "node_modules/tsconfig-paths": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", + "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", + "dev": true, + "dependencies": { + "json5": "^2.2.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + }, + "node_modules/tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "dependencies": { + "tslib": "^1.8.1" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" + } + }, + "node_modules/tsutils-etc": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/tsutils-etc/-/tsutils-etc-1.4.2.tgz", + "integrity": "sha512-2Dn5SxTDOu6YWDNKcx1xu2YUy6PUeKrWZB/x2cQ8vY2+iz3JRembKn/iZ0JLT1ZudGNwQQvtFX9AwvRHbXuPUg==", + "dev": true, + "dependencies": { + "@types/yargs": "^17.0.0", + "yargs": "^17.0.0" + }, + "bin": { + "ts-flags": "bin/ts-flags", + "ts-kind": "bin/ts-kind" + }, + "peerDependencies": { + "tsutils": "^3.0.0", + "typescript": ">=4.0.0" + } + }, + "node_modules/tsutils/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "node_modules/tuf-js": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tuf-js/-/tuf-js-2.2.1.tgz", + "integrity": "sha512-GwIJau9XaA8nLVbUXsN3IlFi7WmQ48gBUrl3FTkkL/XLu/POhBzfmX9hd33FNMX1qAsfl6ozO1iMmW9NC8YniA==", + "dev": true, + "dependencies": { + "@tufjs/models": "2.0.1", + "debug": "^4.3.4", + "make-fetch-happen": "^13.0.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", + "dev": true + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz", + "integrity": "sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz", + "integrity": "sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz", + "integrity": "sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==", + "dev": true, + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.6.tgz", + "integrity": "sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-assert": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/typed-assert/-/typed-assert-1.0.9.tgz", + "integrity": "sha512-KNNZtayBCtmnNmbo5mG47p1XsCyrx6iVqomjcZnec/1Y5GGARaxPs6r49RnSPeUP3YjNYiU9sQHAtY4BBvnZwg==", + "dev": true + }, + "node_modules/typescript": { + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", + "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-compare": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/typescript-compare/-/typescript-compare-0.0.2.tgz", + "integrity": "sha512-8ja4j7pMHkfLJQO2/8tut7ub+J3Lw2S3061eJLFQcvs3tsmJKp8KG5NtpLn7KcY2w08edF74BSVN7qJS0U6oHA==", + "dependencies": { + "typescript-logic": "^0.0.0" + } + }, + "node_modules/typescript-logic": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/typescript-logic/-/typescript-logic-0.0.0.tgz", + "integrity": "sha512-zXFars5LUkI3zP492ls0VskH3TtdeHCqu0i7/duGt60i5IGPIpAHE/DWo5FqJ6EjQ15YKXrt+AETjv60Dat34Q==" + }, + "node_modules/typescript-tuple": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/typescript-tuple/-/typescript-tuple-2.2.1.tgz", + "integrity": "sha512-Zcr0lbt8z5ZdEzERHAMAniTiIKerFCMgd7yjq1fPnDJ43et/k9twIFQMUYff9k5oXcsQ0WpvFcgzK2ZKASoW6Q==", + "dependencies": { + "typescript-compare": "^0.0.2" + } + }, + "node_modules/ua-parser-js": { + "version": "1.0.38", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.38.tgz", + "integrity": "sha512-Aq5ppTOfvrCMgAPneW1HfWj66Xi7XL+/mIy996R1/CLS/rcyJQm6QZdsKrUeivDFQ+Oc9Wyuwor8Ze8peEoUoQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + }, + { + "type": "github", + "url": "https://github.com/sponsors/faisalman" + } + ], + "optional": true, + "peer": true, + "engines": { + "node": "*" + } + }, + "node_modules/uc.micro": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", + "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==" + }, + "node_modules/unbox-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", + "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", + "which-boxed-primitive": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true + }, + "node_modules/undici": { + "version": "6.11.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.11.1.tgz", + "integrity": "sha512-KyhzaLJnV1qa3BSHdj4AZ2ndqI0QWPxYzaIOio0WzcEJB9gvuysprJSLtpvc2D9mhR9jPDUk7xlJlZbH2KR5iw==", + "dev": true, + "engines": { + "node": ">=18.0" + } + }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true, + "optional": true, + "peer": true + }, + "node_modules/unfetch": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/unfetch/-/unfetch-4.2.0.tgz", + "integrity": "sha512-F9p7yYCn6cIW9El1zi0HI6vqpeIvBsr3dSuRO6Xuppb1u5rXpCPmMvLSyECLhybr9isec8Ohl0hPekMVrEinDA==" + }, + "node_modules/unicode-canonical-property-names-ecmascript": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", + "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "dev": true, + "dependencies": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-value-ecmascript": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.0.tgz", + "integrity": "sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-property-aliases-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", + "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/unique-filename": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", + "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", + "dev": true, + "dependencies": { + "unique-slug": "^2.0.0" + } + }, + "node_modules/unique-slug": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz", + "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", + "dev": true, + "dependencies": { + "imurmurhash": "^0.1.4" + } + }, + "node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/untildify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", + "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", + "integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.1.2", + "picocolors": "^1.0.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/uri-js/node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/use-memo-one": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-memo-one/-/use-memo-one-1.1.3.tgz", + "integrity": "sha512-g66/K7ZQGYrI6dy8GLpVcMsBp4s17xNkYJVSMvTEevGy3nDxHOfE6z8BVE22+5G5x7t3+bhzrlTDB7ObrEE0cQ==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/validate-npm-package-name": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-5.0.1.tgz", + "integrity": "sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "node_modules/void-elements": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz", + "integrity": "sha512-qZKX4RnBzH2ugr8Lxa7x+0V6XD9Sb/ouARtiasEQCHB1EVU4NXtmHsDDrx1dO4ne5fc3J6EW05BP1Dl0z0iung==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/warning": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", + "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, + "node_modules/watchpack": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", + "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", + "dev": true, + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/wbuf": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", + "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", + "dev": true, + "dependencies": { + "minimalistic-assert": "^1.0.0" + } + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dev": true, + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/webpack": { + "version": "5.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 0571d166bc5..3a78d5e05f7 100644 --- a/package.json +++ b/package.json @@ -1,27 +1,32 @@ { "name": "dspace-angular", - "version": "8.0.0-next", + "version": "9.0.0-next", "scripts": { "ng": "ng", "config:watch": "nodemon", "test:rest": "ts-node --project ./tsconfig.ts-node.json scripts/test-rest.ts", - "start": "yarn run start:prod", - "start:dev": "nodemon --exec \"cross-env NODE_ENV=development yarn run serve\"", - "start:prod": "yarn run build:prod && cross-env NODE_ENV=production yarn run serve:ssr", - "start:mirador:prod": "yarn run build:mirador && yarn run start:prod", - "preserve": "yarn base-href", + "start": "npm run start:prod", + "start:dev": "nodemon --exec \"cross-env NODE_ENV=development npm run serve\"", + "start:prod": "npm run build:prod && cross-env NODE_ENV=production npm run serve:ssr", + "start:mirador:prod": "npm run build:mirador && npm run start:prod", + "preserve": "npm run base-href", "serve": "ts-node --project ./tsconfig.ts-node.json scripts/serve.ts", "serve:ssr": "node dist/server/main", "analyze": "webpack-bundle-analyzer dist/browser/stats.json", "build": "ng build --configuration development", "build:stats": "ng build --stats-json", - "build:prod": "cross-env NODE_ENV=production yarn run build:ssr", + "build:prod": "cross-env NODE_ENV=production npm run build:ssr", "build:ssr": "ng build --configuration production && ng run dspace-angular:server:production", + "build:lint": "rimraf 'lint/dist/**/*.js' 'lint/dist/**/*.js.map' && tsc -b lint/tsconfig.json", "test": "ng test --source-map=true --watch=false --configuration test", "test:watch": "nodemon --exec \"ng test --source-map=true --watch=true --configuration test\"", "test:headless": "ng test --source-map=true --watch=false --configuration test --browsers=ChromeHeadless --code-coverage", - "lint": "ng lint", - "lint-fix": "ng lint --fix=true", + "test:lint": "npm run build:lint && npm run test:lint:nobuild", + "test:lint:nobuild": "jasmine --config=lint/jasmine.json", + "lint": "npm run build:lint && npm run lint:nobuild", + "lint:nobuild": "ng lint", + "lint-fix": "npm run build:lint && ng lint --fix=true", + "docs:lint": "ts-node --project ./lint/tsconfig.json ./lint/generate-docs.ts", "e2e": "cross-env NODE_ENV=production ng e2e", "clean:dev:config": "rimraf src/assets/config.json", "clean:coverage": "rimraf coverage", @@ -31,8 +36,8 @@ "clean:json": "rimraf *.records.json", "clean:node": "rimraf node_modules", "clean:cli": "rimraf .angular/cache", - "clean:prod": "yarn run clean:dist && yarn run clean:log && yarn run clean:doc && yarn run clean:coverage && yarn run clean:json", - "clean": "yarn run clean:prod && yarn run clean:dev:config && yarn run clean:cli && yarn run clean:node", + "clean:prod": "npm run clean:dist && npm run clean:log && npm run clean:doc && npm run clean:coverage && npm run clean:json", + "clean": "npm run clean:prod && npm run clean:dev:config && npm run clean:cli && npm run clean:node", "sync-i18n": "ts-node --project ./tsconfig.ts-node.json scripts/sync-i18n-files.ts", "build:mirador": "webpack --config webpack/webpack.mirador.config.ts", "merge-i18n": "ts-node --project ./tsconfig.ts-node.json scripts/merge-i18n-files.ts", @@ -40,7 +45,8 @@ "cypress:run": "cypress run", "env:yaml": "ts-node --project ./tsconfig.ts-node.json scripts/env-to-yaml.ts", "base-href": "ts-node --project ./tsconfig.ts-node.json scripts/base-href.ts", - "check-circ-deps": "npx madge --exclude '(bitstream|bundle|collection|config-submission-form|eperson|item|version)\\.model\\.ts$' --circular --extensions ts ./" + "check-circ-deps": "npx madge --exclude '(bitstream|bundle|collection|config-submission-form|eperson|item|version)\\.model\\.ts$' --circular --extensions ts ./", + "postinstall": "npm run build:lint || echo 'Skipped DSpace ESLint plugins.'" }, "browser": { "fs": false, @@ -49,28 +55,61 @@ "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": "^17.3.4", - "@angular/cdk": "^17.3.4", - "@angular/common": "^17.3.4", - "@angular/compiler": "^17.3.4", - "@angular/core": "^17.3.4", - "@angular/forms": "^17.3.4", - "@angular/localize": "17.3.4", - "@angular/platform-browser": "^17.3.4", - "@angular/platform-browser-dynamic": "^17.3.4", - "@angular/platform-server": "^17.3.4", - "@angular/router": "^17.3.4", - "@angular/ssr": "^17.3.0", - "@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": "^16.0.0", "@ng-dynamic-forms/ui-ng-bootstrap": "^16.0.0", @@ -79,131 +118,124 @@ "@ngrx/store": "^17.1.1", "@ngx-translate/core": "^14.0.0", "@nicky-lenaers/ngx-scroll-to": "^14.0.0", - "@types/grecaptcha": "^3.0.4", - "angular-idle-preload": "3.0.0", "angulartics2": "^12.2.0", - "axios": "^1.6.0", + "axios": "^1.7.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.39.0", "date-fns": "^2.29.3", "date-fns-tz": "^1.3.7", "deepmerge": "^4.3.1", - "ejs": "^3.1.9", - "express": "^4.19.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", + "http-proxy-middleware": "^2.0.7", "http-terminator": "^3.2.0", - "isbot": "^3.6.10", + "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", "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": "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", - "pem": "1.14.7", - "prop-types": "^15.8.1", - "react-copy-to-clipboard": "^5.1.0", - "reflect-metadata": "^0.1.13", + "orejime": "^2.3.0", + "pem": "1.14.8", + "reflect-metadata": "^0.2.2", "rxjs": "^7.8.0", - "sanitize-html": "^2.12.1", - "sortablejs": "1.15.0", "uuid": "^8.3.2", - "webfontloader": "1.6.28", - "zone.js": "~0.14.4" + "zone.js": "~0.14.10" }, "devDependencies": { - "@angular-builders/custom-webpack": "~17.0.1", - "@angular-devkit/build-angular": "^17.3.0", - "@angular-eslint/builder": "17.2.1", - "@angular-eslint/eslint-plugin": "17.2.1", - "@angular-eslint/eslint-plugin-template": "17.2.1", - "@angular-eslint/schematics": "17.2.1", - "@angular-eslint/template-parser": "17.2.1", - "@angular/cli": "^17.3.0", - "@angular/compiler-cli": "^17.3.4", - "@angular/language-service": "^17.3.4", + "@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", + "@fortawesome/fontawesome-free": "^6.6.0", "@ngrx/store-devtools": "^17.1.1", - "@ngtools/webpack": "^16.2.12", - "@types/deep-freeze": "0.1.2", + "@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.2", - "browser-sync": "^3.0.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.17.4", - "cypress-axe": "^1.4.0", + "cypress": "^13.16.0", + "cypress-axe": "^1.5.0", "deep-freeze": "0.0.1", "eslint": "^8.39.0", "eslint-plugin-deprecation": "^1.4.1", - "eslint-plugin-import": "^2.27.5", + "eslint-plugin-dspace-angular-html": "file:./lint/dist/src/rules/html", + "eslint-plugin-dspace-angular-ts": "file:./lint/dist/src/rules/ts", + "eslint-plugin-import": "^2.31.0", "eslint-plugin-import-newlines": "^1.3.1", "eslint-plugin-jsdoc": "^45.0.0", "eslint-plugin-jsonc": "^2.6.0", "eslint-plugin-lodash": "^7.4.0", "eslint-plugin-rxjs": "^5.0.3", "eslint-plugin-simple-import-sort": "^10.0.0", - "eslint-plugin-unused-imports": "^2.0.0", - "express-static-gzip": "^2.1.7", + "eslint-plugin-unused-imports": "^3.2.0", + "express-static-gzip": "^2.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", + "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": "~5.3.3", - "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" } -} \ No newline at end of file +} 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 22f34232874..07a05656d87 100644 --- a/server.ts +++ b/server.ts @@ -27,7 +27,7 @@ import * as expressStaticGzip from 'express-static-gzip'; /* eslint-enable import/no-namespace */ import axios from 'axios'; import LRU from 'lru-cache'; -import isbot from 'isbot'; +import { isbot } from 'isbot'; import { createCertificate } from 'pem'; import { createServer } from 'https'; import { json } from 'body-parser'; @@ -99,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(); @@ -428,7 +428,7 @@ 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. + // This re-render is performed behind the scenes to update cached copy for next user. serverSideRender(req, res, next, false); } } else { diff --git a/src/app/access-control/access-control-routes.ts b/src/app/access-control/access-control-routes.ts index a7cce461ef0..07b6f6c4ff4 100644 --- a/src/app/access-control/access-control-routes.ts +++ b/src/app/access-control/access-control-routes.ts @@ -1,16 +1,13 @@ import { AbstractControl } from '@angular/forms'; -import { - mapToCanActivate, - Route, -} from '@angular/router'; +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 { 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, @@ -20,7 +17,7 @@ import { EPeopleRegistryComponent } from './epeople-registry/epeople-registry.co 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 { groupPageGuard } from './group-registry/group-page.guard'; import { GroupsRegistryComponent } from './group-registry/groups-registry.component'; /** @@ -28,7 +25,7 @@ import { GroupsRegistryComponent } from './group-registry/groups-registry.compon */ export const ValidateEmailErrorStateMatcher: DynamicErrorMessagesMatcher = (control: AbstractControl, model: any, hasFocus: boolean) => { - return (control.touched && !hasFocus) || (control.errors?.emailTaken && hasFocus); + return ( control.touched && !hasFocus ) || ( control.errors?.emailTaken && hasFocus ); }; const providers = [ @@ -46,7 +43,7 @@ export const ROUTES: Route[] = [ }, providers, data: { title: 'admin.access-control.epeople.title', breadcrumbKey: 'admin.access-control.epeople' }, - canActivate: mapToCanActivate([SiteAdministratorGuard]), + canActivate: [siteAdministratorGuard], }, { path: `${EPERSON_PATH}/create`, @@ -56,7 +53,7 @@ export const ROUTES: Route[] = [ }, providers, data: { title: 'admin.access-control.epeople.add.title', breadcrumbKey: 'admin.access-control.epeople.add' }, - canActivate: mapToCanActivate([SiteAdministratorGuard]), + canActivate: [siteAdministratorGuard], }, { path: `${EPERSON_PATH}/:id/edit`, @@ -67,7 +64,7 @@ export const ROUTES: Route[] = [ }, providers, data: { title: 'admin.access-control.epeople.edit.title', breadcrumbKey: 'admin.access-control.epeople.edit' }, - canActivate: mapToCanActivate([SiteAdministratorGuard]), + canActivate: [siteAdministratorGuard], }, { path: GROUP_PATH, @@ -77,7 +74,7 @@ export const ROUTES: Route[] = [ }, providers, data: { title: 'admin.access-control.groups.title', breadcrumbKey: 'admin.access-control.groups' }, - canActivate: mapToCanActivate([GroupAdministratorGuard]), + canActivate: [groupAdministratorGuard], }, { path: `${GROUP_PATH}/create`, @@ -90,7 +87,7 @@ export const ROUTES: Route[] = [ title: 'admin.access-control.groups.title.addGroup', breadcrumbKey: 'admin.access-control.groups.addGroup', }, - canActivate: mapToCanActivate([GroupAdministratorGuard]), + canActivate: [groupAdministratorGuard], }, { path: `${GROUP_PATH}/:groupId/edit`, @@ -103,7 +100,7 @@ export const ROUTES: Route[] = [ title: 'admin.access-control.groups.title.singleGroup', breadcrumbKey: 'admin.access-control.groups.singleGroup', }, - canActivate: mapToCanActivate([GroupPageGuard]), + canActivate: [groupPageGuard], }, { path: 'bulk-access', @@ -112,6 +109,6 @@ export const ROUTES: Route[] = [ breadcrumb: i18nBreadcrumbResolver, }, data: { title: 'admin.access-control.bulk-access.title', breadcrumbKey: 'admin.access-control.bulk-access' }, - canActivate: mapToCanActivate([SiteAdministratorGuard]), + canActivate: [siteAdministratorGuard], }, ]; 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 131cb49d6be..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 @@ -23,10 +23,10 @@ {{'admin.access-control.bulk-access-browse.search.header' | translate}}
- + [showThumbnails]="false">
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 641615d1825..b5a26533cfa 100644 --- a/src/app/access-control/epeople-registry/epeople-registry.component.html +++ b/src/app/access-control/epeople-registry/epeople-registry.component.html @@ -41,7 +41,7 @@ {{ dsoNameService.getName(group) }} - {{ dsoNameService.getName(undefined) }} + + {{ dsoNameService.getName((group.object | async)?.payload) }} + diff --git a/src/app/access-control/epeople-registry/eperson-form/eperson-form.component.spec.ts b/src/app/access-control/epeople-registry/eperson-form/eperson-form.component.spec.ts index 7c03b9eab0a..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,5 +1,5 @@ import { CommonModule } from '@angular/common'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; import { ComponentFixture, TestBed, @@ -19,12 +19,10 @@ import { import { ActivatedRoute, Router, + RouterModule, } from '@angular/router'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; -import { - TranslateLoader, - TranslateModule, -} from '@ngx-translate/core'; +import { TranslateModule } from '@ngx-translate/core'; import { Observable, of as observableOf, @@ -49,7 +47,6 @@ import { FormBuilderService } from '../../../shared/form/builder/form-builder.se import { FormComponent } from '../../../shared/form/form.component'; import { ThemedLoadingComponent } from '../../../shared/loading/themed-loading.component'; import { getMockFormBuilderService } from '../../../shared/mocks/form-builder-service.mock'; -import { TranslateLoaderMock } from '../../../shared/mocks/translate-loader.mock'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { PaginationComponent } from '../../../shared/pagination/pagination.component'; import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; @@ -92,9 +89,6 @@ describe('EPersonFormComponent', () => { ePersonDataServiceStub = { activeEPerson: null, allEpeople: mockEPeople, - getEPeople(): Observable>> { - return createSuccessfulRemoteDataObject$(buildPaginatedList(null, this.allEpeople)); - }, getActiveEPerson(): Observable { return observableOf(this.activeEPerson); }, @@ -228,12 +222,8 @@ describe('EPersonFormComponent', () => { router = new RouterStub(); TestBed.configureTestingModule({ imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule, - TranslateModule.forRoot({ - loader: { - provide: TranslateLoader, - useClass: TranslateLoaderMock, - }, - }), + RouterModule.forRoot([]), + TranslateModule.forRoot(), EPersonFormComponent, HasNoValuePipe, ], @@ -251,7 +241,7 @@ describe('EPersonFormComponent', () => { { provide: Router, useValue: router }, EPeopleRegistryComponent, ], - schemas: [NO_ERRORS_SCHEMA], + schemas: [CUSTOM_ELEMENTS_SCHEMA], }) .overrideComponent(EPersonFormComponent, { remove: { imports: [ ThemedLoadingComponent, PaginationComponent,FormComponent] }, @@ -274,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; @@ -378,15 +344,13 @@ describe('EPersonFormComponent', () => { expect(component.formGroup.controls.email.errors.emailTaken).toBeTruthy(); }); }); - - - }); + describe('when submitting the form', () => { let firstName; let lastName; let email; - let canLogIn; + let canLogIn: boolean; let requireCertificate; let expected; @@ -415,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; @@ -454,9 +419,17 @@ describe('EPersonFormComponent', () => { email: email, canLogIn: canLogIn, requireCertificate: requireCertificate, - _links: undefined, + _links: { + groups: { + href: '', + }, + self: { + href: '', + }, + }, }); spyOn(ePersonDataServiceStub, 'getActiveEPerson').and.returnValue(observableOf(expectedWithId)); + component.ngOnInit(); component.onSubmit(); fixture.detectChanges(); }); @@ -504,22 +477,19 @@ describe('EPersonFormComponent', () => { }); describe('delete', () => { - - let ePersonId; let eperson: EPerson; let modalService; beforeEach(() => { spyOn(authService, 'impersonate').and.callThrough(); - ePersonId = 'testEPersonId'; eperson = EPersonMock; component.epersonInitial = eperson; component.canDelete$ = observableOf(true); spyOn(component.epersonService, 'getActiveEPerson').and.returnValue(observableOf(eperson)); modalService = (component as any).modalService; spyOn(modalService, 'open').and.returnValue(Object.assign({ componentInstance: Object.assign({ response: observableOf(true) }) })); + component.ngOnInit(); fixture.detectChanges(); - }); it('the delete button should be visible if the ePerson can be deleted', () => { diff --git a/src/app/access-control/epeople-registry/eperson-form/eperson-form.component.ts b/src/app/access-control/epeople-registry/eperson-form/eperson-form.component.ts index 05efde7cf7d..942481c4fd9 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 @@ -189,6 +189,11 @@ export class EPersonFormComponent implements OnInit, OnDestroy { */ canImpersonate$: Observable; + /** + * The current {@link EPerson} + */ + activeEPerson$: Observable; + /** * List of subscriptions */ @@ -254,7 +259,11 @@ export class EPersonFormComponent implements OnInit, OnDestroy { protected route: ActivatedRoute, protected router: Router, ) { - this.subs.push(this.epersonService.getActiveEPerson().subscribe((eperson: EPerson) => { + } + + ngOnInit() { + this.activeEPerson$ = this.epersonService.getActiveEPerson(); + this.subs.push(this.activeEPerson$.subscribe((eperson: EPerson) => { this.epersonInitial = eperson; if (hasValue(eperson)) { this.isImpersonated = this.authService.isImpersonatingUser(eperson.id); @@ -262,9 +271,6 @@ export class EPersonFormComponent implements OnInit, OnDestroy { this.submitLabel = 'form.submit'; } })); - } - - ngOnInit() { this.initialisePage(); } @@ -272,130 +278,121 @@ export class EPersonFormComponent implements OnInit, OnDestroy { * This method will initialise the page */ initialisePage() { - this.subs.push(this.epersonService.findById(this.route.snapshot.params.id).subscribe((ePersonRD: RemoteData) => { - this.epersonService.editEPerson(ePersonRD.payload); - })); - observableCombineLatest([ - this.translateService.get(`${this.messagePrefix}.firstName`), - this.translateService.get(`${this.messagePrefix}.lastName`), - this.translateService.get(`${this.messagePrefix}.email`), - this.translateService.get(`${this.messagePrefix}.canLogIn`), - this.translateService.get(`${this.messagePrefix}.requireCertificate`), - this.translateService.get(`${this.messagePrefix}.emailHint`), - ]).subscribe(([firstName, lastName, email, canLogIn, requireCertificate, emailHint]) => { - this.firstName = new DynamicInputModel({ - id: 'firstName', - label: firstName, - name: 'firstName', - validators: { - required: null, - }, - required: true, + 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.lastName = new DynamicInputModel({ - id: 'lastName', - label: lastName, - name: 'lastName', - validators: { - required: null, - }, - required: true, + this.requireCertificate = new DynamicCheckboxModel( + { + id: 'requireCertificate', + label: this.translateService.instant(`${this.messagePrefix}.requireCertificate`), + name: 'requireCertificate', + value: (this.epersonInitial != null ? this.epersonInitial.requireCertificate : false), }); - this.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.formModel = [ + this.firstName, + this.lastName, + this.email, + this.canLogIn, + this.requireCertificate, + ]; + this.formGroup = this.formBuilderService.createFormGroup(this.formModel); + this.subs.push(this.activeEPerson$.subscribe((eperson: EPerson) => { + if (eperson != null) { + this.groups$ = this.groupsDataService.findListByHref(eperson._links.groups.href, { + currentPage: 1, + elementsPerPage: this.config.pageSize, + }, undefined, undefined, followLink('object')); + } + this.formGroup.patchValue({ + firstName: eperson != null ? eperson.firstMetadataValue('eperson.firstname') : '', + lastName: eperson != null ? eperson.firstMetadataValue('eperson.lastname') : '', + email: eperson != null ? eperson.email : '', + canLogIn: eperson != null ? eperson.canLogIn : true, + requireCertificate: eperson != null ? eperson.requireCertificate : false, }); - this.canLogIn = new DynamicCheckboxModel( - { - id: 'canLogIn', - label: canLogIn, - name: 'canLogIn', - value: (this.epersonInitial != null ? this.epersonInitial.canLogIn : true), - }); - this.requireCertificate = new DynamicCheckboxModel( - { - id: 'requireCertificate', - label: requireCertificate, - name: 'requireCertificate', - value: (this.epersonInitial != null ? this.epersonInitial.requireCertificate : false), + + 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.groupsPageInfoState$ = this.groups$.pipe( - map(groupsRD => groupsRD.payload.pageInfo), - ); - - this.canImpersonate$ = activeEPerson$.pipe( - switchMap((eperson) => { - if (hasValue(eperson)) { - return this.authorizationService.isAuthorized(FeatureID.LoginOnBehalfOf, eperson.self); - } else { - return observableOf(false); - } - }), - ); - this.canDelete$ = activeEPerson$.pipe( - switchMap((eperson) => this.authorizationService.isAuthorized(FeatureID.CanDelete, hasValue(eperson) ? eperson.self : undefined)), - ); - this.canReset$ = observableOf(true); - }); + this.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); } /** @@ -414,7 +411,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy { * Emit the updated/created eperson using the EventEmitter submitForm */ onSubmit() { - this.epersonService.getActiveEPerson().pipe(take(1)).subscribe( + this.activeEPerson$.pipe(take(1)).subscribe( (ePerson: EPerson) => { const values = { metadata: { @@ -533,7 +530,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy { * It'll either show a success or error message depending on whether the delete was successful or not. */ delete(): void { - this.epersonService.getActiveEPerson().pipe( + this.activeEPerson$.pipe( take(1), switchMap((eperson: EPerson) => { const modalRef = this.modalService.open(ConfirmationModalComponent); @@ -637,7 +634,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy { * Update the list of groups by fetching it from the rest api or cache */ private updateGroups(options) { - this.subs.push(this.epersonService.getActiveEPerson().subscribe((eperson: EPerson) => { + this.subs.push(this.activeEPerson$.subscribe((eperson: EPerson) => { this.groups$ = this.groupsDataService.findListByHref(eperson._links.groups.href, options); })); } diff --git a/src/app/access-control/group-registry/group-form/group-form.component.html b/src/app/access-control/group-registry/group-form/group-form.component.html index dc477352e56..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,7 +2,7 @@
-
+

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

@@ -23,11 +23,15 @@

- - - + + + + + + +
-
+
-
- -
- - - - + +
+ +
+ +
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 02de06f4155..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,6 +1,6 @@ import { CommonModule } from '@angular/common'; import { HttpClient } from '@angular/common/http'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; import { ComponentFixture, TestBed, @@ -23,11 +23,7 @@ import { } from '@angular/router'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { Store } from '@ngrx/store'; -import { - TranslateLoader, - TranslateModule, - TranslateService, -} from '@ngx-translate/core'; +import { TranslateModule } from '@ngx-translate/core'; import { Operation } from 'fast-json-patch'; import { Observable, @@ -61,15 +57,14 @@ import { FormComponent } from '../../../shared/form/form.component'; import { DSONameServiceMock } from '../../../shared/mocks/dso-name.service.mock'; import { getMockFormBuilderService } from '../../../shared/mocks/form-builder-service.mock'; import { RouterMock } from '../../../shared/mocks/router.mock'; -import { getMockTranslateService } from '../../../shared/mocks/translate.service.mock'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; +import { ActivatedRouteStub } from '../../../shared/testing/active-router.stub'; import { GroupMock, GroupMock2, } from '../../../shared/testing/group-mock'; import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub'; -import { TranslateLoaderMock } from '../../../shared/testing/translate-loader.mock'; import { GroupFormComponent } from './group-form.component'; import { MembersListComponent } from './members-list/members-list.component'; import { SubgroupsListComponent } from './subgroup-list/subgroups-list.component'; @@ -78,19 +73,19 @@ import { ValidateGroupExists } from './validators/group-exists.validator'; describe('GroupFormComponent', () => { let component: GroupFormComponent; let fixture: ComponentFixture; - let translateService: TranslateService; let builderService: FormBuilderService; let ePersonDataServiceStub: any; let groupsDataServiceStub: any; let dsoDataServiceStub: any; let authorizationService: AuthorizationDataService; let notificationService: NotificationsServiceStub; - let router; + let router: RouterMock; + let route: ActivatedRouteStub; - let groups; - let groupName; - let groupDescription; - let expected; + let groups: Group[]; + let groupName: string; + let groupDescription: string; + let expected: Group; beforeEach(waitForAsync(() => { groups = [GroupMock, GroupMock2]; @@ -105,6 +100,15 @@ describe('GroupFormComponent', () => { }, ], }, + object: createSuccessfulRemoteDataObject$(undefined), + _links: { + self: { + href: 'group-selflink', + }, + object: { + href: 'group-objectlink', + }, + }, }); ePersonDataServiceStub = {}; groupsDataServiceStub = { @@ -141,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); }, @@ -223,17 +234,15 @@ describe('GroupFormComponent', () => { return typeof value === 'object' && value !== null; }, }); - translateService = getMockTranslateService(); router = new RouterMock(); + route = new ActivatedRouteStub(); notificationService = new NotificationsServiceStub(); + return TestBed.configureTestingModule({ imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule, - TranslateModule.forRoot({ - loader: { - provide: TranslateLoader, - useClass: TranslateLoaderMock, - }, - }), GroupFormComponent], + TranslateModule.forRoot(), + GroupFormComponent, + ], providers: [ { provide: DSONameService, useValue: new DSONameServiceMock() }, { provide: EPersonDataService, useValue: ePersonDataServiceStub }, @@ -249,14 +258,11 @@ describe('GroupFormComponent', () => { { 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], + schemas: [CUSTOM_ELEMENTS_SCHEMA], }) .overrideComponent(GroupFormComponent, { remove: { imports: [ @@ -279,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(() => { @@ -288,14 +294,22 @@ describe('GroupFormComponent', () => { fixture.detectChanges(); }); - it('should emit a new group using the correct values', (async () => { - await fixture.whenStable().then(() => { - expect(component.submitForm.emit).toHaveBeenCalledWith(expected); - }); + it('should emit a new group using the correct values', (() => { + expect(component.submitForm.emit).toHaveBeenCalledWith(jasmine.objectContaining({ + name: groupName, + metadata: { + 'dc.description': [ + { + value: groupDescription, + }, + ], + }, + })); })); }); + describe('with active Group', () => { - let expected2; + let expected2: Group; beforeEach(() => { expected2 = Object.assign(new Group(), { name: 'newGroupName', @@ -306,15 +320,24 @@ describe('GroupFormComponent', () => { }, ], }, + object: createSuccessfulRemoteDataObject$(undefined), + _links: { + self: { + href: 'group-selflink', + }, + object: { + href: 'group-objectlink', + }, + }, }); spyOn(groupsDataServiceStub, 'getActiveGroup').and.returnValue(observableOf(expected)); spyOn(groupsDataServiceStub, 'patch').and.returnValue(createSuccessfulRemoteDataObject$(expected2)); - component.groupName.value = 'newGroupName'; - component.onSubmit(); - fixture.detectChanges(); + component.ngOnInit(); }); it('should edit with name and description operations', () => { + component.groupName.setValue('newGroupName'); + component.onSubmit(); const operations = [{ op: 'add', path: '/metadata/dc.description', @@ -328,9 +351,8 @@ describe('GroupFormComponent', () => { }); it('should edit with description operations', () => { - component.groupName.value = null; + component.groupName.setValue(null); component.onSubmit(); - fixture.detectChanges(); const operations = [{ op: 'add', path: '/metadata/dc.description', @@ -340,9 +362,9 @@ describe('GroupFormComponent', () => { }); it('should edit with name operations', () => { - component.groupDescription.value = null; + component.groupName.setValue('newGroupName'); + component.groupDescription.setValue(null); component.onSubmit(); - fixture.detectChanges(); const operations = [{ op: 'replace', path: '/name', @@ -351,12 +373,13 @@ describe('GroupFormComponent', () => { expect(groupsDataServiceStub.patch).toHaveBeenCalledWith(expected, operations); }); - it('should emit the existing group using the correct new values', (async () => { - await fixture.whenStable().then(() => { - expect(component.submitForm.emit).toHaveBeenCalledWith(expected2); - }); - })); + it('should emit the existing group using the correct new values', () => { + component.onSubmit(); + expect(component.submitForm.emit).toHaveBeenCalledWith(expected2); + }); + it('should emit success notification', () => { + component.onSubmit(); expect(notificationService.success).toHaveBeenCalled(); }); }); @@ -371,11 +394,8 @@ describe('GroupFormComponent', () => { describe('check form validation', () => { - let groupCommunity; - beforeEach(() => { groupName = 'testName'; - groupCommunity = 'testgroupCommunity'; groupDescription = 'testgroupDescription'; expected = Object.assign(new Group(), { @@ -387,8 +407,17 @@ describe('GroupFormComponent', () => { }, ], }, + _links: { + self: { + href: 'group-selflink', + }, + object: { + href: 'group-objectlink', + }, + }, }); spyOn(component.submitForm, 'emit'); + spyOn(dsoDataServiceStub, 'findByHref').and.returnValue(observableOf(expected)); fixture.detectChanges(); component.initialisePage(); @@ -438,21 +467,20 @@ describe('GroupFormComponent', () => { }); describe('delete', () => { - let deleteButton; - - beforeEach(() => { - component.initialisePage(); + let deleteButton: HTMLButtonElement; - component.canEdit$ = observableOf(true); - component.groupBeingEdited = { + beforeEach(async () => { + spyOn(groupsDataServiceStub, 'delete').and.callThrough(); + component.activeGroup$ = observableOf({ + id: 'active-group', permanent: false, - } as Group; + } as Group); + component.canEdit$ = observableOf(true); + + component.initialisePage(); fixture.detectChanges(); deleteButton = fixture.debugElement.query(By.css('.delete-button')).nativeElement; - - spyOn(groupsDataServiceStub, 'delete').and.callThrough(); - spyOn(groupsDataServiceStub, 'getActiveGroup').and.returnValue(observableOf({ id: 'active-group' })); }); describe('if confirmed via modal', () => { diff --git a/src/app/access-control/group-registry/group-form/group-form.component.ts b/src/app/access-control/group-registry/group-form/group-form.component.ts index 9fcbcbfeff6..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 @@ -11,7 +11,10 @@ import { OnInit, Output, } from '@angular/core'; -import { UntypedFormGroup } from '@angular/forms'; +import { + AbstractControl, + UntypedFormGroup, +} from '@angular/forms'; import { ActivatedRoute, Router, @@ -31,13 +34,10 @@ import { Operation } from 'fast-json-patch'; import { combineLatest as observableCombineLatest, Observable, - of as observableOf, Subscription, } from 'rxjs'; import { - catchError, debounceTime, - filter, map, switchMap, take, @@ -53,7 +53,6 @@ import { FeatureID } from '../../../core/data/feature-authorization/feature-id'; import { PaginatedList } from '../../../core/data/paginated-list.model'; import { RemoteData } from '../../../core/data/remote-data'; import { RequestService } from '../../../core/data/request.service'; -import { EPersonDataService } from '../../../core/eperson/eperson-data.service'; import { GroupDataService } from '../../../core/eperson/group-data.service'; import { Group } from '../../../core/eperson/models/group.model'; import { Collection } from '../../../core/shared/collection.model'; @@ -61,9 +60,9 @@ import { Community } from '../../../core/shared/community.model'; import { DSpaceObject } from '../../../core/shared/dspace-object.model'; import { NoContent } from '../../../core/shared/NoContent.model'; import { + getAllCompletedRemoteData, getFirstCompletedRemoteData, getFirstSucceededRemoteData, - getFirstSucceededRemoteDataPayload, getRemoteDataPayload, } from '../../../core/shared/operators'; import { AlertComponent } from '../../../shared/alert/alert.component'; @@ -117,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 @@ -163,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 */ - groupBeingEdited: Group; + canEdit$: 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} */ - canEdit$: Observable; + activeGroup$: Observable; + + /** + * The current {@link Group}'s linked {@link Community}/{@link Collection} + */ + activeGroupLinkedDSO$: Observable; + + /** + * Link to the current {@link Group}'s {@link Community}/{@link Collection} edit role tab + */ + linkedEditRolesRoute$: Observable; /** * The AlertType enumeration - * @type {AlertType} */ - public AlertTypeEnum = AlertType; + public readonly AlertType = AlertType; /** * Subscription to email field value change @@ -186,126 +194,121 @@ export class GroupFormComponent implements OnInit, OnDestroy { constructor( public groupDataService: GroupDataService, - private ePersonDataService: EPersonDataService, - private dSpaceObjectDataService: DSpaceObjectDataService, - private formBuilderService: FormBuilderService, - private translateService: TranslateService, - private notificationsService: NotificationsService, - private route: ActivatedRoute, + protected dSpaceObjectDataService: DSpaceObjectDataService, + protected formBuilderService: FormBuilderService, + protected translateService: TranslateService, + protected notificationsService: NotificationsService, + protected route: ActivatedRoute, protected router: Router, - private authorizationService: AuthorizationDataService, - private modalService: NgbModal, + protected authorizationService: AuthorizationDataService, + protected modalService: NgbModal, public requestService: RequestService, protected changeDetectorRef: ChangeDetectorRef, public dsoNameService: DSONameService, ) { } - ngOnInit() { + ngOnInit(): void { + if (this.route.snapshot.params.groupId !== 'newGroup') { + this.setActiveGroup(this.route.snapshot.params.groupId); + } + this.activeGroup$ = this.groupDataService.getActiveGroup(); + this.activeGroupLinkedDSO$ = this.getActiveGroupLinkedDSO(); + this.linkedEditRolesRoute$ = this.getLinkedEditRolesRoute(); + this.canEdit$ = this.activeGroupLinkedDSO$.pipe( + switchMap((dso: DSpaceObject) => { + if (hasValue(dso)) { + return [false]; + } else { + return this.activeGroup$.pipe( + hasValueOperator(), + switchMap((group: Group) => this.authorizationService.isAuthorized(FeatureID.CanDelete, group.self)), + ); + } + }), + ); this.initialisePage(); } initialisePage() { - this.subs.push(this.route.params.subscribe((params) => { - if (params.groupId !== 'newGroup') { - this.setActiveGroup(params.groupId); - } - })); - this.canEdit$ = this.groupDataService.getActiveGroup().pipe( - hasValueOperator(), - switchMap((group: Group) => { - return observableCombineLatest([ - this.authorizationService.isAuthorized(FeatureID.CanDelete, isNotEmpty(group) ? group.self : undefined), - this.hasLinkedDSO(group), - ]).pipe( - map(([isAuthorized, hasLinkedDSO]: [boolean, boolean]) => isAuthorized && !hasLinkedDSO), - ); - }), - ); - 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) { - - // Disable group name exists validator - this.formGroup.controls.groupName.clearAsyncValidators(); - - this.groupBeingEdited = activeGroup; - - if (linkedObject?.name) { - if (!this.formGroup.controls.groupCommunity) { - this.formBuilderService.insertFormGroupControl(1, this.formGroup, this.formModel, this.groupCommunity); - this.formGroup.patchValue({ - groupName: activeGroup.name, - groupCommunity: linkedObject?.name ?? '', - groupDescription: activeGroup.firstMetadataValue('dc.description'), - }); - } - } else { - this.formModel = [ - this.groupName, - this.groupDescription, - ]; - this.formGroup.patchValue({ - groupName: activeGroup.name, - groupDescription: activeGroup.firstMetadataValue('dc.description'), - }); + this.subs.push( + observableCombineLatest([ + this.activeGroup$, + this.canEdit$, + this.activeGroupLinkedDSO$, + ]).subscribe(([activeGroup, canEdit, linkedObject]) => { + + if (activeGroup != null) { + + // Disable group name exists validator + this.formGroup.controls.groupName.clearAsyncValidators(); + + if (isNotEmpty(linkedObject?.name)) { + if (!this.formGroup.controls.groupCommunity) { + this.formBuilderService.insertFormGroupControl(1, this.formGroup, this.formModel, groupCommunityModel); + this.groupDescription = this.formGroup.get('groupCommunity'); } - 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(); + } + } + }), + ); } /** @@ -324,9 +327,9 @@ export class GroupFormComponent implements OnInit, OnDestroy { * Emit the updated/created eperson using the EventEmitter submitForm */ onSubmit() { - this.groupDataService.getActiveGroup().pipe(take(1)).subscribe( - (group: Group) => { - const values = { + this.activeGroup$.pipe(take(1)).subscribe((group: Group) => { + if (group === null) { + this.createNewGroup({ name: this.groupName.value, metadata: { 'dc.description': [ @@ -335,14 +338,11 @@ export class GroupFormComponent implements OnInit, OnDestroy { }, ], }, - }; - if (group === null) { - this.createNewGroup(values); - } else { - this.editGroup(group); - } - }, - ); + }); + } else { + this.editGroup(group); + } + }); } /** @@ -448,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')) @@ -467,7 +467,7 @@ export class GroupFormComponent implements OnInit, OnDestroy { * It'll either show a success or error message depending on whether the delete was successful or not. */ delete() { - this.groupDataService.getActiveGroup().pipe(take(1)).subscribe((group: Group) => { + this.activeGroup$.pipe(take(1)).subscribe((group: Group) => { const modalRef = this.modalService.open(ConfirmationModalComponent); modalRef.componentInstance.name = this.dsoNameService.getName(group); modalRef.componentInstance.headerLabel = this.messagePrefix + '.delete-group.modal.header'; @@ -511,52 +511,38 @@ export class GroupFormComponent implements OnInit, OnDestroy { } /** - * Check if group has a linked object (community or collection linked to a workflow group) - * @param group + * Get the active {@link Group}'s linked object if it has one ({@link Community} or {@link Collection} linked to a + * workflow group) */ - hasLinkedDSO(group: Group): Observable { - if (hasValue(group) && hasValue(group._links.object.href)) { - return this.getLinkedDSO(group).pipe( - map((rd: RemoteData) => { - return hasValue(rd) && hasValue(rd.payload); - }), - catchError(() => observableOf(false)), - ); - } - } - - /** - * Get group's linked object if it has one (community or collection linked to a workflow group) - * @param 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 1b0386dee2c..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 @@ -20,25 +20,33 @@

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

- - {{eperson.id}} + + {{epersonDTO.eperson.id}} - - {{ dsoNameService.getName(eperson) }} + + {{ dsoNameService.getName(epersonDTO.eperson) }} - {{messagePrefix + '.table.email' | translate}}: {{ eperson.email ? eperson.email : '-' }}
- {{messagePrefix + '.table.netid' | translate}}: {{ eperson.netid ? eperson.netid : '-' }} + {{messagePrefix + '.table.email' | translate}}: {{ epersonDTO.eperson.email ? epersonDTO.eperson.email : '-' }}
+ {{messagePrefix + '.table.netid' | translate}}: {{ epersonDTO.eperson.netid ? epersonDTO.eperson.netid : '-' }}
- +
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 9cc77275579..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 @@ -222,13 +222,13 @@ describe('MembersListComponent', () => { describe('if first delete button is pressed', () => { beforeEach(() => { + spyOn(component, 'search').and.callThrough(); const deleteButton: DebugElement = fixture.debugElement.query(By.css('#ePeopleMembersOfGroup tbody .fa-trash-alt')); deleteButton.nativeElement.click(); fixture.detectChanges(); }); - it('then no ePerson remains as a member of the active group.', () => { - const epersonsFound = fixture.debugElement.queryAll(By.css('#ePeopleMembersOfGroup tbody tr')); - expect(epersonsFound.length).toEqual(0); + it('should trigger the search to add the user back to the search table', () => { + expect(component.search).toHaveBeenCalled(); }); }); }); @@ -264,13 +264,13 @@ describe('MembersListComponent', () => { describe('if first add button is pressed', () => { beforeEach(() => { + spyOn(component, 'search').and.callThrough(); const addButton: DebugElement = fixture.debugElement.query(By.css('#epersonsSearch tbody .fa-plus')); addButton.nativeElement.click(); fixture.detectChanges(); }); - it('then all (two) ePersons are member of the active group. No non-members left', () => { - epersonsFound = fixture.debugElement.queryAll(By.css('#epersonsSearch tbody tr')); - expect(epersonsFound.length).toEqual(0); + it('should trigger the search to remove the user from the search table', () => { + expect(component.search).toHaveBeenCalled(); }); }); }); diff --git a/src/app/access-control/group-registry/group-form/members-list/members-list.component.ts b/src/app/access-control/group-registry/group-form/members-list/members-list.component.ts index 92f764fe98a..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 @@ -24,21 +24,29 @@ import { } from '@ngx-translate/core'; import { BehaviorSubject, + combineLatest as observableCombineLatest, Observable, + ObservedValueOf, + of as observableOf, Subscription, } from 'rxjs'; import { + defaultIfEmpty, map, switchMap, take, } from 'rxjs/operators'; import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service'; -import { PaginatedList } from '../../../../core/data/paginated-list.model'; +import { + buildPaginatedList, + PaginatedList, +} from '../../../../core/data/paginated-list.model'; import { RemoteData } from '../../../../core/data/remote-data'; import { EPersonDataService } from '../../../../core/eperson/eperson-data.service'; import { GroupDataService } from '../../../../core/eperson/group-data.service'; import { EPerson } from '../../../../core/eperson/models/eperson.model'; +import { EpersonDtoModel } from '../../../../core/eperson/models/eperson-dto.model'; import { Group } from '../../../../core/eperson/models/group.model'; import { PaginationService } from '../../../../core/pagination/pagination.service'; import { @@ -114,21 +122,21 @@ export interface EPersonListActionConfig { export class MembersListComponent implements OnInit, OnDestroy { @Input() - messagePrefix: string; + messagePrefix: string; @Input() - actionConfig: EPersonListActionConfig = { - add: { - css: 'btn-outline-primary', - disabled: false, - icon: 'fas fa-plus fa-fw', - }, - remove: { - css: 'btn-outline-danger', - disabled: false, - icon: 'fas fa-trash-alt fa-fw', - }, - }; + actionConfig: EPersonListActionConfig = { + add: { + css: 'btn-outline-primary', + disabled: false, + icon: 'fas fa-plus fa-fw', + }, + remove: { + css: 'btn-outline-danger', + disabled: false, + icon: 'fas fa-trash-alt fa-fw', + }, + }; /** * EPeople being displayed in search result, initially all members, after search result of search @@ -137,7 +145,7 @@ export class MembersListComponent implements OnInit, OnDestroy { /** * List of EPeople members of currently active group being edited */ - ePeopleMembersOfGroup: BehaviorSubject> = new BehaviorSubject>(undefined); + ePeopleMembersOfGroup: BehaviorSubject> = new BehaviorSubject(undefined); /** * Pagination config used to display the list of EPeople that are result of EPeople search @@ -226,10 +234,35 @@ export class MembersListComponent implements OnInit, OnDestroy { return rd; } }), - getRemoteDataPayload()) - .subscribe((paginatedListOfEPersons: PaginatedList) => { - this.ePeopleMembersOfGroup.next(paginatedListOfEPersons); - })); + switchMap((epersonListRD: RemoteData>) => { + const dtos$ = observableCombineLatest([...epersonListRD.payload.page.map((member: EPerson) => { + const dto$: Observable = observableCombineLatest( + this.isMemberOfGroup(member), (isMember: ObservedValueOf>) => { + const epersonDtoModel: EpersonDtoModel = new EpersonDtoModel(); + epersonDtoModel.eperson = member; + epersonDtoModel.ableToDelete = isMember; + return epersonDtoModel; + }); + return dto$; + })]); + return dtos$.pipe(defaultIfEmpty([]), map((dtos: EpersonDtoModel[]) => { + return buildPaginatedList(epersonListRD.payload.pageInfo, dtos); + })); + }), + ).subscribe((paginatedListOfDTOs: PaginatedList) => { + this.ePeopleMembersOfGroup.next(paginatedListOfDTOs); + }), + ); + } + + /** + * We always return true since this is only used by the top section (which represents all the users part of the group + * in {@link MembersListComponent}) + * + * @param possibleMember EPerson that is a possible member (being tested) of the group currently being edited + */ + isMemberOfGroup(possibleMember: EPerson): Observable { + return observableOf(true); } /** diff --git a/src/app/access-control/group-registry/group-form/subgroup-list/subgroups-list.component.ts b/src/app/access-control/group-registry/group-form/subgroup-list/subgroups-list.component.ts index fc25dd4fc99..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 @@ -80,7 +80,7 @@ enum SubKey { export class SubgroupsListComponent implements OnInit, OnDestroy { @Input() - messagePrefix: string; + messagePrefix: string; /** * Result of search groups, initially all groups diff --git a/src/app/access-control/group-registry/group-page.guard.spec.ts b/src/app/access-control/group-registry/group-page.guard.spec.ts index b1648f59ec9..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,14 +1,24 @@ +import { + TestBed, + waitForAsync, +} from '@angular/core/testing'; import { ActivatedRouteSnapshot, Router, + UrlTree, } from '@angular/router'; -import { of as observableOf } from 'rxjs'; +import { + Observable, + of as observableOf, +} from 'rxjs'; import { AuthService } from '../../core/auth/auth.service'; import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; import { FeatureID } from '../../core/data/feature-authorization/feature-id'; import { HALEndpointService } from '../../core/shared/hal-endpoint.service'; -import { GroupPageGuard } from './group-page.guard'; +import { groupPageGuard } from './group-page.guard'; + +jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000; // Increase timeout to 10 seconds describe('GroupPageGuard', () => { const groupsEndpointUrl = 'https://test.org/api/eperson/groups'; @@ -20,42 +30,54 @@ describe('GroupPageGuard', () => { }, } as unknown as ActivatedRouteSnapshot; - let guard: GroupPageGuard; let halEndpointService: HALEndpointService; let authorizationService: AuthorizationDataService; let router: Router; let authService: AuthService; - beforeEach(() => { + function init() { halEndpointService = jasmine.createSpyObj(['getEndpoint']); - (halEndpointService as any).getEndpoint.and.returnValue(observableOf(groupsEndpointUrl)); + ( halEndpointService as any ).getEndpoint.and.returnValue(observableOf(groupsEndpointUrl)); authorizationService = jasmine.createSpyObj(['isAuthorized']); // NOTE: value is set in beforeEach router = jasmine.createSpyObj(['parseUrl']); - (router as any).parseUrl.and.returnValue = {}; + ( router as any ).parseUrl.and.returnValue = {}; authService = jasmine.createSpyObj(['isAuthenticated']); - (authService as any).isAuthenticated.and.returnValue(observableOf(true)); + ( authService as any ).isAuthenticated.and.returnValue(observableOf(true)); - guard = new GroupPageGuard(halEndpointService, authorizationService, router, authService); - }); + TestBed.configureTestingModule({ + providers: [ + { provide: AuthorizationDataService, useValue: authorizationService }, + { provide: Router, useValue: router }, + { provide: AuthService, useValue: authService }, + { provide: HALEndpointService, useValue: halEndpointService }, + ], + }); + } + + beforeEach(waitForAsync(() => { + init(); + })); it('should be created', () => { - expect(guard).toBeTruthy(); + expect(groupPageGuard).toBeTruthy(); }); describe('canActivate', () => { describe('when the current user can manage the group', () => { beforeEach(() => { - (authorizationService as any).isAuthorized.and.returnValue(observableOf(true)); + ( authorizationService as any ).isAuthorized.and.returnValue(observableOf(true)); }); it('should return true', (done) => { - guard.canActivate( - routeSnapshotWithGroupId, { url: 'current-url' } as any, - ).subscribe((result) => { + const result$ = TestBed.runInInjectionContext(() => { + return groupPageGuard()(routeSnapshotWithGroupId, { url: 'current-url' } as any); + }) as Observable; + + result$.subscribe((result) => { expect(authorizationService.isAuthorized).toHaveBeenCalledWith( FeatureID.CanManageGroup, groupEndpointUrl, undefined, ); @@ -71,15 +93,18 @@ describe('GroupPageGuard', () => { }); it('should not return true', (done) => { - guard.canActivate( - routeSnapshotWithGroupId, { url: 'current-url' } as any, - ).subscribe((result) => { + const result$ = TestBed.runInInjectionContext(() => { + return groupPageGuard()(routeSnapshotWithGroupId, { url: 'current-url' } as any); + }) as Observable; + + result$.subscribe((result) => { expect(authorizationService.isAuthorized).toHaveBeenCalledWith( FeatureID.CanManageGroup, groupEndpointUrl, undefined, ); expect(result).not.toBeTrue(); done(); }); + }); }); }); diff --git a/src/app/access-control/group-registry/group-page.guard.ts b/src/app/access-control/group-registry/group-page.guard.ts index 928271887c8..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,7 +1,7 @@ -import { Injectable } from '@angular/core'; +import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, - Router, + CanActivateFn, RouterStateSnapshot, } from '@angular/router'; import { @@ -10,34 +10,29 @@ import { } from 'rxjs'; import { map } from 'rxjs/operators'; -import { AuthService } from '../../core/auth/auth.service'; -import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service'; -import { SomeFeatureAuthorizationGuard } from '../../core/data/feature-authorization/feature-authorization-guard/some-feature-authorization.guard'; +import { + someFeatureAuthorizationGuard, + StringGuardParamFn, +} from '../../core/data/feature-authorization/feature-authorization-guard/some-feature-authorization.guard'; import { FeatureID } from '../../core/data/feature-authorization/feature-id'; import { HALEndpointService } from '../../core/shared/hal-endpoint.service'; -@Injectable({ - providedIn: 'root', -}) -export class GroupPageGuard extends SomeFeatureAuthorizationGuard { - - protected groupsEndpoint = 'groups'; - - constructor(protected halEndpointService: HALEndpointService, - protected authorizationService: AuthorizationDataService, - protected router: Router, - protected authService: AuthService) { - super(authorizationService, router, authService); - } - - getFeatureIDs(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { - return observableOf([FeatureID.CanManageGroup]); - } +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/groups-registry.component.html b/src/app/access-control/group-registry/groups-registry.component.html index 201e1cfe248..c2d998d9541 100644 --- a/src/app/access-control/group-registry/groups-registry.component.html +++ b/src/app/access-control/group-registry/groups-registry.component.html @@ -33,7 +33,7 @@
- + class="btn btn-outline-secondary mr-2">{{ 'service.detail.delete.cancel' | translate }} - + +
diff --git a/src/app/admin/admin-registries/bitstream-formats/bitstream-formats.component.spec.ts b/src/app/admin/admin-registries/bitstream-formats/bitstream-formats.component.spec.ts index 9ee767cda05..c6d619b6118 100644 --- a/src/app/admin/admin-registries/bitstream-formats/bitstream-formats.component.spec.ts +++ b/src/app/admin/admin-registries/bitstream-formats/bitstream-formats.component.spec.ts @@ -1,38 +1,20 @@ -import { CommonModule } from '@angular/common'; -import { - Component, - NO_ERRORS_SCHEMA, -} from '@angular/core'; +import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; import { ComponentFixture, TestBed, waitForAsync, } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; -import { RouterTestingModule } from '@angular/router/testing'; -import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { RouterModule } from '@angular/router'; import { provideMockStore } from '@ngrx/store/testing'; import { TranslateModule } from '@ngx-translate/core'; -import { - cold, - getTestScheduler, - hot, -} from 'jasmine-marbles'; -import { - of as observableOf, - of, -} from 'rxjs'; -import { TestScheduler } from 'rxjs/testing'; +import { hot } from 'jasmine-marbles'; +import { of as observableOf } from 'rxjs'; -import { APP_DATA_SERVICES_MAP } from '../../../../config/app-config.interface'; import { BitstreamFormatDataService } from '../../../core/data/bitstream-format-data.service'; -import { ConfigurationDataService } from '../../../core/data/configuration-data.service'; -import { GroupDataService } from '../../../core/eperson/group-data.service'; import { PaginationService } from '../../../core/pagination/pagination.service'; import { BitstreamFormat } from '../../../core/shared/bitstream-format.model'; import { BitstreamFormatSupportLevel } from '../../../core/shared/bitstream-format-support-level'; -import { XSRFService } from '../../../core/xsrf/xsrf.service'; -import { HostWindowService } from '../../../shared/host-window.service'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { PaginationComponent } from '../../../shared/pagination/pagination.component'; import { @@ -41,7 +23,6 @@ import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$, } from '../../../shared/remote-data.utils'; -import { HostWindowServiceStub } from '../../../shared/testing/host-window-service.stub'; import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub'; import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub'; import { createPaginatedList } from '../../../shared/testing/utils.test'; @@ -52,7 +33,6 @@ describe('BitstreamFormatsComponent', () => { let comp: BitstreamFormatsComponent; let fixture: ComponentFixture; let bitstreamFormatService; - let scheduler: TestScheduler; let notificationsServiceStub; let paginationService; @@ -107,8 +87,6 @@ describe('BitstreamFormatsComponent', () => { const initAsync = () => { notificationsServiceStub = new NotificationsServiceStub(); - scheduler = getTestScheduler(); - bitstreamFormatService = jasmine.createSpyObj('bitstreamFormatService', { findAll: observableOf(mockFormatsRD), find: createSuccessfulRemoteDataObject$(mockFormatsList[0]), @@ -120,33 +98,28 @@ describe('BitstreamFormatsComponent', () => { clearBitStreamFormatRequests: observableOf('cleared'), }); - const groupDataService = jasmine.createSpyObj('groupsDataService', { - findListByHref: createSuccessfulRemoteDataObject$(createPaginatedList([])), - getGroupRegistryRouterLink: '', - getUUIDFromString: '', - }); - - - const configurationDataService = jasmine.createSpyObj('ConfigurationDataService', { - findByPropertyName: of({ payload: { value: 'test' } }), - }); - paginationService = new PaginationServiceStub(); TestBed.configureTestingModule({ - imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule, BitstreamFormatsComponent, PaginationComponent, EnumKeysPipe], + imports: [ + BitstreamFormatsComponent, + EnumKeysPipe, + RouterModule.forRoot([]), + TranslateModule.forRoot(), + ], providers: [ provideMockStore(), - { provide: APP_DATA_SERVICES_MAP, useValue: {} }, { provide: BitstreamFormatDataService, useValue: bitstreamFormatService }, - { provide: HostWindowService, useValue: new HostWindowServiceStub(0) }, { provide: NotificationsService, useValue: notificationsServiceStub }, { provide: PaginationService, useValue: paginationService }, - { provide: GroupDataService, useValue: groupDataService }, - { provide: ConfigurationDataService, useValue: configurationDataService }, - { provide: XSRFService, useValue: {} }, ], - schemas: [NO_ERRORS_SCHEMA], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + }).overrideComponent(BitstreamFormatsComponent, { + remove: { + imports: [ + PaginationComponent, + ], + }, }).compileComponents(); }; @@ -214,18 +187,18 @@ describe('BitstreamFormatsComponent', () => { describe('isSelected', () => { beforeEach(waitForAsync(initAsync)); beforeEach(initBeforeEach); - it('should return an observable of true if the provided bistream is in the list returned by the service', () => { - const result = comp.isSelected(bitstreamFormat1); - - expect(result).toBeObservable(cold('b', { b: true })); + it('should return an observable of true if the provided bitstream is in the list returned by the service', () => { + comp.selectedBitstreamFormatIDs().subscribe((selectedBitstreamFormatIDs: string[]) => { + expect(selectedBitstreamFormatIDs).toContain(bitstreamFormat1.id); + }); }); - it('should return an observable of false if the provided bistream is not in the list returned by the service', () => { + it('should return an observable of false if the provided bitstream is not in the list returned by the service', () => { const format = new BitstreamFormat(); format.uuid = 'new'; - const result = comp.isSelected(format); - - expect(result).toBeObservable(cold('b', { b: false })); + comp.selectedBitstreamFormatIDs().subscribe((selectedBitstreamFormatIDs: string[]) => { + expect(selectedBitstreamFormatIDs).not.toContain(format.id); + }); }); }); @@ -251,8 +224,6 @@ describe('BitstreamFormatsComponent', () => { beforeEach(waitForAsync(() => { notificationsServiceStub = new NotificationsServiceStub(); - scheduler = getTestScheduler(); - bitstreamFormatService = jasmine.createSpyObj('bitstreamFormatService', { findAll: observableOf(mockFormatsRD), find: createSuccessfulRemoteDataObject$(mockFormatsList[0]), @@ -264,37 +235,26 @@ describe('BitstreamFormatsComponent', () => { clearBitStreamFormatRequests: observableOf('cleared'), }); - const groupDataService = jasmine.createSpyObj('groupsDataService', { - findListByHref: createSuccessfulRemoteDataObject$(createPaginatedList([])), - getGroupRegistryRouterLink: '', - getUUIDFromString: '', - }); - - const configurationDataService = jasmine.createSpyObj('ConfigurationDataService', { - findByPropertyName: of({ payload: { value: 'test' } }), - }); - paginationService = new PaginationServiceStub(); TestBed.configureTestingModule({ imports: [ - CommonModule, - RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule, - BitstreamFormatsComponent, PaginationComponent, EnumKeysPipe], + BitstreamFormatsComponent, + EnumKeysPipe, + PaginationComponent, + RouterModule.forRoot([]), + TranslateModule.forRoot(), + ], providers: [ provideMockStore(), { provide: BitstreamFormatDataService, useValue: bitstreamFormatService }, - { provide: HostWindowService, useValue: new HostWindowServiceStub(0) }, { provide: NotificationsService, useValue: notificationsServiceStub }, { provide: PaginationService, useValue: paginationService }, - { provide: GroupDataService, useValue: groupDataService }, - { provide: ConfigurationDataService, useValue: configurationDataService }, ], - schemas: [NO_ERRORS_SCHEMA], + schemas: [CUSTOM_ELEMENTS_SCHEMA], }) .overrideComponent(BitstreamFormatsComponent, { remove: { imports: [PaginationComponent] }, - add: { imports: [TestPaginationComponent] }, }) .compileComponents(); }, @@ -321,8 +281,6 @@ describe('BitstreamFormatsComponent', () => { beforeEach(waitForAsync(() => { notificationsServiceStub = new NotificationsServiceStub(); - scheduler = getTestScheduler(); - bitstreamFormatService = jasmine.createSpyObj('bitstreamFormatService', { findAll: observableOf(mockFormatsRD), find: createSuccessfulRemoteDataObject$(mockFormatsList[0]), @@ -334,35 +292,24 @@ describe('BitstreamFormatsComponent', () => { clearBitStreamFormatRequests: observableOf('cleared'), }); - const groupDataService = jasmine.createSpyObj('groupsDataService', { - findListByHref: createSuccessfulRemoteDataObject$(createPaginatedList([])), - getGroupRegistryRouterLink: '', - getUUIDFromString: '', - }); - - const configurationDataService = jasmine.createSpyObj('ConfigurationDataService', { - findByPropertyName: of({ payload: { value: 'test' } }), - }); - paginationService = new PaginationServiceStub(); TestBed.configureTestingModule({ - imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule, BitstreamFormatsComponent, PaginationComponent, EnumKeysPipe], + imports: [ + BitstreamFormatsComponent, + EnumKeysPipe, + RouterModule.forRoot([]), + TranslateModule.forRoot(), + ], providers: [ - provideMockStore(), { provide: BitstreamFormatDataService, useValue: bitstreamFormatService }, - { provide: HostWindowService, useValue: new HostWindowServiceStub(0) }, { provide: NotificationsService, useValue: notificationsServiceStub }, { provide: PaginationService, useValue: paginationService }, - { provide: PaginationService, useValue: paginationService }, - { provide: GroupDataService, useValue: groupDataService }, - { provide: ConfigurationDataService, useValue: configurationDataService }, ], - schemas: [NO_ERRORS_SCHEMA], + schemas: [CUSTOM_ELEMENTS_SCHEMA], }) .overrideComponent(BitstreamFormatsComponent, { remove: { imports: [PaginationComponent] }, - add: { imports: [TestPaginationComponent] }, }) .compileComponents(); }, @@ -384,12 +331,3 @@ describe('BitstreamFormatsComponent', () => { }); }); }); - -@Component({ - exportAs: 'paginationComponent', - selector: 'ds-pagination', - template: ``, - standalone: true, -}) -export class TestPaginationComponent { -} diff --git a/src/app/admin/admin-registries/bitstream-formats/bitstream-formats.component.ts b/src/app/admin/admin-registries/bitstream-formats/bitstream-formats.component.ts index 72739507be3..74869670c59 100644 --- a/src/app/admin/admin-registries/bitstream-formats/bitstream-formats.component.ts +++ b/src/app/admin/admin-registries/bitstream-formats/bitstream-formats.component.ts @@ -8,18 +8,12 @@ import { OnDestroy, OnInit, } from '@angular/core'; -import { - Router, - RouterLink, -} from '@angular/router'; +import { RouterLink } from '@angular/router'; import { TranslateModule, TranslateService, } from '@ngx-translate/core'; -import { - combineLatest as observableCombineLatest, - Observable, -} from 'rxjs'; +import { Observable } from 'rxjs'; import { map, mergeMap, @@ -61,7 +55,12 @@ export class BitstreamFormatsComponent implements OnInit, OnDestroy { /** * A paginated list of bitstream formats to be shown on the page */ - bitstreamFormats: Observable>>; + bitstreamFormats$: Observable>>; + + /** + * The currently selected {@link BitstreamFormat} IDs + */ + selectedBitstreamFormatIDs$: Observable; /** * The current pagination configuration for the page @@ -74,7 +73,6 @@ export class BitstreamFormatsComponent implements OnInit, OnDestroy { }); constructor(private notificationsService: NotificationsService, - private router: Router, private translateService: TranslateService, private bitstreamFormatService: BitstreamFormatDataService, private paginationService: PaginationService, @@ -122,21 +120,18 @@ export class BitstreamFormatsComponent implements OnInit, OnDestroy { } /** - * Deselects all selecetd bitstream formats + * Deselects all selected bitstream formats */ deselectAll() { this.bitstreamFormatService.deselectAllBitstreamFormats(); } /** - * Checks whether a given bitstream format is selected in the list (checkbox) - * @param bitstreamFormat + * Returns the list of all the bitstream formats that are selected in the list (checkbox) */ - isSelected(bitstreamFormat: BitstreamFormat): Observable { + selectedBitstreamFormatIDs(): Observable { return this.bitstreamFormatService.getSelectedBitstreamFormats().pipe( - map((bitstreamFormats: BitstreamFormat[]) => { - return bitstreamFormats.find((selectedFormat) => selectedFormat.id === bitstreamFormat.id) != null; - }), + map((bitstreamFormats: BitstreamFormat[]) => bitstreamFormats.map((selectedFormat) => selectedFormat.id)), ); } @@ -160,27 +155,23 @@ export class BitstreamFormatsComponent implements OnInit, OnDestroy { const prefix = 'admin.registries.bitstream-formats.delete'; const suffix = success ? 'success' : 'failure'; - const messages = observableCombineLatest( - this.translateService.get(`${prefix}.${suffix}.head`), - this.translateService.get(`${prefix}.${suffix}.amount`, { amount: amount }), - ); - messages.subscribe(([head, content]) => { + const head: string = this.translateService.instant(`${prefix}.${suffix}.head`); + const content: string = this.translateService.instant(`${prefix}.${suffix}.amount`, { amount: amount }); - if (success) { - this.notificationsService.success(head, content); - } else { - this.notificationsService.error(head, content); - } - }); + if (success) { + this.notificationsService.success(head, content); + } else { + this.notificationsService.error(head, content); + } } ngOnInit(): void { - - this.bitstreamFormats = this.paginationService.getFindListOptions(this.pageConfig.id, this.pageConfig).pipe( + this.bitstreamFormats$ = this.paginationService.getFindListOptions(this.pageConfig.id, this.pageConfig).pipe( switchMap((findListOptions: FindListOptions) => { return this.bitstreamFormatService.findAll(findListOptions); }), ); + this.selectedBitstreamFormatIDs$ = this.selectedBitstreamFormatIDs(); } diff --git a/src/app/admin/admin-registries/bitstream-formats/format-form/format-form.component.ts b/src/app/admin/admin-registries/bitstream-formats/format-form/format-form.component.ts index 08889ce28be..37ae0d1dc0d 100644 --- a/src/app/admin/admin-registries/bitstream-formats/format-form/format-form.component.ts +++ b/src/app/admin/admin-registries/bitstream-formats/format-form/format-form.component.ts @@ -154,12 +154,12 @@ export class FormatFormComponent implements OnInit { (fieldModel: DynamicFormControlModel) => { if (fieldModel.name === 'extensions') { if (hasValue(this.bitstreamFormat.extensions)) { - const extenstions = this.bitstreamFormat.extensions; + const extensions = this.bitstreamFormat.extensions; const formArray = (fieldModel as DynamicFormArrayModel); - for (let i = 0; i < extenstions.length; i++) { + for (let i = 0; i < extensions.length; i++) { formArray.insertGroup(i).group[0] = new DynamicInputModel({ id: `extension-${i}`, - value: extenstions[i], + value: extensions[i], }, this.arrayInputElementLayout); } } @@ -172,7 +172,7 @@ export class FormatFormComponent implements OnInit { } /** - * Creates an updated bistream format based on the current values in the form + * Creates an updated bitstream format based on the current values in the form * Emits the updated bitstream format trouhg the updatedFormat emitter */ onSubmit() { diff --git a/src/app/admin/admin-registries/metadata-registry/metadata-registry.component.html b/src/app/admin/admin-registries/metadata-registry/metadata-registry.component.html index 63242b47954..8b3c6fe972e 100644 --- a/src/app/admin/admin-registries/metadata-registry/metadata-registry.component.html +++ b/src/app/admin/admin-registries/metadata-registry/metadata-registry.component.html @@ -27,14 +27,14 @@

{{'admin.registries.metadata.head' | + [ngClass]="{'table-primary' : (activeMetadataSchema$ | async)?.id === schema.id}"> {{schema.id}} diff --git a/src/app/admin/admin-registries/metadata-registry/metadata-registry.component.spec.ts b/src/app/admin/admin-registries/metadata-registry/metadata-registry.component.spec.ts index 8fe31a0fd47..4ee61c35d57 100644 --- a/src/app/admin/admin-registries/metadata-registry/metadata-registry.component.spec.ts +++ b/src/app/admin/admin-registries/metadata-registry/metadata-registry.component.spec.ts @@ -17,9 +17,7 @@ import { TranslateModule } from '@ngx-translate/core'; import { of as observableOf } from 'rxjs'; import { FormBuilderService } from 'src/app/shared/form/builder/form-builder.service'; -import { RestResponse } from '../../../core/cache/response.models'; import { ConfigurationDataService } from '../../../core/data/configuration-data.service'; -import { buildPaginatedList } from '../../../core/data/paginated-list.model'; import { GroupDataService } from '../../../core/eperson/group-data.service'; import { MetadataSchema } from '../../../core/metadata/metadata-schema.model'; import { PaginationService } from '../../../core/pagination/pagination.service'; @@ -36,7 +34,9 @@ import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.u import { HostWindowServiceStub } from '../../../shared/testing/host-window-service.stub'; import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub'; import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub'; +import { RegistryServiceStub } from '../../../shared/testing/registry.service.stub'; import { SearchConfigurationServiceStub } from '../../../shared/testing/search-configuration-service.stub'; +import { createPaginatedList } from '../../../shared/testing/utils.test'; import { EnumKeysPipe } from '../../../shared/utils/enum-keys-pipe'; import { MetadataRegistryComponent } from './metadata-registry.component'; import { MetadataSchemaFormComponent } from './metadata-schema-form/metadata-schema-form.component'; @@ -44,9 +44,11 @@ import { MetadataSchemaFormComponent } from './metadata-schema-form/metadata-sch describe('MetadataRegistryComponent', () => { let comp: MetadataRegistryComponent; let fixture: ComponentFixture; - let registryService: RegistryService; - let paginationService; - const mockSchemasList = [ + + let paginationService: PaginationServiceStub; + let registryService: RegistryServiceStub; + + const mockSchemasList: MetadataSchema[] = [ { id: 1, _links: { @@ -67,25 +69,7 @@ describe('MetadataRegistryComponent', () => { prefix: 'mock', namespace: 'http://dspace.org/mockschema', }, - ]; - const mockSchemas = createSuccessfulRemoteDataObject$(buildPaginatedList(null, mockSchemasList)); - /* eslint-disable no-empty,@typescript-eslint/no-empty-function */ - const registryServiceStub = { - getMetadataSchemas: () => mockSchemas, - getActiveMetadataSchema: () => observableOf(undefined), - getSelectedMetadataSchemas: () => observableOf([]), - editMetadataSchema: (schema) => { - }, - cancelEditMetadataSchema: () => { - }, - deleteMetadataSchema: () => observableOf(new RestResponse(true, 200, 'OK')), - deselectAllMetadataSchema: () => { - }, - clearMetadataSchemaRequests: () => observableOf(undefined), - }; - /* eslint-enable no-empty, @typescript-eslint/no-empty-function */ - - paginationService = new PaginationServiceStub(); + ] as MetadataSchema[]; const configurationDataService = jasmine.createSpyObj('configurationDataService', { findByPropertyName: createSuccessfulRemoteDataObject$(Object.assign(new ConfigurationProperty(), { @@ -109,6 +93,10 @@ describe('MetadataRegistryComponent', () => { ); beforeEach(waitForAsync(() => { + paginationService = new PaginationServiceStub(); + registryService = new RegistryServiceStub(); + spyOn(registryService, 'getMetadataSchemas').and.returnValue(createSuccessfulRemoteDataObject$(createPaginatedList(mockSchemasList))); + TestBed.configureTestingModule({ imports: [ CommonModule, @@ -120,7 +108,7 @@ describe('MetadataRegistryComponent', () => { EnumKeysPipe, ], providers: [ - { provide: RegistryService, useValue: registryServiceStub }, + { provide: RegistryService, useValue: registryService }, { provide: HostWindowService, useValue: new HostWindowServiceStub(0) }, { provide: PaginationService, useValue: paginationService }, { @@ -190,7 +178,7 @@ describe('MetadataRegistryComponent', () => { })); it('should cancel editing the selected schema when clicked again', waitForAsync(() => { - spyOn(registryService, 'getActiveMetadataSchema').and.returnValue(observableOf(mockSchemasList[0] as MetadataSchema)); + comp.activeMetadataSchema$ = observableOf(mockSchemasList[0] as MetadataSchema); spyOn(registryService, 'cancelEditMetadataSchema'); row.click(); fixture.detectChanges(); @@ -205,7 +193,7 @@ describe('MetadataRegistryComponent', () => { beforeEach(() => { spyOn(registryService, 'deleteMetadataSchema').and.callThrough(); - spyOn(registryService, 'getSelectedMetadataSchemas').and.returnValue(observableOf(selectedSchemas as MetadataSchema[])); + comp.selectedMetadataSchemaIDs$ = observableOf(selectedSchemas.map((selectedSchema: MetadataSchema) => selectedSchema.id)); comp.deleteSchemas(); fixture.detectChanges(); }); diff --git a/src/app/admin/admin-registries/metadata-registry/metadata-registry.component.ts b/src/app/admin/admin-registries/metadata-registry/metadata-registry.component.ts index a148559bd2b..be1239ab95f 100644 --- a/src/app/admin/admin-registries/metadata-registry/metadata-registry.component.ts +++ b/src/app/admin/admin-registries/metadata-registry/metadata-registry.component.ts @@ -4,19 +4,20 @@ import { NgForOf, NgIf, } from '@angular/common'; -import { Component } from '@angular/core'; import { - Router, - RouterLink, -} from '@angular/router'; + Component, + OnDestroy, + OnInit, +} from '@angular/core'; +import { RouterLink } from '@angular/router'; import { TranslateModule, TranslateService, } from '@ngx-translate/core'; import { BehaviorSubject, - combineLatest as observableCombineLatest, Observable, + Subscription, zip, } from 'rxjs'; import { @@ -33,7 +34,6 @@ import { PaginationService } from '../../../core/pagination/pagination.service'; import { RegistryService } from '../../../core/registry/registry.service'; import { NoContent } from '../../../core/shared/NoContent.model'; import { getFirstCompletedRemoteData } from '../../../core/shared/operators'; -import { hasValue } from '../../../shared/empty.util'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { PaginationComponent } from '../../../shared/pagination/pagination.component'; import { toFindListOptions } from '../../../shared/pagination/pagination.utils'; @@ -60,13 +60,23 @@ import { MetadataSchemaFormComponent } from './metadata-schema-form/metadata-sch * A component used for managing all existing metadata schemas within the repository. * The admin can create, edit or delete metadata schemas here. */ -export class MetadataRegistryComponent { +export class MetadataRegistryComponent implements OnDestroy, OnInit { /** * A list of all the current metadata schemas within the repository */ metadataSchemas: Observable>>; + /** + * The {@link MetadataSchema}that is being edited + */ + activeMetadataSchema$: Observable; + + /** + * The selected {@link MetadataSchema} IDs + */ + selectedMetadataSchemaIDs$: Observable; + /** * Pagination config used to display the list of metadata schemas */ @@ -76,15 +86,25 @@ export class MetadataRegistryComponent { }); /** - * Whether or not the list of MetadataSchemas needs an update + * Whether the list of MetadataSchemas needs an update */ needsUpdate$: BehaviorSubject = new BehaviorSubject(true); - constructor(private registryService: RegistryService, - private notificationsService: NotificationsService, - private router: Router, - private paginationService: PaginationService, - private translateService: TranslateService) { + subscriptions: Subscription[] = []; + + constructor( + protected registryService: RegistryService, + protected notificationsService: NotificationsService, + protected paginationService: PaginationService, + protected translateService: TranslateService, + ) { + } + + ngOnInit(): void { + this.activeMetadataSchema$ = this.registryService.getActiveMetadataSchema(); + this.selectedMetadataSchemaIDs$ = this.registryService.getSelectedMetadataSchemas().pipe( + map((schemas: MetadataSchema[]) => schemas.map((schema: MetadataSchema) => schema.id)), + ); this.updateSchemas(); } @@ -113,30 +133,13 @@ export class MetadataRegistryComponent { * @param schema */ editSchema(schema: MetadataSchema) { - this.getActiveSchema().pipe(take(1)).subscribe((activeSchema) => { + this.subscriptions.push(this.activeMetadataSchema$.pipe(take(1)).subscribe((activeSchema: MetadataSchema) => { if (schema === activeSchema) { this.registryService.cancelEditMetadataSchema(); } else { this.registryService.editMetadataSchema(schema); } - }); - } - - /** - * Checks whether the given metadata schema is active (being edited) - * @param schema - */ - isActive(schema: MetadataSchema): Observable { - return this.getActiveSchema().pipe( - map((activeSchema) => schema === activeSchema), - ); - } - - /** - * Gets the active metadata schema (being edited) - */ - getActiveSchema(): Observable { - return this.registryService.getActiveMetadataSchema(); + })); } /** @@ -150,42 +153,25 @@ export class MetadataRegistryComponent { this.registryService.deselectMetadataSchema(schema); } - /** - * Checks whether a given metadata schema is selected in the list (checkbox) - * @param schema - */ - isSelected(schema: MetadataSchema): Observable { - return this.registryService.getSelectedMetadataSchemas().pipe( - map((schemas) => schemas.find((selectedSchema) => selectedSchema === schema) != null), - ); - } - /** * Delete all the selected metadata schemas */ deleteSchemas() { - this.registryService.getSelectedMetadataSchemas().pipe(take(1)).subscribe( - (schemas) => { - const tasks$ = []; - for (const schema of schemas) { - if (hasValue(schema.id)) { - tasks$.push(this.registryService.deleteMetadataSchema(schema.id).pipe(getFirstCompletedRemoteData())); - } - } - zip(...tasks$).subscribe((responses: RemoteData[]) => { - const successResponses = responses.filter((response: RemoteData) => response.hasSucceeded); - const failedResponses = responses.filter((response: RemoteData) => response.hasFailed); - if (successResponses.length > 0) { - this.showNotification(true, successResponses.length); - } - if (failedResponses.length > 0) { - this.showNotification(false, failedResponses.length); - } - this.registryService.deselectAllMetadataSchema(); - this.registryService.cancelEditMetadataSchema(); - }); - }, - ); + this.subscriptions.push(this.selectedMetadataSchemaIDs$.pipe( + take(1), + switchMap((schemaIDs: number[]) => zip(schemaIDs.map((schemaID: number) => this.registryService.deleteMetadataSchema(schemaID).pipe(getFirstCompletedRemoteData())))), + ).subscribe((responses: RemoteData[]) => { + const successResponses: RemoteData[] = responses.filter((response: RemoteData) => response.hasSucceeded); + const failedResponses: RemoteData[] = responses.filter((response: RemoteData) => response.hasFailed); + if (successResponses.length > 0) { + this.showNotification(true, successResponses.length); + } + if (failedResponses.length > 0) { + this.showNotification(false, failedResponses.length); + } + this.registryService.deselectAllMetadataSchema(); + this.registryService.cancelEditMetadataSchema(); + })); } /** @@ -196,20 +182,20 @@ export class MetadataRegistryComponent { showNotification(success: boolean, amount: number) { const prefix = 'admin.registries.schema.notification'; const suffix = success ? 'success' : 'failure'; - const messages = observableCombineLatest( - this.translateService.get(success ? `${prefix}.${suffix}` : `${prefix}.${suffix}`), - this.translateService.get(`${prefix}.deleted.${suffix}`, { amount: amount }), - ); - messages.subscribe(([head, content]) => { - if (success) { - this.notificationsService.success(head, content); - } else { - this.notificationsService.error(head, content); - } - }); + + const head: string = this.translateService.instant(success ? `${prefix}.${suffix}` : `${prefix}.${suffix}`); + const content: string = this.translateService.instant(`${prefix}.deleted.${suffix}`, { amount: amount }); + + if (success) { + this.notificationsService.success(head, content); + } else { + this.notificationsService.error(head, content); + } } + ngOnDestroy(): void { this.paginationService.clearPagination(this.config.id); + this.subscriptions.map((subscription: Subscription) => subscription.unsubscribe()); } } diff --git a/src/app/admin/admin-registries/metadata-registry/metadata-schema-form/metadata-schema-form.component.html b/src/app/admin/admin-registries/metadata-registry/metadata-schema-form/metadata-schema-form.component.html index 85d1e90692a..15cc81d9cab 100644 --- a/src/app/admin/admin-registries/metadata-registry/metadata-schema-form/metadata-schema-form.component.html +++ b/src/app/admin/admin-registries/metadata-registry/metadata-schema-form/metadata-schema-form.component.html @@ -1,4 +1,4 @@ -
+

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

diff --git a/src/app/admin/admin-registries/metadata-registry/metadata-schema-form/metadata-schema-form.component.spec.ts b/src/app/admin/admin-registries/metadata-registry/metadata-schema-form/metadata-schema-form.component.spec.ts index 8a08b59800e..f98e2743241 100644 --- a/src/app/admin/admin-registries/metadata-registry/metadata-schema-form/metadata-schema-form.component.spec.ts +++ b/src/app/admin/admin-registries/metadata-registry/metadata-schema-form/metadata-schema-form.component.spec.ts @@ -1,5 +1,5 @@ import { CommonModule } from '@angular/common'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; import { ComponentFixture, inject, @@ -16,42 +16,26 @@ import { RegistryService } from '../../../../core/registry/registry.service'; import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service'; import { FormComponent } from '../../../../shared/form/form.component'; import { getMockFormBuilderService } from '../../../../shared/mocks/form-builder-service.mock'; +import { RegistryServiceStub } from '../../../../shared/testing/registry.service.stub'; import { EnumKeysPipe } from '../../../../shared/utils/enum-keys-pipe'; import { MetadataSchemaFormComponent } from './metadata-schema-form.component'; describe('MetadataSchemaFormComponent', () => { let component: MetadataSchemaFormComponent; let fixture: ComponentFixture; - let registryService: RegistryService; - /* eslint-disable no-empty,@typescript-eslint/no-empty-function */ - const registryServiceStub = { - getActiveMetadataSchema: () => observableOf(undefined), - createOrUpdateMetadataSchema: (schema: MetadataSchema) => observableOf(schema), - cancelEditMetadataSchema: () => { - }, - clearMetadataSchemaRequests: () => observableOf(undefined), - }; - const formBuilderServiceStub = { - createFormGroup: () => { - return { - patchValue: () => { - }, - reset(_value?: any, _options?: { onlySelf?: boolean; emitEvent?: boolean; }): void { - }, - }; - }, - }; - /* eslint-enable no-empty, @typescript-eslint/no-empty-function */ + let registryService: RegistryServiceStub; beforeEach(waitForAsync(() => { + registryService = new RegistryServiceStub(); + return TestBed.configureTestingModule({ imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule, MetadataSchemaFormComponent, EnumKeysPipe], providers: [ - { provide: RegistryService, useValue: registryServiceStub }, + { provide: RegistryService, useValue: registryService }, { provide: FormBuilderService, useValue: getMockFormBuilderService() }, ], - schemas: [NO_ERRORS_SCHEMA], + schemas: [CUSTOM_ELEMENTS_SCHEMA], }) .overrideComponent(MetadataSchemaFormComponent, { remove: { @@ -88,7 +72,7 @@ describe('MetadataSchemaFormComponent', () => { describe('without an active schema', () => { beforeEach(() => { - spyOn(registryService, 'getActiveMetadataSchema').and.returnValue(observableOf(undefined)); + component.activeMetadataSchema$ = observableOf(undefined); component.onSubmit(); fixture.detectChanges(); }); @@ -107,7 +91,7 @@ describe('MetadataSchemaFormComponent', () => { } as MetadataSchema); beforeEach(() => { - spyOn(registryService, 'getActiveMetadataSchema').and.returnValue(observableOf(expectedWithId)); + component.activeMetadataSchema$ = observableOf(expectedWithId); component.onSubmit(); fixture.detectChanges(); }); diff --git a/src/app/admin/admin-registries/metadata-registry/metadata-schema-form/metadata-schema-form.component.ts b/src/app/admin/admin-registries/metadata-registry/metadata-schema-form/metadata-schema-form.component.ts index 068c7f1bab4..c58c4bef10c 100644 --- a/src/app/admin/admin-registries/metadata-registry/metadata-schema-form/metadata-schema-form.component.ts +++ b/src/app/admin/admin-registries/metadata-registry/metadata-schema-form/metadata-schema-form.component.ts @@ -21,13 +21,13 @@ import { TranslateService, } from '@ngx-translate/core'; import { - combineLatest, Observable, + Subscription, } from 'rxjs'; import { + map, switchMap, take, - tap, } from 'rxjs/operators'; import { MetadataSchema } from '../../../../core/metadata/metadata-schema.model'; @@ -102,64 +102,71 @@ export class MetadataSchemaFormComponent implements OnInit, OnDestroy { */ @Output() submitForm: EventEmitter = new EventEmitter(); - constructor(public registryService: RegistryService, private formBuilderService: FormBuilderService, private translateService: TranslateService) { + /** + * The {@link MetadataSchema} that is currently being edited + */ + activeMetadataSchema$: Observable; + + subscriptions: Subscription[] = []; + + constructor( + protected registryService: RegistryService, + protected formBuilderService: FormBuilderService, + protected translateService: TranslateService, + ) { } ngOnInit() { - combineLatest([ - this.translateService.get(`${this.messagePrefix}.name`), - this.translateService.get(`${this.messagePrefix}.namespace`), - ]).subscribe(([name, namespace]) => { - this.name = new DynamicInputModel({ - id: 'name', - label: name, - name: 'name', - validators: { - required: null, - pattern: '^[^. ,]*$', - maxLength: 32, - }, - required: true, - errorMessages: { - pattern: 'error.validation.metadata.name.invalid-pattern', - maxLength: 'error.validation.metadata.name.max-length', - }, - }); - this.namespace = new DynamicInputModel({ - id: 'namespace', - label: namespace, - name: 'namespace', - validators: { - required: null, - maxLength: 256, - }, - required: true, - errorMessages: { - maxLength: 'error.validation.metadata.namespace.max-length', - }, - }); - this.formModel = [ - new DynamicFormGroupModel( - { - id: 'metadatadataschemagroup', - group:[this.namespace, this.name], - }), - ]; - this.formGroup = this.formBuilderService.createFormGroup(this.formModel); - this.registryService.getActiveMetadataSchema().subscribe((schema: MetadataSchema) => { - if (schema == null) { - this.clearFields(); - } else { - this.formGroup.patchValue({ - metadatadataschemagroup: { - name: schema.prefix, - namespace: schema.namespace, - }, - }); - this.name.disabled = true; - } - }); + this.name = new DynamicInputModel({ + id: 'name', + label: this.translateService.instant(`${this.messagePrefix}.name`), + name: 'name', + validators: { + required: null, + pattern: '^[^. ,]*$', + maxLength: 32, + }, + required: true, + errorMessages: { + pattern: 'error.validation.metadata.name.invalid-pattern', + maxLength: 'error.validation.metadata.name.max-length', + }, }); + this.namespace = new DynamicInputModel({ + id: 'namespace', + label: this.translateService.instant(`${this.messagePrefix}.namespace`), + name: 'namespace', + validators: { + required: null, + maxLength: 256, + }, + required: true, + errorMessages: { + maxLength: 'error.validation.metadata.namespace.max-length', + }, + }); + this.formModel = [ + new DynamicFormGroupModel( + { + id: 'metadatadataschemagroup', + group:[this.namespace, this.name], + }), + ]; + this.formGroup = this.formBuilderService.createFormGroup(this.formModel); + this.activeMetadataSchema$ = this.registryService.getActiveMetadataSchema(); + this.subscriptions.push(this.activeMetadataSchema$.subscribe((schema: MetadataSchema) => { + if (schema == null) { + this.clearFields(); + } else { + this.formGroup.patchValue({ + metadatadataschemagroup: { + name: schema.prefix, + namespace: schema.namespace, + }, + }); + this.name.disabled = true; + } + })); } /** @@ -176,48 +183,29 @@ export class MetadataSchemaFormComponent implements OnInit, OnDestroy { * Emit the updated/created schema using the EventEmitter submitForm */ onSubmit(): void { - this.registryService - .getActiveMetadataSchema() - .pipe( - take(1), - switchMap((schema: MetadataSchema) => { - const metadataValues = { - prefix: this.name.value, - namespace: this.namespace.value, - }; - - let createOrUpdate$: Observable; - - if (schema == null) { - createOrUpdate$ = - this.registryService.createOrUpdateMetadataSchema( - Object.assign(new MetadataSchema(), metadataValues), - ); - } else { - const updatedSchema = Object.assign( - new MetadataSchema(), - schema, - { - namespace: metadataValues.namespace, - }, - ); - createOrUpdate$ = - this.registryService.createOrUpdateMetadataSchema( - updatedSchema, - ); - } - - return createOrUpdate$; - }), - tap(() => { - this.registryService.clearMetadataSchemaRequests().subscribe(); - }), - ) - .subscribe((updatedOrCreatedSchema: MetadataSchema) => { - this.submitForm.emit(updatedOrCreatedSchema); - this.clearFields(); - this.registryService.cancelEditMetadataSchema(); - }); + this.activeMetadataSchema$.pipe( + take(1), + switchMap((schema: MetadataSchema) => { + const metadataValues = { + prefix: this.name.value, + namespace: this.namespace.value, + }; + if (schema == null) { + return this.registryService.createOrUpdateMetadataSchema(Object.assign(new MetadataSchema(), metadataValues)); + } else { + return this.registryService.createOrUpdateMetadataSchema(Object.assign(new MetadataSchema(), schema, { + namespace: metadataValues.namespace, + })); + } + }), + switchMap((updatedOrCreatedSchema: MetadataSchema) => this.registryService.clearMetadataSchemaRequests().pipe( + map(() => updatedOrCreatedSchema), + )), + ).subscribe((updatedOrCreatedSchema: MetadataSchema) => { + this.submitForm.emit(updatedOrCreatedSchema); + this.clearFields(); + this.registryService.cancelEditMetadataSchema(); + }); } /** @@ -233,5 +221,6 @@ export class MetadataSchemaFormComponent implements OnInit, OnDestroy { */ ngOnDestroy(): void { this.onCancel(); + this.subscriptions.forEach((subscription: Subscription) => subscription.unsubscribe()); } } diff --git a/src/app/admin/admin-registries/metadata-schema/metadata-field-form/metadata-field-form.component.spec.ts b/src/app/admin/admin-registries/metadata-schema/metadata-field-form/metadata-field-form.component.spec.ts index 8044811ece0..440f52a24d8 100644 --- a/src/app/admin/admin-registries/metadata-schema/metadata-field-form/metadata-field-form.component.spec.ts +++ b/src/app/admin/admin-registries/metadata-schema/metadata-field-form/metadata-field-form.component.spec.ts @@ -1,5 +1,5 @@ import { CommonModule } from '@angular/common'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; import { ComponentFixture, inject, @@ -17,13 +17,15 @@ import { RegistryService } from '../../../../core/registry/registry.service'; import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service'; import { FormComponent } from '../../../../shared/form/form.component'; import { getMockFormBuilderService } from '../../../../shared/mocks/form-builder-service.mock'; +import { RegistryServiceStub } from '../../../../shared/testing/registry.service.stub'; import { EnumKeysPipe } from '../../../../shared/utils/enum-keys-pipe'; import { MetadataFieldFormComponent } from './metadata-field-form.component'; describe('MetadataFieldFormComponent', () => { let component: MetadataFieldFormComponent; let fixture: ComponentFixture; - let registryService: RegistryService; + + let registryService: RegistryServiceStub; const metadataSchema = Object.assign(new MetadataSchema(), { id: 1, @@ -31,37 +33,16 @@ describe('MetadataFieldFormComponent', () => { prefix: 'fake', }); - /* eslint-disable no-empty,@typescript-eslint/no-empty-function */ - const registryServiceStub = { - getActiveMetadataField: () => observableOf(undefined), - createMetadataField: (field: MetadataField) => observableOf(field), - updateMetadataField: (field: MetadataField) => observableOf(field), - cancelEditMetadataField: () => { - }, - cancelEditMetadataSchema: () => { - }, - clearMetadataFieldRequests: () => observableOf(undefined), - }; - const formBuilderServiceStub = { - createFormGroup: () => { - return { - patchValue: () => { - }, - reset(_value?: any, _options?: { onlySelf?: boolean; emitEvent?: boolean; }): void { - }, - }; - }, - }; - /* eslint-enable no-empty, @typescript-eslint/no-empty-function */ - beforeEach(waitForAsync(() => { + registryService = new RegistryServiceStub(); + return TestBed.configureTestingModule({ imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule, MetadataFieldFormComponent, EnumKeysPipe], providers: [ - { provide: RegistryService, useValue: registryServiceStub }, + { provide: RegistryService, useValue: registryService }, { provide: FormBuilderService, useValue: getMockFormBuilderService() }, ], - schemas: [NO_ERRORS_SCHEMA], + schemas: [CUSTOM_ELEMENTS_SCHEMA], }) .overrideComponent(MetadataFieldFormComponent, { remove: { imports: [FormComponent] }, diff --git a/src/app/admin/admin-registries/metadata-schema/metadata-schema.component.html b/src/app/admin/admin-registries/metadata-schema/metadata-schema.component.html index f748279a1d5..288266fb75e 100644 --- a/src/app/admin/admin-registries/metadata-schema/metadata-schema.component.html +++ b/src/app/admin/admin-registries/metadata-schema/metadata-schema.component.html @@ -31,8 +31,8 @@

{{'admin.registries.schema.fields.head' | translate}}

- + [ngClass]="{'table-primary' : (activeField$ | async)?.id === field.id}"> + { let comp: MetadataSchemaComponent; let fixture: ComponentFixture; - let registryService: RegistryService; - const mockSchemasList = [ + + let registryService: RegistryServiceStub; + let activatedRoute: ActivatedRouteStub; + let paginationService: PaginationServiceStub; + + const mockSchemasList: MetadataSchema[] = [ { id: 1, _links: { @@ -67,8 +67,8 @@ describe('MetadataSchemaComponent', () => { prefix: 'mock', namespace: 'http://dspace.org/mockschema', }, - ]; - const mockFieldsList = [ + ] as MetadataSchema[]; + const mockFieldsList: MetadataField[] = [ { id: 1, _links: { @@ -117,33 +117,8 @@ describe('MetadataSchemaComponent', () => { scopeNote: null, schema: createSuccessfulRemoteDataObject$(mockSchemasList[1]), }, - ]; - const mockSchemas = createSuccessfulRemoteDataObject$(buildPaginatedList(null, mockSchemasList)); - /* eslint-disable no-empty,@typescript-eslint/no-empty-function */ - const registryServiceStub = { - getMetadataSchemas: () => mockSchemas, - getMetadataFieldsBySchema: (schema: MetadataSchema) => createSuccessfulRemoteDataObject$(buildPaginatedList(null, mockFieldsList.filter((value) => value.id === 3 || value.id === 4))), - getMetadataSchemaByPrefix: (schemaName: string) => createSuccessfulRemoteDataObject$(mockSchemasList.filter((value) => value.prefix === schemaName)[0]), - getActiveMetadataField: () => observableOf(undefined), - getSelectedMetadataFields: () => observableOf([]), - editMetadataField: (schema) => { - }, - cancelEditMetadataField: () => { - }, - deleteMetadataField: () => observableOf(new RestResponse(true, 200, 'OK')), - deselectAllMetadataField: () => { - }, - clearMetadataFieldRequests: () => observableOf(undefined), - }; - /* eslint-enable no-empty, @typescript-eslint/no-empty-function */ + ] as MetadataField[]; const schemaNameParam = 'mock'; - const activatedRouteStub = Object.assign(new ActivatedRouteStub(), { - params: observableOf({ - schemaName: schemaNameParam, - }), - }); - - const paginationService = new PaginationServiceStub(); const configurationDataService = jasmine.createSpyObj('configurationDataService', { findByPropertyName: createSuccessfulRemoteDataObject$(Object.assign(new ConfigurationProperty(), { @@ -162,6 +137,14 @@ describe('MetadataSchemaComponent', () => { beforeEach(waitForAsync(() => { + activatedRoute = new ActivatedRouteStub({ + schemaName: schemaNameParam, + }); + paginationService = new PaginationServiceStub(); + registryService = new RegistryServiceStub(); + spyOn(registryService, 'getMetadataFieldsBySchema').and.returnValue(createSuccessfulRemoteDataObject$(buildPaginatedList(null, mockFieldsList.filter((value) => value.id === 3 || value.id === 4)))); + spyOn(registryService, 'getMetadataSchemaByPrefix').and.callFake((schemaName) => createSuccessfulRemoteDataObject$(mockSchemasList.filter((value) => value.prefix === schemaName)[0])); + TestBed.configureTestingModule({ imports: [ CommonModule, @@ -174,10 +157,9 @@ describe('MetadataSchemaComponent', () => { VarDirective, ], providers: [ - { provide: RegistryService, useValue: registryServiceStub }, - { provide: ActivatedRoute, useValue: activatedRouteStub }, + { provide: RegistryService, useValue: registryService }, + { provide: ActivatedRoute, useValue: activatedRoute }, { provide: HostWindowService, useValue: new HostWindowServiceStub(0) }, - { provide: Router, useValue: new RouterStub() }, { provide: PaginationService, useValue: paginationService }, { provide: NotificationsService, @@ -187,7 +169,7 @@ describe('MetadataSchemaComponent', () => { { provide: ConfigurationDataService, useValue: configurationDataService }, { provide: SearchConfigurationService, useValue: new SearchConfigurationServiceStub() }, ], - schemas: [NO_ERRORS_SCHEMA], + schemas: [CUSTOM_ELEMENTS_SCHEMA], }) .overrideComponent(MetadataSchemaComponent, { remove: { @@ -242,7 +224,7 @@ describe('MetadataSchemaComponent', () => { })); it('should cancel editing the selected field when clicked again', waitForAsync(() => { - spyOn(registryService, 'getActiveMetadataField').and.returnValue(observableOf(mockFieldsList[2] as MetadataField)); + comp.activeField$ = observableOf(mockFieldsList[2] as MetadataField); spyOn(registryService, 'cancelEditMetadataField'); row.click(); fixture.detectChanges(); @@ -257,7 +239,7 @@ describe('MetadataSchemaComponent', () => { beforeEach(() => { spyOn(registryService, 'deleteMetadataField').and.callThrough(); - spyOn(registryService, 'getSelectedMetadataFields').and.returnValue(observableOf(selectedFields as MetadataField[])); + comp.selectedMetadataFieldIDs$ = observableOf(selectedFields.map((metadataField: MetadataField) => metadataField.id)); comp.deleteFields(); fixture.detectChanges(); }); diff --git a/src/app/admin/admin-registries/metadata-schema/metadata-schema.component.ts b/src/app/admin/admin-registries/metadata-schema/metadata-schema.component.ts index c5c1d650b1c..ec5d6b4cb09 100644 --- a/src/app/admin/admin-registries/metadata-schema/metadata-schema.component.ts +++ b/src/app/admin/admin-registries/metadata-schema/metadata-schema.component.ts @@ -20,9 +20,9 @@ import { import { BehaviorSubject, combineLatest, - combineLatest as observableCombineLatest, Observable, of as observableOf, + Subscription, zip, } from 'rxjs'; import { @@ -42,7 +42,6 @@ import { getFirstCompletedRemoteData, getFirstSucceededRemoteDataPayload, } from '../../../core/shared/operators'; -import { hasValue } from '../../../shared/empty.util'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { PaginationComponent } from '../../../shared/pagination/pagination.component'; import { toFindListOptions } from '../../../shared/pagination/pagination.utils'; @@ -71,7 +70,7 @@ import { MetadataFieldFormComponent } from './metadata-field-form/metadata-field * A component used for managing all existing metadata fields within the current metadata schema. * The admin can create, edit or delete metadata fields here. */ -export class MetadataSchemaComponent implements OnInit, OnDestroy { +export class MetadataSchemaComponent implements OnDestroy, OnInit { /** * The metadata schema */ @@ -96,26 +95,33 @@ export class MetadataSchemaComponent implements OnInit, OnDestroy { */ needsUpdate$: BehaviorSubject = new BehaviorSubject(true); - constructor(private registryService: RegistryService, - private route: ActivatedRoute, - private notificationsService: NotificationsService, - private paginationService: PaginationService, - private translateService: TranslateService) { + /** + * The current {@link MetadataField} that is being edited + */ + activeField$: Observable; - } + /** + * The selected {@link MetadataField} IDs + */ + selectedMetadataFieldIDs$: Observable; - ngOnInit(): void { - this.route.params.subscribe((params) => { - this.initialize(params); - }); + subscriptions: Subscription[] = []; + + constructor( + protected registryService: RegistryService, + protected route: ActivatedRoute, + protected notificationsService: NotificationsService, + protected paginationService: PaginationService, + protected translateService: TranslateService, + ) { } - /** - * Initialize the component using the params within the url (schemaName) - * @param params - */ - initialize(params) { - this.metadataSchema$ = this.registryService.getMetadataSchemaByPrefix(params.schemaName).pipe(getFirstSucceededRemoteDataPayload()); + ngOnInit(): void { + this.metadataSchema$ = this.registryService.getMetadataSchemaByPrefix(this.route.snapshot.params.schemaName).pipe(getFirstSucceededRemoteDataPayload()); + this.activeField$ = this.registryService.getActiveMetadataField(); + this.selectedMetadataFieldIDs$ = this.registryService.getSelectedMetadataFields().pipe( + map((metadataFields: MetadataField[]) => metadataFields.map((metadataField: MetadataField) => metadataField.id)), + ); this.updateFields(); } @@ -148,30 +154,13 @@ export class MetadataSchemaComponent implements OnInit, OnDestroy { * @param field */ editField(field: MetadataField) { - this.getActiveField().pipe(take(1)).subscribe((activeField) => { + this.subscriptions.push(this.activeField$.pipe(take(1)).subscribe((activeField) => { if (field === activeField) { this.registryService.cancelEditMetadataField(); } else { this.registryService.editMetadataField(field); } - }); - } - - /** - * Checks whether the given metadata field is active (being edited) - * @param field - */ - isActive(field: MetadataField): Observable { - return this.getActiveField().pipe( - map((activeField) => field === activeField), - ); - } - - /** - * Gets the active metadata field (being edited) - */ - getActiveField(): Observable { - return this.registryService.getActiveMetadataField(); + })); } /** @@ -185,42 +174,25 @@ export class MetadataSchemaComponent implements OnInit, OnDestroy { this.registryService.deselectMetadataField(field); } - /** - * Checks whether a given metadata field is selected in the list (checkbox) - * @param field - */ - isSelected(field: MetadataField): Observable { - return this.registryService.getSelectedMetadataFields().pipe( - map((fields) => fields.find((selectedField) => selectedField === field) != null), - ); - } - /** * Delete all the selected metadata fields */ deleteFields() { - this.registryService.getSelectedMetadataFields().pipe(take(1)).subscribe( - (fields) => { - const tasks$ = []; - for (const field of fields) { - if (hasValue(field.id)) { - tasks$.push(this.registryService.deleteMetadataField(field.id).pipe(getFirstCompletedRemoteData())); - } - } - zip(...tasks$).subscribe((responses: RemoteData[]) => { - const successResponses = responses.filter((response: RemoteData) => response.hasSucceeded); - const failedResponses = responses.filter((response: RemoteData) => response.hasFailed); - if (successResponses.length > 0) { - this.showNotification(true, successResponses.length); - } - if (failedResponses.length > 0) { - this.showNotification(false, failedResponses.length); - } - this.registryService.deselectAllMetadataField(); - this.registryService.cancelEditMetadataField(); - }); - }, - ); + this.subscriptions.push(this.selectedMetadataFieldIDs$.pipe( + take(1), + switchMap((fieldIDs) => zip(fieldIDs.map((fieldID) => this.registryService.deleteMetadataField(fieldID).pipe(getFirstCompletedRemoteData())))), + ).subscribe((responses: RemoteData[]) => { + const successResponses = responses.filter((response: RemoteData) => response.hasSucceeded); + const failedResponses = responses.filter((response: RemoteData) => response.hasFailed); + if (successResponses.length > 0) { + this.showNotification(true, successResponses.length); + } + if (failedResponses.length > 0) { + this.showNotification(false, failedResponses.length); + } + this.registryService.deselectAllMetadataField(); + this.registryService.cancelEditMetadataField(); + })); } /** @@ -231,21 +203,19 @@ export class MetadataSchemaComponent implements OnInit, OnDestroy { showNotification(success: boolean, amount: number) { const prefix = 'admin.registries.schema.notification'; const suffix = success ? 'success' : 'failure'; - const messages = observableCombineLatest([ - this.translateService.get(success ? `${prefix}.${suffix}` : `${prefix}.${suffix}`), - this.translateService.get(`${prefix}.field.deleted.${suffix}`, { amount: amount }), - ]); - messages.subscribe(([head, content]) => { - if (success) { - this.notificationsService.success(head, content); - } else { - this.notificationsService.error(head, content); - } - }); + const head = this.translateService.instant(success ? `${prefix}.${suffix}` : `${prefix}.${suffix}`); + const content = this.translateService.instant(`${prefix}.field.deleted.${suffix}`, { amount: amount }); + if (success) { + this.notificationsService.success(head, content); + } else { + this.notificationsService.error(head, content); + } } + ngOnDestroy(): void { this.paginationService.clearPagination(this.config.id); this.registryService.deselectAllMetadataField(); + this.subscriptions.forEach((subscription: Subscription) => subscription.unsubscribe()); } } diff --git a/src/app/admin/admin-reports/filtered-collections/filtered-collections.component.ts b/src/app/admin/admin-reports/filtered-collections/filtered-collections.component.ts index b4bebfc924f..e1f54bd8d3e 100644 --- a/src/app/admin/admin-reports/filtered-collections/filtered-collections.component.ts +++ b/src/app/admin/admin-reports/filtered-collections/filtered-collections.component.ts @@ -4,6 +4,7 @@ import { } from '@angular/common'; import { Component, + OnInit, ViewChild, } from '@angular/core'; import { @@ -40,7 +41,7 @@ import { FilteredCollections } from './filtered-collections.model'; ], standalone: true, }) -export class FilteredCollectionsComponent { +export class FilteredCollectionsComponent implements OnInit { queryForm: FormGroup; results: FilteredCollections = new FilteredCollections(); diff --git a/src/app/admin/admin-reports/filtered-items/filtered-items.component.ts b/src/app/admin/admin-reports/filtered-items/filtered-items.component.ts index 9fd4643573e..4cb93aa9be7 100644 --- a/src/app/admin/admin-reports/filtered-items/filtered-items.component.ts +++ b/src/app/admin/admin-reports/filtered-items/filtered-items.component.ts @@ -5,6 +5,7 @@ import { } from '@angular/common'; import { Component, + OnInit, ViewChild, } from '@angular/core'; import { @@ -66,7 +67,7 @@ import { QueryPredicate } from './query-predicate.model'; ], standalone: true, }) -export class FilteredItemsComponent { +export class FilteredItemsComponent implements OnInit { collections: OptionVO[]; presetQueries: PresetQuery[]; @@ -90,7 +91,7 @@ export class FilteredItemsComponent { private formBuilder: FormBuilder, private restService: DspaceRestService) {} - ngOnInit() { + ngOnInit(): void { this.loadCollections(); this.loadPresetQueries(); this.loadMetadataFields(); diff --git a/src/app/admin/admin-routes.ts b/src/app/admin/admin-routes.ts index a89dbb2c0a9..e5afe09cc79 100644 --- a/src/app/admin/admin-routes.ts +++ b/src/app/admin/admin-routes.ts @@ -3,7 +3,7 @@ import { Route } from '@angular/router'; import { i18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver'; import { AdminCurationTasksComponent } from './admin-curation-tasks/admin-curation-tasks.component'; import { BatchImportPageComponent } from './admin-import-batch-page/batch-import-page.component'; -import { MetadataImportPageComponent } from './admin-import-metadata-page/metadata-import-page.component'; +import { ThemedMetadataImportPageComponent } from './admin-import-metadata-page/themed-metadata-import-page.component'; import { LDN_PATH, NOTIFICATIONS_MODULE_PATH, @@ -11,8 +11,8 @@ import { REGISTRIES_MODULE_PATH, REPORTS_MODULE_PATH, } from './admin-routing-paths'; -import { AdminSearchPageComponent } from './admin-search-page/admin-search-page.component'; -import { AdminWorkflowPageComponent } from './admin-workflow-page/admin-workflow-page.component'; +import { ThemedAdminSearchPageComponent } from './admin-search-page/themed-admin-search-page.component'; +import { ThemedAdminWorkflowPageComponent } from './admin-workflow-page/themed-admin-workflow-page.component'; export const ROUTES: Route[] = [ { @@ -28,13 +28,13 @@ export const ROUTES: Route[] = [ { path: 'search', resolve: { breadcrumb: i18nBreadcrumbResolver }, - component: AdminSearchPageComponent, + component: ThemedAdminSearchPageComponent, data: { title: 'admin.search.title', breadcrumbKey: 'admin.search' }, }, { path: 'workflow', resolve: { breadcrumb: i18nBreadcrumbResolver }, - component: AdminWorkflowPageComponent, + component: ThemedAdminWorkflowPageComponent, data: { title: 'admin.workflow.title', breadcrumbKey: 'admin.workflow' }, }, { @@ -46,7 +46,7 @@ export const ROUTES: Route[] = [ { path: 'metadata-import', resolve: { breadcrumb: i18nBreadcrumbResolver }, - component: MetadataImportPageComponent, + component: ThemedMetadataImportPageComponent, data: { title: 'admin.metadata-import.title', breadcrumbKey: 'admin.metadata-import' }, }, { diff --git a/src/app/admin/admin-search-page/admin-search-page.component.html b/src/app/admin/admin-search-page/admin-search-page.component.html index 516799ddf90..69ff132fe3f 100644 --- a/src/app/admin/admin-search-page/admin-search-page.component.html +++ b/src/app/admin/admin-search-page/admin-search-page.component.html @@ -1 +1 @@ - + diff --git a/src/app/admin/admin-search-page/admin-search-page.component.ts b/src/app/admin/admin-search-page/admin-search-page.component.ts index 99909b8257f..4ae11a9d47a 100644 --- a/src/app/admin/admin-search-page/admin-search-page.component.ts +++ b/src/app/admin/admin-search-page/admin-search-page.component.ts @@ -4,7 +4,7 @@ import { Context } from '../../core/shared/context.model'; import { ThemedConfigurationSearchPageComponent } from '../../search-page/themed-configuration-search-page.component'; @Component({ - selector: 'ds-admin-search-page', + selector: 'ds-base-admin-search-page', templateUrl: './admin-search-page.component.html', styleUrls: ['./admin-search-page.component.scss'], standalone: true, diff --git a/src/app/admin/admin-search-page/admin-search-results/admin-search-result-grid-element/collection-search-result/collection-admin-search-result-grid-element.component.ts b/src/app/admin/admin-search-page/admin-search-results/admin-search-result-grid-element/collection-search-result/collection-admin-search-result-grid-element.component.ts index 172226dd076..38352e7816d 100644 --- a/src/app/admin/admin-search-page/admin-search-results/admin-search-result-grid-element/collection-search-result/collection-admin-search-result-grid-element.component.ts +++ b/src/app/admin/admin-search-page/admin-search-results/admin-search-result-grid-element/collection-search-result/collection-admin-search-result-grid-element.component.ts @@ -1,4 +1,7 @@ -import { Component } from '@angular/core'; +import { + Component, + OnInit, +} from '@angular/core'; import { RouterLink } from '@angular/router'; import { getCollectionEditRoute } from '../../../../../collection-page/collection-page-routing-paths'; @@ -21,10 +24,10 @@ import { SearchResultGridElementComponent } from '../../../../../shared/object-g /** * The component for displaying a list element for a collection search result on the admin search page */ -export class CollectionAdminSearchResultGridElementComponent extends SearchResultGridElementComponent { +export class CollectionAdminSearchResultGridElementComponent extends SearchResultGridElementComponent implements OnInit { editPath: string; - ngOnInit() { + ngOnInit(): void { super.ngOnInit(); this.editPath = getCollectionEditRoute(this.dso.uuid); } diff --git a/src/app/admin/admin-search-page/admin-search-results/admin-search-result-grid-element/community-search-result/community-admin-search-result-grid-element.component.ts b/src/app/admin/admin-search-page/admin-search-results/admin-search-result-grid-element/community-search-result/community-admin-search-result-grid-element.component.ts index 50be35229dd..509c1e92669 100644 --- a/src/app/admin/admin-search-page/admin-search-results/admin-search-result-grid-element/community-search-result/community-admin-search-result-grid-element.component.ts +++ b/src/app/admin/admin-search-page/admin-search-results/admin-search-result-grid-element/community-search-result/community-admin-search-result-grid-element.component.ts @@ -1,4 +1,7 @@ -import { Component } from '@angular/core'; +import { + Component, + OnInit, +} from '@angular/core'; import { RouterLink } from '@angular/router'; import { getCommunityEditRoute } from '../../../../../community-page/community-page-routing-paths'; @@ -21,10 +24,10 @@ import { SearchResultGridElementComponent } from '../../../../../shared/object-g /** * The component for displaying a list element for a community search result on the admin search page */ -export class CommunityAdminSearchResultGridElementComponent extends SearchResultGridElementComponent { +export class CommunityAdminSearchResultGridElementComponent extends SearchResultGridElementComponent implements OnInit { editPath: string; - ngOnInit() { + ngOnInit(): void { super.ngOnInit(); this.editPath = getCommunityEditRoute(this.dso.uuid); } diff --git a/src/app/admin/admin-search-page/admin-search-results/admin-search-result-list-element/collection-search-result/collection-admin-search-result-list-element.component.ts b/src/app/admin/admin-search-page/admin-search-results/admin-search-result-list-element/collection-search-result/collection-admin-search-result-list-element.component.ts index 37afbf14fee..0924379ea54 100644 --- a/src/app/admin/admin-search-page/admin-search-results/admin-search-result-list-element/collection-search-result/collection-admin-search-result-list-element.component.ts +++ b/src/app/admin/admin-search-page/admin-search-results/admin-search-result-list-element/collection-search-result/collection-admin-search-result-list-element.component.ts @@ -1,4 +1,7 @@ -import { Component } from '@angular/core'; +import { + Component, + OnInit, +} from '@angular/core'; import { RouterLink } from '@angular/router'; import { TranslateModule } from '@ngx-translate/core'; @@ -22,10 +25,10 @@ import { SearchResultListElementComponent } from '../../../../../shared/object-l /** * The component for displaying a list element for a collection search result on the admin search page */ -export class CollectionAdminSearchResultListElementComponent extends SearchResultListElementComponent { +export class CollectionAdminSearchResultListElementComponent extends SearchResultListElementComponent implements OnInit { editPath: string; - ngOnInit() { + ngOnInit(): void { super.ngOnInit(); this.editPath = getCollectionEditRoute(this.dso.uuid); } diff --git a/src/app/admin/admin-search-page/admin-search-results/admin-search-result-list-element/community-search-result/community-admin-search-result-list-element.component.ts b/src/app/admin/admin-search-page/admin-search-results/admin-search-result-list-element/community-search-result/community-admin-search-result-list-element.component.ts index 5861f15c1ff..c4146dbd60d 100644 --- a/src/app/admin/admin-search-page/admin-search-results/admin-search-result-list-element/community-search-result/community-admin-search-result-list-element.component.ts +++ b/src/app/admin/admin-search-page/admin-search-results/admin-search-result-list-element/community-search-result/community-admin-search-result-list-element.component.ts @@ -1,4 +1,7 @@ -import { Component } from '@angular/core'; +import { + Component, + OnInit, +} from '@angular/core'; import { RouterLink } from '@angular/router'; import { TranslateModule } from '@ngx-translate/core'; @@ -22,10 +25,10 @@ import { SearchResultListElementComponent } from '../../../../../shared/object-l /** * The component for displaying a list element for a community search result on the admin search page */ -export class CommunityAdminSearchResultListElementComponent extends SearchResultListElementComponent { +export class CommunityAdminSearchResultListElementComponent extends SearchResultListElementComponent implements OnInit { editPath: string; - ngOnInit() { + ngOnInit(): void { super.ngOnInit(); this.editPath = getCommunityEditRoute(this.dso.uuid); } diff --git a/src/app/admin/admin-search-page/themed-admin-search-page.component.ts b/src/app/admin/admin-search-page/themed-admin-search-page.component.ts new file mode 100644 index 00000000000..d49c1847846 --- /dev/null +++ b/src/app/admin/admin-search-page/themed-admin-search-page.component.ts @@ -0,0 +1,29 @@ +import { Component } from '@angular/core'; + +import { ThemedComponent } from '../../shared/theme-support/themed.component'; +import { AdminSearchPageComponent } from './admin-search-page.component'; + +/** + * Themed wrapper for {@link AdminSearchPageComponent} + */ +@Component({ + selector: 'ds-admin-search-page', + templateUrl: '../../shared/theme-support/themed.component.html', + standalone: true, + imports: [AdminSearchPageComponent], +}) +export class ThemedAdminSearchPageComponent extends ThemedComponent { + + protected getComponentName(): string { + return 'AdminSearchPageComponent'; + } + + protected importThemedComponent(themeName: string): Promise { + return import(`../../../themes/${themeName}/app/admin/admin-search-page/admin-search-page.component`); + } + + protected importUnthemedComponent(): Promise { + return import('./admin-search-page.component'); + } + +} diff --git a/src/app/admin/admin-sidebar/admin-sidebar-section/admin-sidebar-section.component.html b/src/app/admin/admin-sidebar/admin-sidebar-section/admin-sidebar-section.component.html index 24ba17fff47..30a7a3353b9 100644 --- a/src/app/admin/admin-sidebar/admin-sidebar-section/admin-sidebar-section.component.html +++ b/src/app/admin/admin-sidebar/admin-sidebar-section/admin-sidebar-section.component.html @@ -14,7 +14,9 @@ diff --git a/src/app/admin/admin-sidebar/admin-sidebar-section/admin-sidebar-section.component.ts b/src/app/admin/admin-sidebar/admin-sidebar-section/admin-sidebar-section.component.ts index ff9897ce9e8..2910f948a24 100644 --- a/src/app/admin/admin-sidebar/admin-sidebar-section/admin-sidebar-section.component.ts +++ b/src/app/admin/admin-sidebar/admin-sidebar-section/admin-sidebar-section.component.ts @@ -17,6 +17,7 @@ import { MenuID } from '../../../shared/menu/menu-id.model'; import { LinkMenuItemModel } from '../../../shared/menu/menu-item/models/link.model'; import { MenuSection } from '../../../shared/menu/menu-section.model'; import { MenuSectionComponent } from '../../../shared/menu/menu-section/menu-section.component'; +import { BrowserOnlyPipe } from '../../../shared/utils/browser-only.pipe'; /** * Represents a non-expandable section in the admin sidebar @@ -26,7 +27,7 @@ import { MenuSectionComponent } from '../../../shared/menu/menu-section/menu-sec templateUrl: './admin-sidebar-section.component.html', styleUrls: ['./admin-sidebar-section.component.scss'], standalone: true, - imports: [NgClass, RouterLink, TranslateModule], + imports: [NgClass, RouterLink, TranslateModule, BrowserOnlyPipe], }) export class AdminSidebarSectionComponent extends MenuSectionComponent implements OnInit { diff --git a/src/app/admin/admin-sidebar/admin-sidebar.component.html b/src/app/admin/admin-sidebar/admin-sidebar.component.html index f3d5409a642..41376f777e1 100644 --- a/src/app/admin/admin-sidebar/admin-sidebar.component.html +++ b/src/app/admin/admin-sidebar/admin-sidebar.component.html @@ -42,6 +42,7 @@

{{ 'menu.header.admin' | translate }}

@@ -61,7 +61,7 @@ - +
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 e0456ca700f..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 @@ -38,7 +38,7 @@ import { FlatNode } from '../flat-node.model'; * 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, 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 dc6c0aa3453..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 @@ -5,10 +5,11 @@ import { CommunityListComponent } from './community-list.component'; @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/themed-community-list-page.component.ts b/src/app/community-list-page/themed-community-list-page.component.ts index 4f0575c7db7..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 @@ -7,10 +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 e9c5f9bf292..d32d9e408f2 100644 --- a/src/app/community-page/community-form/community-form.component.ts +++ b/src/app/community-page/community-form/community-form.component.ts @@ -10,6 +10,7 @@ import { SimpleChange, SimpleChanges, } from '@angular/core'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { DynamicFormControlModel, DynamicFormService, @@ -108,8 +109,9 @@ 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) { diff --git a/src/app/community-page/community-page-administrator.guard.ts b/src/app/community-page/community-page-administrator.guard.ts index 23d429b4840..ecbc9b86c0c 100644 --- a/src/app/community-page/community-page-administrator.guard.ts +++ b/src/app/community-page/community-page-administrator.guard.ts @@ -1,43 +1,16 @@ -import { Injectable } from '@angular/core'; -import { - ActivatedRouteSnapshot, - ResolveFn, - Router, - RouterStateSnapshot, -} from '@angular/router'; -import { - Observable, - of as observableOf, -} from 'rxjs'; +import { CanActivateFn } from '@angular/router'; +import { of as observableOf } from 'rxjs'; -import { AuthService } from '../core/auth/auth.service'; -import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service'; -import { DsoPageSingleFeatureGuard } from '../core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard'; +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 { RemoteData } from '../core/data/remote-data'; -import { Community } from '../core/shared/community.model'; 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 { - - protected resolver: ResolveFn> = communityPageResolver; - - constructor(protected authorizationService: AuthorizationDataService, - protected router: Router, - protected authService: AuthService) { - super(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 index 656b96c311e..2c8a7942a4a 100644 --- a/src/app/community-page/community-page-routes.ts +++ b/src/app/community-page/community-page-routes.ts @@ -1,7 +1,4 @@ -import { - mapToCanActivate, - Route, -} from '@angular/router'; +import { Route } from '@angular/router'; import { browseByGuard } from '../browse-by/browse-by-guard'; import { browseByI18nBreadcrumbResolver } from '../browse-by/browse-by-i18n-breadcrumb.resolver'; @@ -14,7 +11,7 @@ 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 { communityPageAdministratorGuard } from './community-page-administrator.guard'; import { COMMUNITY_CREATE_PATH, COMMUNITY_EDIT_PATH, @@ -54,7 +51,6 @@ export const ROUTES: Route[] = [ resolve: { dso: communityPageResolver, breadcrumb: communityBreadcrumbResolver, - menu: dsoEditMenuResolver, }, runGuardsAndResolvers: 'always', children: [ @@ -62,7 +58,7 @@ export const ROUTES: Route[] = [ path: COMMUNITY_EDIT_PATH, loadChildren: () => import('./edit-community-page/edit-community-page-routes') .then((m) => m.ROUTES), - canActivate: mapToCanActivate([CommunityPageAdministratorGuard]), + canActivate: [communityPageAdministratorGuard], }, { path: 'delete', @@ -73,6 +69,9 @@ export const ROUTES: Route[] = [ { path: '', component: ThemedCommunityPageComponent, + resolve: { + menu: dsoEditMenuResolver, + }, children: [ { path: '', diff --git a/src/app/community-page/community-page.component.html b/src/app/community-page/community-page.component.html index b3e577af7d3..a695e2019a3 100644 --- a/src/app/community-page/community-page.component.html +++ b/src/app/community-page/community-page.component.html @@ -10,8 +10,8 @@ - - + + @@ -25,8 +25,8 @@
- - + +
@@ -39,5 +39,5 @@ - + diff --git a/src/app/community-page/community-page.component.ts b/src/app/community-page/community-page.component.ts index ce3d05aef9f..d04ecbee190 100644 --- a/src/app/community-page/community-page.component.ts +++ b/src/app/community-page/community-page.component.ts @@ -32,7 +32,7 @@ import { Community } from '../core/shared/community.model'; import { getAllSucceededRemoteDataPayload } from '../core/shared/operators'; import { fadeInOut } from '../shared/animations/fade'; import { ThemedComcolPageBrowseByComponent } from '../shared/comcol/comcol-page-browse-by/themed-comcol-page-browse-by.component'; -import { ComcolPageContentComponent } from '../shared/comcol/comcol-page-content/comcol-page-content.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'; @@ -47,13 +47,13 @@ import { ThemedCollectionPageSubCollectionListComponent } from './sections/sub-c import { ThemedCommunityPageSubCommunityListComponent } from './sections/sub-com-col-section/sub-community-list/themed-community-page-sub-community-list.component'; @Component({ - selector: 'ds-community-page', + selector: 'ds-base-community-page', styleUrls: ['./community-page.component.scss'], templateUrl: './community-page.component.html', changeDetection: ChangeDetectionStrategy.OnPush, animations: [fadeInOut], imports: [ - ComcolPageContentComponent, + ThemedComcolPageContentComponent, ErrorComponent, ThemedLoadingComponent, NgIf, 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.ts b/src/app/community-page/create-community-page/create-community-page.component.ts index acc5279a5c1..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 @@ -15,6 +15,7 @@ import { RequestService } from '../../core/data/request.service'; import { RouteService } from '../../core/services/route.service'; import { Community } from '../../core/shared/community.model'; import { CreateComColPageComponent } from '../../shared/comcol/comcol-forms/create-comcol-page/create-comcol-page.component'; +import { ThemedLoadingComponent } from '../../shared/loading/themed-loading.component'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { VarDirective } from '../../shared/utils/var.directive'; import { CommunityFormComponent } from '../community-form/community-form.component'; @@ -32,6 +33,7 @@ import { CommunityFormComponent } from '../community-form/community-form.compone VarDirective, NgIf, AsyncPipe, + ThemedLoadingComponent, ], standalone: true, }) 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/edit-community-page-routes.ts b/src/app/community-page/edit-community-page/edit-community-page-routes.ts index a15312a216e..2402c2037d5 100644 --- 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 @@ -1,10 +1,7 @@ -import { - mapToCanActivate, - Route, -} from '@angular/router'; +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 { 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'; @@ -28,7 +25,7 @@ export const ROUTES: Route[] = [ }, data: { breadcrumbKey: 'community.edit' }, component: EditCommunityPageComponent, - canActivate: mapToCanActivate([CommunityAdministratorGuard]), + canActivate: [communityAdministratorGuard], children: [ { path: '', diff --git a/src/app/community-page/sections/sub-com-col-section/sub-collection-list/community-page-sub-collection-list.component.html b/src/app/community-page/sections/sub-com-col-section/sub-collection-list/community-page-sub-collection-list.component.html index b5fbf1a01dd..59d7b3bb5e2 100644 --- a/src/app/community-page/sections/sub-com-col-section/sub-collection-list/community-page-sub-collection-list.component.html +++ b/src/app/community-page/sections/sub-com-col-section/sub-collection-list/community-page-sub-collection-list.component.html @@ -9,5 +9,5 @@

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

- + diff --git a/src/app/community-page/sections/sub-com-col-section/sub-collection-list/community-page-sub-collection-list.component.ts b/src/app/community-page/sections/sub-com-col-section/sub-collection-list/community-page-sub-collection-list.component.ts index 2935f255958..1e8ff1d46c3 100644 --- a/src/app/community-page/sections/sub-com-col-section/sub-collection-list/community-page-sub-collection-list.component.ts +++ b/src/app/community-page/sections/sub-com-col-section/sub-collection-list/community-page-sub-collection-list.component.ts @@ -36,7 +36,7 @@ import { PaginationComponentOptions } from '../../../../shared/pagination/pagina import { VarDirective } from '../../../../shared/utils/var.directive'; @Component({ - selector: 'ds-community-page-sub-collection-list', + selector: 'ds-base-community-page-sub-collection-list', styleUrls: ['./community-page-sub-collection-list.component.scss'], templateUrl: './community-page-sub-collection-list.component.html', animations: [fadeIn], diff --git a/src/app/community-page/sections/sub-com-col-section/sub-collection-list/themed-community-page-sub-collection-list.component.ts b/src/app/community-page/sections/sub-com-col-section/sub-collection-list/themed-community-page-sub-collection-list.component.ts index ff5d057b312..4a965bc9264 100644 --- a/src/app/community-page/sections/sub-com-col-section/sub-collection-list/themed-community-page-sub-collection-list.component.ts +++ b/src/app/community-page/sections/sub-com-col-section/sub-collection-list/themed-community-page-sub-collection-list.component.ts @@ -8,10 +8,11 @@ import { ThemedComponent } from '../../../../shared/theme-support/themed.compone import { CommunityPageSubCollectionListComponent } from './community-page-sub-collection-list.component'; @Component({ - selector: 'ds-themed-community-page-sub-collection-list', + selector: 'ds-community-page-sub-collection-list', styleUrls: [], templateUrl: '../../../../shared/theme-support/themed.component.html', standalone: true, + imports: [CommunityPageSubCollectionListComponent], }) export class ThemedCollectionPageSubCollectionListComponent extends ThemedComponent { @Input() community: Community; diff --git a/src/app/community-page/sections/sub-com-col-section/sub-com-col-section.component.html b/src/app/community-page/sections/sub-com-col-section/sub-com-col-section.component.html index 515e08ffdfe..a811014bcc9 100644 --- a/src/app/community-page/sections/sub-com-col-section/sub-com-col-section.component.html +++ b/src/app/community-page/sections/sub-com-col-section/sub-com-col-section.component.html @@ -1,8 +1,8 @@ - - - + - + diff --git a/src/app/community-page/sections/sub-com-col-section/sub-community-list/community-page-sub-community-list.component.html b/src/app/community-page/sections/sub-com-col-section/sub-community-list/community-page-sub-community-list.component.html index 0834d08ba58..7f9840f6b7f 100644 --- a/src/app/community-page/sections/sub-com-col-section/sub-community-list/community-page-sub-community-list.component.html +++ b/src/app/community-page/sections/sub-com-col-section/sub-community-list/community-page-sub-community-list.component.html @@ -9,5 +9,5 @@

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

- + diff --git a/src/app/community-page/sections/sub-com-col-section/sub-community-list/community-page-sub-community-list.component.ts b/src/app/community-page/sections/sub-com-col-section/sub-community-list/community-page-sub-community-list.component.ts index 4f74eff601b..36bd9919bb8 100644 --- a/src/app/community-page/sections/sub-com-col-section/sub-community-list/community-page-sub-community-list.component.ts +++ b/src/app/community-page/sections/sub-com-col-section/sub-community-list/community-page-sub-community-list.component.ts @@ -35,7 +35,7 @@ import { PaginationComponentOptions } from '../../../../shared/pagination/pagina 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], diff --git a/src/app/community-page/sections/sub-com-col-section/sub-community-list/themed-community-page-sub-community-list.component.ts b/src/app/community-page/sections/sub-com-col-section/sub-community-list/themed-community-page-sub-community-list.component.ts index 11b62d68e46..5988ad0f5ea 100644 --- a/src/app/community-page/sections/sub-com-col-section/sub-community-list/themed-community-page-sub-community-list.component.ts +++ b/src/app/community-page/sections/sub-com-col-section/sub-community-list/themed-community-page-sub-community-list.component.ts @@ -8,10 +8,11 @@ import { ThemedComponent } from '../../../../shared/theme-support/themed.compone import { CommunityPageSubCommunityListComponent } from './community-page-sub-community-list.component'; @Component({ - selector: 'ds-themed-community-page-sub-community-list', + selector: 'ds-community-page-sub-community-list', styleUrls: [], templateUrl: '../../../../shared/theme-support/themed.component.html', standalone: true, + imports: [CommunityPageSubCommunityListComponent], }) export class ThemedCommunityPageSubCommunityListComponent extends ThemedComponent { diff --git a/src/app/community-page/themed-community-page.component.ts b/src/app/community-page/themed-community-page.component.ts index 41a29607190..b655452041a 100644 --- a/src/app/community-page/themed-community-page.component.ts +++ b/src/app/community-page/themed-community-page.component.ts @@ -7,10 +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/models/auth-status.model.ts b/src/app/core/auth/models/auth-status.model.ts index 1a6938887d5..f25e5a48928 100644 --- a/src/app/core/auth/models/auth-status.model.ts +++ b/src/app/core/auth/models/auth-status.model.ts @@ -36,14 +36,14 @@ export class AuthStatus implements CacheableObject { * The unique identifier of this auth status */ @autoserialize - id: string; + id: string; /** * The type for this AuthStatus */ @excludeFromEquals @autoserialize - type: ResourceType; + type: ResourceType; /** * The UUID of this auth status @@ -57,19 +57,19 @@ export class AuthStatus implements CacheableObject { * True if REST API is up and running, should never return false */ @autoserialize - okay: boolean; + okay: boolean; /** * If the auth status represents an authenticated state */ @autoserialize - authenticated: boolean; + authenticated: boolean; /** * The {@link HALLink}s for this AuthStatus */ @deserialize - _links: { + _links: { self: HALLink; eperson: HALLink; specialGroups: HALLink; @@ -80,32 +80,32 @@ export class AuthStatus implements CacheableObject { * Will be undefined unless the eperson {@link HALLink} has been resolved. */ @link(EPERSON) - eperson?: Observable>; + eperson?: Observable>; /** * The SpecialGroup of this auth status * Will be undefined unless the SpecialGroup {@link HALLink} has been resolved. */ @link(GROUP, true) - specialGroups?: Observable>>; + specialGroups?: Observable>>; /** * True if the token is valid, false if there was no token or the token wasn't valid */ @autoserialize - token?: AuthTokenInfo; + token?: AuthTokenInfo; /** * Authentication error if there was one for this status */ // TODO should be refactored to use the RemoteData error @autoserialize - error?: AuthError; + error?: AuthError; /** * All authentication methods enabled at the backend */ @autoserialize - authMethods: AuthMethod[]; + authMethods: AuthMethod[]; } diff --git a/src/app/core/auth/models/short-lived-token.model.ts b/src/app/core/auth/models/short-lived-token.model.ts index d91a26e990a..5e8587d02dd 100644 --- a/src/app/core/auth/models/short-lived-token.model.ts +++ b/src/app/core/auth/models/short-lived-token.model.ts @@ -22,19 +22,19 @@ export class ShortLivedToken implements CacheableObject { */ @excludeFromEquals @autoserialize - type: ResourceType; + type: ResourceType; /** * The value for this ShortLivedToken */ @autoserializeAs('token') - value: string; + value: string; /** * The {@link HALLink}s for this ShortLivedToken */ @deserialize - _links: { + _links: { self: HALLink; }; } diff --git a/src/app/core/breadcrumbs/dso-name.service.spec.ts b/src/app/core/breadcrumbs/dso-name.service.spec.ts index 90b08dca143..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; @@ -25,6 +29,26 @@ describe(`DSONameService`, () => { }, }); + mockEPersonName = 'John Doe'; + mockEPerson = Object.assign(new DSpaceObject(), { + firstMetadataValue(keyOrKeys: string | string[], valueFilter?: MetadataValueFilter): string { + return mockEPersonName; + }, + getRenderTypes(): (string | GenericConstructor)[] { + return ['EPerson', Item, DSpaceObject]; + }, + }); + + mockEPersonNameFirst = 'John'; + mockEPersonFirst = Object.assign(new DSpaceObject(), { + firstMetadataValue(keyOrKeys: string | string[], valueFilter?: MetadataValueFilter): string { + return mockEPersonNameFirst; + }, + getRenderTypes(): (string | GenericConstructor)[] { + return ['EPerson', Item, DSpaceObject]; + }, + }); + mockOrgUnitName = 'Molecular Spectroscopy'; mockOrgUnit = Object.assign(new DSpaceObject(), { firstMetadataValue(keyOrKeys: string | string[], valueFilter?: MetadataValueFilter): string { @@ -67,6 +91,15 @@ describe(`DSONameService`, () => { expect(result).toBe('Bingo!'); }); + it(`should use the EPerson factory for EPerson objects`, () => { + spyOn((service as any).factories, 'EPerson').and.returnValue('Bingo!'); + + const result = service.getName(mockEPerson); + + expect((service as any).factories.EPerson).toHaveBeenCalledWith(mockEPerson); + expect(result).toBe('Bingo!'); + }); + it(`should use the Default factory for regular DSpaceObjects`, () => { spyOn((service as any).factories, 'Default').and.returnValue('Bingo!'); @@ -107,6 +140,35 @@ describe(`DSONameService`, () => { }); }); + describe(`factories.EPerson`, () => { + describe(`with eperson.firstname and without eperson.lastname`, () => { + beforeEach(() => { + spyOn(mockEPerson, 'firstMetadataValue').and.returnValues(...mockEPersonName.split(' ')); + }); + + it(`should return 'eperson.firstname' and 'eperson.lastname'`, () => { + const result = (service as any).factories.EPerson(mockEPerson); + expect(result).toBe(mockEPersonName); + expect(mockEPerson.firstMetadataValue).toHaveBeenCalledWith('eperson.firstname'); + expect(mockEPerson.firstMetadataValue).toHaveBeenCalledWith('eperson.lastname'); + }); + }); + + describe(` with eperson.firstname and without eperson.lastname`, () => { + beforeEach(() => { + spyOn(mockEPersonFirst, 'firstMetadataValue').and.returnValues(mockEPersonNameFirst, undefined); + }); + + it(`should return 'eperson.firstname'`, () => { + const result = (service as any).factories.EPerson(mockEPersonFirst); + expect(result).toBe(mockEPersonNameFirst); + expect(mockEPersonFirst.firstMetadataValue).toHaveBeenCalledWith('eperson.firstname'); + expect(mockEPersonFirst.firstMetadataValue).toHaveBeenCalledWith('eperson.lastname'); + }); + }); + }); + + describe(`factories.OrgUnit`, () => { beforeEach(() => { spyOn(mockOrgUnit, 'firstMetadataValue').and.callThrough(); diff --git a/src/app/core/breadcrumbs/item-breadcrumb.resolver.ts b/src/app/core/breadcrumbs/item-breadcrumb.resolver.ts index cb16cedb422..ef021123d42 100644 --- a/src/app/core/breadcrumbs/item-breadcrumb.resolver.ts +++ b/src/app/core/breadcrumbs/item-breadcrumb.resolver.ts @@ -7,7 +7,7 @@ import { import { Observable } from 'rxjs'; import { BreadcrumbConfig } from '../../breadcrumbs/breadcrumb/breadcrumb-config.model'; -import { ITEM_PAGE_LINKS_TO_FOLLOW } from '../../item-page/item.resolver'; +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'; @@ -24,7 +24,7 @@ export const itemBreadcrumbResolver: ResolveFn> = ( breadcrumbService: DSOBreadcrumbsService = inject(DSOBreadcrumbsService), dataService: ItemDataService = inject(ItemDataService), ): Observable> => { - const linksToFollow: FollowLinkConfig[] = ITEM_PAGE_LINKS_TO_FOLLOW as FollowLinkConfig[]; + const linksToFollow: FollowLinkConfig[] = getItemPageLinksToFollow() as FollowLinkConfig[]; return DSOBreadcrumbResolver( route, state, diff --git a/src/app/core/browse/browse.service.spec.ts b/src/app/core/browse/browse.service.spec.ts index f9999439e1e..f3328c2bedd 100644 --- a/src/app/core/browse/browse.service.spec.ts +++ b/src/app/core/browse/browse.service.spec.ts @@ -7,7 +7,6 @@ import { of as observableOf } from 'rxjs'; import { TestScheduler } from 'rxjs/testing'; import { getMockHrefOnlyDataService } from '../../shared/mocks/href-only-data.service.mock'; -import { getMockRemoteDataBuildService } from '../../shared/mocks/remote-data-build.service.mock'; import { getMockRequestService } from '../../shared/mocks/request.service.mock'; import { createSuccessfulRemoteDataObject, @@ -18,7 +17,6 @@ import { createPaginatedList, getFirstUsedArgumentOfSpyMethod, } from '../../shared/testing/utils.test'; -import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { RequestService } from '../data/request.service'; import { RequestEntry } from '../data/request-entry.model'; import { FlatBrowseDefinition } from '../shared/flat-browse-definition.model'; @@ -31,7 +29,6 @@ describe('BrowseService', () => { let scheduler: TestScheduler; let service: BrowseService; let requestService: RequestService; - let rdbService: RemoteDataBuildService; const browsesEndpointURL = 'https://rest.api/browses'; const halService: any = new HALEndpointServiceStub(browsesEndpointURL); @@ -129,7 +126,6 @@ describe('BrowseService', () => { halService, browseDefinitionDataService, hrefOnlyDataService, - rdbService, ); } @@ -141,11 +137,9 @@ describe('BrowseService', () => { beforeEach(() => { requestService = getMockRequestService(getRequestEntry$(true)); - rdbService = getMockRemoteDataBuildService(); service = initTestService(); spyOn(halService, 'getEndpoint').and .returnValue(hot('--a-', { a: browsesEndpointURL })); - spyOn(rdbService, 'buildList').and.callThrough(); }); it('should call BrowseDefinitionDataService to create the RemoteData Observable', () => { @@ -162,9 +156,7 @@ describe('BrowseService', () => { beforeEach(() => { requestService = getMockRequestService(getRequestEntry$(true)); - rdbService = getMockRemoteDataBuildService(); service = initTestService(); - spyOn(rdbService, 'buildList').and.callThrough(); }); describe('when getBrowseEntriesFor is called with a valid browse definition id', () => { @@ -215,7 +207,6 @@ describe('BrowseService', () => { describe('if getBrowseDefinitions fires', () => { beforeEach(() => { requestService = getMockRequestService(getRequestEntry$(true)); - rdbService = getMockRemoteDataBuildService(); service = initTestService(); spyOn(service, 'getBrowseDefinitions').and .returnValue(hot('--a-', { @@ -270,7 +261,6 @@ describe('BrowseService', () => { describe('if getBrowseDefinitions doesn\'t fire', () => { it('should return undefined', () => { requestService = getMockRequestService(getRequestEntry$(true)); - rdbService = getMockRemoteDataBuildService(); service = initTestService(); spyOn(service, 'getBrowseDefinitions').and .returnValue(hot('----')); @@ -288,9 +278,7 @@ describe('BrowseService', () => { describe('getFirstItemFor', () => { beforeEach(() => { requestService = getMockRequestService(); - rdbService = getMockRemoteDataBuildService(); service = initTestService(); - spyOn(rdbService, 'buildList').and.callThrough(); }); describe('when getFirstItemFor is called with a valid browse definition id', () => { diff --git a/src/app/core/browse/browse.service.ts b/src/app/core/browse/browse.service.ts index 43f33f26e49..5fe06a700e5 100644 --- a/src/app/core/browse/browse.service.ts +++ b/src/app/core/browse/browse.service.ts @@ -6,6 +6,7 @@ import { startWith, } from 'rxjs/operators'; +import { environment } from '../../../environments/environment'; import { hasValue, hasValueOperator, @@ -16,7 +17,6 @@ import { followLink, FollowLinkConfig, } from '../../shared/utils/follow-link-config.model'; -import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { SortDirection } from '../cache/models/sort-options.model'; import { HrefOnlyDataService } from '../data/href-only-data.service'; import { PaginatedList } from '../data/paginated-list.model'; @@ -38,9 +38,15 @@ import { URLCombiner } from '../url-combiner/url-combiner'; import { BrowseDefinitionDataService } from './browse-definition-data.service'; import { BrowseEntrySearchOptions } from './browse-entry-search-options.model'; -export const BROWSE_LINKS_TO_FOLLOW: FollowLinkConfig[] = [ - followLink('thumbnail'), -]; +export function getBrowseLinksToFollow(): FollowLinkConfig[] { + const followLinks = [ + followLink('thumbnail'), + ]; + if (environment.item.showAccessStatuses) { + followLinks.push(followLink('accessStatus')); + } + return followLinks; +} /** * The service handling all browse requests @@ -67,7 +73,6 @@ export class BrowseService { protected halService: HALEndpointService, private browseDefinitionDataService: BrowseDefinitionDataService, private hrefOnlyDataService: HrefOnlyDataService, - private rdb: RemoteDataBuildService, ) { } @@ -117,7 +122,7 @@ export class BrowseService { }), ); if (options.fetchThumbnail ) { - return this.hrefOnlyDataService.findListByHref(href$, {}, undefined, undefined, ...BROWSE_LINKS_TO_FOLLOW); + return this.hrefOnlyDataService.findListByHref(href$, {}, undefined, undefined, ...getBrowseLinksToFollow()); } return this.hrefOnlyDataService.findListByHref(href$); } @@ -165,7 +170,7 @@ export class BrowseService { }), ); if (options.fetchThumbnail) { - return this.hrefOnlyDataService.findListByHref(href$, {}, undefined, undefined, ...BROWSE_LINKS_TO_FOLLOW); + return this.hrefOnlyDataService.findListByHref(href$, {}, undefined, undefined, ...getBrowseLinksToFollow()); } return this.hrefOnlyDataService.findListByHref(href$); } diff --git a/src/app/core/cache/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/models/self-link.model.ts b/src/app/core/cache/models/self-link.model.ts index 903a7794951..a87acdd5065 100644 --- a/src/app/core/cache/models/self-link.model.ts +++ b/src/app/core/cache/models/self-link.model.ts @@ -3,9 +3,9 @@ import { autoserialize } from 'cerialize'; export class SelfLink { @autoserialize - self: string; + self: string; @autoserialize - uuid: string; + uuid: string; } diff --git a/src/app/core/cache/object-cache.reducer.ts b/src/app/core/cache/object-cache.reducer.ts index 631ecd22097..1a780ff0083 100644 --- a/src/app/core/cache/object-cache.reducer.ts +++ b/src/app/core/cache/object-cache.reducer.ts @@ -174,20 +174,25 @@ export function objectCacheReducer(state = initialState, action: ObjectCacheActi * the new state, with the object added, or overwritten. */ function addToObjectCache(state: ObjectCacheState, action: AddToObjectCacheAction): ObjectCacheState { - const existing = state[action.payload.objectToCache._links.self.href] || {} as any; + const cacheLink = hasValue(action.payload.objectToCache?._links?.self) ? action.payload.objectToCache._links.self.href : action.payload.alternativeLink; + const existing = state[cacheLink] || {} as any; const newAltLinks = hasValue(action.payload.alternativeLink) ? [action.payload.alternativeLink] : []; - return Object.assign({}, state, { - [action.payload.objectToCache._links.self.href]: { - data: action.payload.objectToCache, - timeCompleted: action.payload.timeCompleted, - msToLive: action.payload.msToLive, - requestUUIDs: [action.payload.requestUUID, ...(existing.requestUUIDs || [])], - dependentRequestUUIDs: existing.dependentRequestUUIDs || [], - isDirty: isNotEmpty(existing.patches), - patches: existing.patches || [], - alternativeLinks: [...(existing.alternativeLinks || []), ...newAltLinks], - } as ObjectCacheEntry, - }); + if (hasValue(cacheLink)) { + return Object.assign({}, state, { + [cacheLink]: { + data: action.payload.objectToCache, + timeCompleted: action.payload.timeCompleted, + msToLive: action.payload.msToLive, + requestUUIDs: [action.payload.requestUUID, ...(existing.requestUUIDs || [])], + dependentRequestUUIDs: existing.dependentRequestUUIDs || [], + isDirty: isNotEmpty(existing.patches), + patches: existing.patches || [], + alternativeLinks: [...(existing.alternativeLinks || []), ...newAltLinks], + } as ObjectCacheEntry, + }); + } else { + return state; + } } /** @@ -304,7 +309,7 @@ function addDependentsObjectCacheState(state: ObjectCacheState, action: AddDepen /** - * Remove all dependent request UUIDs from a cached object, used to clear out-of-date depedencies + * Remove all dependent request UUIDs from a cached object, used to clear out-of-date dependencies * * @param state the current state * @param action an AddDependentsObjectCacheAction diff --git a/src/app/core/cache/object-cache.service.ts b/src/app/core/cache/object-cache.service.ts index 69242732a50..f645b5a878d 100644 --- a/src/app/core/cache/object-cache.service.ts +++ b/src/app/core/cache/object-cache.service.ts @@ -99,7 +99,9 @@ export class ObjectCacheService { * An optional alternative link to this object */ add(object: CacheableObject, msToLive: number, requestUUID: string, alternativeLink?: string): void { - object = this.linkService.removeResolvedLinks(object); // Ensure the object we're storing has no resolved links + if (hasValue(object)) { + object = this.linkService.removeResolvedLinks(object); // Ensure the object we're storing has no resolved links + } this.store.dispatch(new AddToObjectCacheAction(object, new Date().getTime(), msToLive, requestUUID, alternativeLink)); } @@ -175,11 +177,15 @@ export class ObjectCacheService { }, ), map((entry: ObjectCacheEntry) => { - const type: GenericConstructor = getClassForType((entry.data as any).type); - if (typeof type !== 'function') { - throw new Error(`${type} is not a valid constructor for ${JSON.stringify(entry.data)}`); + if (hasValue(entry.data)) { + const type: GenericConstructor = getClassForType((entry.data as any).type); + if (typeof type !== 'function') { + throw new Error(`${type} is not a valid constructor for ${JSON.stringify(entry.data)}`); + } + return Object.assign(new type(), entry.data) as T; + } else { + return null; } - return Object.assign(new type(), entry.data) as T; }), ); } diff --git a/src/app/core/config/models/bulk-access-condition-options.model.ts b/src/app/core/config/models/bulk-access-condition-options.model.ts index c4913438524..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 @@ -25,19 +25,19 @@ export class BulkAccessConditionOptions extends ConfigObject { */ @excludeFromEquals @autoserialize - type: ResourceType; + type: ResourceType; @autoserializeAs(String, 'name') - uuid: string; + uuid: string; @autoserialize - id: string; + id: string; @autoserialize - itemAccessConditionOptions: AccessesConditionOption[]; + itemAccessConditionOptions: AccessesConditionOption[]; @autoserialize - bitstreamAccessConditionOptions: AccessesConditionOption[]; + bitstreamAccessConditionOptions: AccessesConditionOption[]; _links: { self: HALLink }; } diff --git a/src/app/core/config/models/config-submission-access.model.ts b/src/app/core/config/models/config-submission-access.model.ts index 4617dc47191..2e9a9291835 100644 --- a/src/app/core/config/models/config-submission-access.model.ts +++ b/src/app/core/config/models/config-submission-access.model.ts @@ -22,25 +22,25 @@ export class SubmissionAccessModel extends ConfigObject { * A list of available item access conditions */ @autoserialize - accessConditionOptions: AccessesConditionOption[]; + accessConditionOptions: AccessesConditionOption[]; /** * Boolean that indicates whether the current item must be findable via search or browse. */ @autoserialize - discoverable: boolean; + discoverable: boolean; /** * Boolean that indicates whether or not the user can change the discoverable flag. */ @autoserialize - canChangeDiscoverable: boolean; + canChangeDiscoverable: boolean; /** * The links to all related resources returned by the rest api. */ @deserialize - _links: { + _links: { self: HALLink }; diff --git a/src/app/core/config/models/config-submission-definition.model.ts b/src/app/core/config/models/config-submission-definition.model.ts index 2d6b1ad604c..eda4f54340b 100644 --- a/src/app/core/config/models/config-submission-definition.model.ts +++ b/src/app/core/config/models/config-submission-definition.model.ts @@ -23,20 +23,20 @@ export class SubmissionDefinitionModel extends ConfigObject { * A boolean representing if this submission definition is the default or not */ @autoserialize - isDefault: boolean; + isDefault: boolean; /** * A list of SubmissionSectionModel that are present in this submission definition */ // TODO refactor using remotedata @deserialize - sections: PaginatedList; + sections: PaginatedList; /** * The links to all related resources returned by the rest api. */ @deserialize - _links: { + _links: { self: HALLink, collections: HALLink, sections: HALLink diff --git a/src/app/core/config/models/config-submission-form.model.ts b/src/app/core/config/models/config-submission-form.model.ts index f6011adc764..c524a839160 100644 --- a/src/app/core/config/models/config-submission-form.model.ts +++ b/src/app/core/config/models/config-submission-form.model.ts @@ -27,5 +27,5 @@ export class SubmissionFormModel extends ConfigObject { * An array of [FormRowModel] that are present in this form */ @autoserialize - rows: FormRowModel[]; + rows: FormRowModel[]; } diff --git a/src/app/core/config/models/config-submission-section.model.ts b/src/app/core/config/models/config-submission-section.model.ts index 8ad1d3a0e4c..653e2e874dc 100644 --- a/src/app/core/config/models/config-submission-section.model.ts +++ b/src/app/core/config/models/config-submission-section.model.ts @@ -4,20 +4,16 @@ import { inheritSerialization, } from 'cerialize'; +import { + SectionScope, + SectionVisibility, +} from '../../../submission/objects/section-visibility.model'; import { SectionsType } from '../../../submission/sections/sections-type'; import { typedObject } from '../../cache/builders/build-decorators'; import { HALLink } from '../../shared/hal-link.model'; import { ConfigObject } from './config.model'; import { SUBMISSION_SECTION_TYPE } from './config-type'; -/** - * An interface that define section visibility and its properties. - */ -export interface SubmissionSectionVisibility { - main: any; - other: any; -} - @typedObject @inheritSerialization(ConfigObject) export class SubmissionSectionModel extends ConfigObject { @@ -27,37 +23,43 @@ export class SubmissionSectionModel extends ConfigObject { * The header for this section */ @autoserialize - header: string; + header: string; /** * A boolean representing if this submission section is the mandatory or not */ @autoserialize - mandatory: boolean; + mandatory: boolean; + + /** + * The submission scope for this section + */ + @autoserialize + scope: SectionScope; /** * A string representing the kind of section object */ @autoserialize - sectionType: SectionsType; + sectionType: SectionsType; /** * A string representing the type this section extends */ @autoserialize - extendsSectionType: SectionsType; + extendsSectionType: SectionsType; /** - * The [SubmissionSectionVisibility] object for this section + * The [SectionVisibility] object for this section */ @autoserialize - visibility: SubmissionSectionVisibility; + visibility: SectionVisibility; /** * The {@link HALLink}s for this SubmissionSectionModel */ @deserialize - _links: { + _links: { self: HALLink; config: HALLink; }; diff --git a/src/app/core/config/models/config-submission-upload.model.ts b/src/app/core/config/models/config-submission-upload.model.ts index edc4626f83e..cabc84d0f55 100644 --- a/src/app/core/config/models/config-submission-upload.model.ts +++ b/src/app/core/config/models/config-submission-upload.model.ts @@ -27,22 +27,22 @@ export class SubmissionUploadModel extends ConfigObject { * A list of available bitstream access conditions */ @autoserialize - accessConditionOptions: AccessConditionOption[]; + accessConditionOptions: AccessConditionOption[]; /** * An object representing the configuration describing the bitstream metadata form */ @link(SUBMISSION_FORMS_TYPE) - metadata?: Observable>; + metadata?: Observable>; @autoserialize - required: boolean; + required: boolean; @autoserialize - maxSize: number; + maxSize: number; @deserialize - _links: { + _links: { metadata: HALLink self: HALLink }; diff --git a/src/app/core/config/models/config.model.ts b/src/app/core/config/models/config.model.ts index c1db44e8919..74d090b89db 100644 --- a/src/app/core/config/models/config.model.ts +++ b/src/app/core/config/models/config.model.ts @@ -27,13 +27,13 @@ export abstract class ConfigObject implements CacheableObject { */ @excludeFromEquals @autoserialize - type: ResourceType; + type: ResourceType; /** * The links to all related resources returned by the rest api. */ @deserialize - _links: { + _links: { self: HALLink, [name: string]: HALLink }; diff --git a/src/app/core/data-services-map.ts b/src/app/core/data-services-map.ts index 8e76eef9258..c9ebbc5ffc3 100644 --- a/src/app/core/data-services-map.ts +++ b/src/app/core/data-services-map.ts @@ -27,12 +27,12 @@ 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 { SUGGESTION } from './notifications/models/suggestion-objects.resource-type'; -import { SUGGESTION_SOURCE } from './notifications/models/suggestion-source-object.resource-type'; -import { SUGGESTION_TARGET } from './notifications/models/suggestion-target-object.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'; @@ -131,9 +131,9 @@ export const LAZY_DATA_SERVICES: LazyDataServicesMap = new Map([ [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-data.service').then(m => m.SuggestionsDataService)], - [SUGGESTION_SOURCE.value, () => import('./notifications/source/suggestion-source-data.service').then(m => m.SuggestionSourceDataService)], - [SUGGESTION_TARGET.value, () => import('./notifications/target/suggestion-target-data.service').then(m => m.SuggestionTargetDataService)], + [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 1240585027f..ed587c26d2f 100644 --- a/src/app/core/data/access-status-data.service.spec.ts +++ b/src/app/core/data/access-status-data.service.spec.ts @@ -63,12 +63,12 @@ describe('AccessStatusDataService', () => { /** * Create an AccessStatusDataService used for testing - * @param reponse$ Supply a RemoteData to be returned by the REST API (optional) + * @param response$ Supply a RemoteData to be returned by the REST API (optional) */ - function createService(reponse$?: Observable>) { + function createService(response$?: Observable>) { requestService = getMockRequestService(); - let buildResponse$ = reponse$; - if (hasNoValue(reponse$)) { + let buildResponse$ = response$; + if (hasNoValue(response$)) { buildResponse$ = createSuccessfulRemoteDataObject$({}); } rdbService = jasmine.createSpyObj('rdbService', { diff --git a/src/app/core/data/base/base-data.service.spec.ts b/src/app/core/data/base/base-data.service.spec.ts index 3f44ad5e5ac..fc3704bce56 100644 --- a/src/app/core/data/base/base-data.service.spec.ts +++ b/src/app/core/data/base/base-data.service.spec.ts @@ -65,6 +65,7 @@ describe('BaseDataService', () => { let selfLink; let linksToFollow; let testScheduler; + let remoteDataTimestamp: number; let remoteDataMocks: { [responseType: string]: RemoteData }; let remoteDataPageMocks: { [responseType: string]: RemoteData }; @@ -85,7 +86,9 @@ describe('BaseDataService', () => { expect(actual).toEqual(expected); }); - const timeStamp = new Date().getTime(); + // The response's lastUpdated equals the time of 60 seconds after the test started, ensuring they are not perceived + // as cached values. + remoteDataTimestamp = new Date().getTime() + 60 * 1000; const msToLive = 15 * 60 * 1000; const payload = { foo: 'bar', @@ -112,22 +115,22 @@ describe('BaseDataService', () => { const statusCodeError = 404; const errorMessage = 'not found'; remoteDataMocks = { - RequestPending: new RemoteData(undefined, msToLive, timeStamp, RequestEntryState.RequestPending, undefined, undefined, undefined), - ResponsePending: new RemoteData(undefined, msToLive, timeStamp, RequestEntryState.ResponsePending, undefined, undefined, undefined), - ResponsePendingStale: new RemoteData(undefined, msToLive, timeStamp, RequestEntryState.ResponsePendingStale, undefined, undefined, undefined), - Success: new RemoteData(timeStamp, msToLive, timeStamp, RequestEntryState.Success, undefined, payload, statusCodeSuccess), - SuccessStale: new RemoteData(timeStamp, msToLive, timeStamp, RequestEntryState.SuccessStale, undefined, payload, statusCodeSuccess), - Error: new RemoteData(timeStamp, msToLive, timeStamp, RequestEntryState.Error, errorMessage, undefined, statusCodeError), - ErrorStale: new RemoteData(timeStamp, msToLive, timeStamp, RequestEntryState.ErrorStale, errorMessage, undefined, statusCodeError), + RequestPending: new RemoteData(undefined, msToLive, remoteDataTimestamp, RequestEntryState.RequestPending, undefined, undefined, undefined), + ResponsePending: new RemoteData(undefined, msToLive, remoteDataTimestamp, RequestEntryState.ResponsePending, undefined, undefined, undefined), + ResponsePendingStale: new RemoteData(undefined, msToLive, remoteDataTimestamp, RequestEntryState.ResponsePendingStale, undefined, undefined, undefined), + Success: new RemoteData(remoteDataTimestamp, msToLive, remoteDataTimestamp, RequestEntryState.Success, undefined, payload, statusCodeSuccess), + SuccessStale: new RemoteData(remoteDataTimestamp, msToLive, remoteDataTimestamp, RequestEntryState.SuccessStale, undefined, payload, statusCodeSuccess), + Error: new RemoteData(remoteDataTimestamp, msToLive, remoteDataTimestamp, RequestEntryState.Error, errorMessage, undefined, statusCodeError), + ErrorStale: new RemoteData(remoteDataTimestamp, msToLive, remoteDataTimestamp, RequestEntryState.ErrorStale, errorMessage, undefined, statusCodeError), }; remoteDataPageMocks = { - RequestPending: new RemoteData(undefined, msToLive, timeStamp, RequestEntryState.RequestPending, undefined, undefined, undefined), - ResponsePending: new RemoteData(undefined, msToLive, timeStamp, RequestEntryState.ResponsePending, undefined, undefined, undefined), - ResponsePendingStale: new RemoteData(undefined, msToLive, timeStamp, RequestEntryState.ResponsePendingStale, undefined, undefined, undefined), - Success: new RemoteData(timeStamp, msToLive, timeStamp, RequestEntryState.Success, undefined, createPaginatedList([payload]), statusCodeSuccess), - SuccessStale: new RemoteData(timeStamp, msToLive, timeStamp, RequestEntryState.SuccessStale, undefined, createPaginatedList([payload]), statusCodeSuccess), - Error: new RemoteData(timeStamp, msToLive, timeStamp, RequestEntryState.Error, errorMessage, undefined, statusCodeError), - ErrorStale: new RemoteData(timeStamp, msToLive, timeStamp, RequestEntryState.ErrorStale, errorMessage, undefined, statusCodeError), + RequestPending: new RemoteData(undefined, msToLive, remoteDataTimestamp, RequestEntryState.RequestPending, undefined, undefined, undefined), + ResponsePending: new RemoteData(undefined, msToLive, remoteDataTimestamp, RequestEntryState.ResponsePending, undefined, undefined, undefined), + ResponsePendingStale: new RemoteData(undefined, msToLive, remoteDataTimestamp, RequestEntryState.ResponsePendingStale, undefined, undefined, undefined), + Success: new RemoteData(remoteDataTimestamp, msToLive, remoteDataTimestamp, RequestEntryState.Success, undefined, createPaginatedList([payload]), statusCodeSuccess), + SuccessStale: new RemoteData(remoteDataTimestamp, msToLive, remoteDataTimestamp, RequestEntryState.SuccessStale, undefined, createPaginatedList([payload]), statusCodeSuccess), + Error: new RemoteData(remoteDataTimestamp, msToLive, remoteDataTimestamp, RequestEntryState.Error, errorMessage, undefined, statusCodeError), + ErrorStale: new RemoteData(remoteDataTimestamp, msToLive, remoteDataTimestamp, RequestEntryState.ErrorStale, errorMessage, undefined, statusCodeError), }; return new TestService( @@ -361,11 +364,15 @@ describe('BaseDataService', () => { spyOn(service as any, 'reRequestStaleRemoteData').and.callFake(() => (source) => source); }); - - it(`should not emit a cached completed RemoteData, but only start emitting after the state first changes to RequestPending`, () => { + it('should not emit a cached completed RemoteData', () => { + // Old cached value from 1 minute before the test started + const oldCachedSucceededData: RemoteData = Object.assign({}, remoteDataPageMocks.Success, { + timeCompleted: remoteDataTimestamp - 2 * 60 * 1000, + lastUpdated: remoteDataTimestamp - 2 * 60 * 1000, + } as RemoteData); testScheduler.run(({ cold, expectObservable }) => { spyOn(rdbService, 'buildSingle').and.returnValue(cold('a-b-c-d-e', { - a: remoteDataMocks.Success, + a: oldCachedSucceededData, b: remoteDataMocks.RequestPending, c: remoteDataMocks.ResponsePending, d: remoteDataMocks.Success, @@ -383,6 +390,22 @@ describe('BaseDataService', () => { }); }); + it('should emit the first completed RemoteData since the request was made', () => { + testScheduler.run(({ cold, expectObservable }) => { + spyOn(rdbService, 'buildSingle').and.returnValue(cold('a-b', { + a: remoteDataMocks.Success, + b: remoteDataMocks.SuccessStale, + })); + const expected = 'a-b'; + const values = { + a: remoteDataMocks.Success, + b: remoteDataMocks.SuccessStale, + }; + + expectObservable(service.findByHref(selfLink, false, true, ...linksToFollow)).toBe(expected, values); + }); + }); + it(`should not emit a cached stale RemoteData, but only start emitting after the state first changes to RequestPending`, () => { testScheduler.run(({ cold, expectObservable }) => { spyOn(rdbService, 'buildSingle').and.returnValue(cold('a-b-c-d-e-f-g', { @@ -411,17 +434,12 @@ describe('BaseDataService', () => { it('should link all the followLinks of a cached object by calling addDependency', () => { spyOn(objectCache, 'addDependency').and.callThrough(); testScheduler.run(({ cold, expectObservable, flush }) => { - spyOn(rdbService, 'buildSingle').and.returnValue(cold('a-b-c-d', { + spyOn(rdbService, 'buildSingle').and.returnValue(cold('a', { a: remoteDataMocks.Success, - b: remoteDataMocks.RequestPending, - c: remoteDataMocks.ResponsePending, - d: remoteDataMocks.Success, })); - const expected = '--b-c-d'; + const expected = 'a'; const values = { - b: remoteDataMocks.RequestPending, - c: remoteDataMocks.ResponsePending, - d: remoteDataMocks.Success, + a: remoteDataMocks.Success, }; expectObservable(service.findByHref(selfLink, false, false, ...linksToFollow)).toBe(expected, values); @@ -570,11 +588,15 @@ describe('BaseDataService', () => { spyOn(service as any, 'reRequestStaleRemoteData').and.callFake(() => (source) => source); }); - - it(`should not emit a cached completed RemoteData, but only start emitting after the state first changes to RequestPending`, () => { + it('should not emit a cached completed RemoteData', () => { testScheduler.run(({ cold, expectObservable }) => { + // Old cached value from 1 minute before the test started + const oldCachedSucceededData: RemoteData = Object.assign({}, remoteDataPageMocks.Success, { + timeCompleted: remoteDataTimestamp - 2 * 60 * 1000, + lastUpdated: remoteDataTimestamp - 2 * 60 * 1000, + } as RemoteData); spyOn(rdbService, 'buildList').and.returnValue(cold('a-b-c-d-e', { - a: remoteDataPageMocks.Success, + a: oldCachedSucceededData, b: remoteDataPageMocks.RequestPending, c: remoteDataPageMocks.ResponsePending, d: remoteDataPageMocks.Success, @@ -592,6 +614,22 @@ describe('BaseDataService', () => { }); }); + it('should emit the first completed RemoteData since the request was made', () => { + testScheduler.run(({ cold, expectObservable }) => { + spyOn(rdbService, 'buildList').and.returnValue(cold('a-b', { + a: remoteDataPageMocks.Success, + b: remoteDataPageMocks.SuccessStale, + })); + const expected = 'a-b'; + const values = { + a: remoteDataPageMocks.Success, + b: remoteDataPageMocks.SuccessStale, + }; + + expectObservable(service.findListByHref(selfLink, findListOptions, false, true, ...linksToFollow)).toBe(expected, values); + }); + }); + it(`should not emit a cached stale RemoteData, but only start emitting after the state first changes to RequestPending`, () => { testScheduler.run(({ cold, expectObservable }) => { spyOn(rdbService, 'buildList').and.returnValue(cold('a-b-c-d-e-f-g', { diff --git a/src/app/core/data/base/base-data.service.ts b/src/app/core/data/base/base-data.service.ts index d09ee21ee03..979379e95b2 100644 --- a/src/app/core/data/base/base-data.service.ts +++ b/src/app/core/data/base/base-data.service.ts @@ -285,6 +285,7 @@ export class BaseDataService implements HALDataServic map((href: string) => this.buildHrefFromFindOptions(href, {}, [], ...linksToFollow)), ); + const startTime: number = new Date().getTime(); this.createAndSendGetRequest(requestHref$, useCachedVersionIfAvailable); const response$: Observable> = this.rdbService.buildSingle(requestHref$, ...linksToFollow).pipe( @@ -292,7 +293,7 @@ export class BaseDataService implements HALDataServic // call it isn't immediately returned, but we wait until the remote data for the new request // is created. If useCachedVersionIfAvailable is false it also ensures you don't get a // cached completed object - skipWhile((rd: RemoteData) => rd.isStale || (!useCachedVersionIfAvailable && rd.hasCompleted)), + skipWhile((rd: RemoteData) => rd.isStale || (!useCachedVersionIfAvailable && rd.lastUpdated < startTime)), this.reRequestStaleRemoteData(reRequestOnStale, () => this.findByHref(href$, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow)), ); @@ -338,6 +339,7 @@ export class BaseDataService implements HALDataServic map((href: string) => this.buildHrefFromFindOptions(href, options, [], ...linksToFollow)), ); + const startTime: number = new Date().getTime(); this.createAndSendGetRequest(requestHref$, useCachedVersionIfAvailable); const response$: Observable>> = this.rdbService.buildList(requestHref$, ...linksToFollow).pipe( @@ -345,7 +347,7 @@ export class BaseDataService implements HALDataServic // call it isn't immediately returned, but we wait until the remote data for the new request // is created. If useCachedVersionIfAvailable is false it also ensures you don't get a // cached completed object - skipWhile((rd: RemoteData>) => rd.isStale || (!useCachedVersionIfAvailable && rd.hasCompleted)), + skipWhile((rd: RemoteData>) => rd.isStale || (!useCachedVersionIfAvailable && rd.lastUpdated < startTime)), this.reRequestStaleRemoteData(reRequestOnStale, () => this.findListByHref(href$, options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow)), ); diff --git a/src/app/core/data/base/delete-data.spec.ts b/src/app/core/data/base/delete-data.spec.ts index 53d651402f1..f3fa72a285a 100644 --- a/src/app/core/data/base/delete-data.spec.ts +++ b/src/app/core/data/base/delete-data.spec.ts @@ -209,6 +209,11 @@ describe('DeleteDataImpl', () => { method: RestRequestMethod.DELETE, href: 'some-href?copyVirtualMetadata=a©VirtualMetadata=b©VirtualMetadata=c', })); + + const callback = (rdbService.buildFromRequestUUIDAndAwait as jasmine.Spy).calls.argsFor(0)[1]; + callback(); + expect(service.invalidateByHref).toHaveBeenCalledWith('some-href'); + done(); }); }); diff --git a/src/app/core/data/base/delete-data.ts b/src/app/core/data/base/delete-data.ts index e758ee40fa0..c47a6af8474 100644 --- a/src/app/core/data/base/delete-data.ts +++ b/src/app/core/data/base/delete-data.ts @@ -75,15 +75,16 @@ export class DeleteDataImpl extends IdentifiableDataS deleteByHref(href: string, copyVirtualMetadata?: string[]): Observable> { const requestId = this.requestService.generateRequestId(); + let deleteHref: string = href; if (copyVirtualMetadata) { copyVirtualMetadata.forEach((id) => - href += (href.includes('?') ? '&' : '?') + deleteHref += (deleteHref.includes('?') ? '&' : '?') + 'copyVirtualMetadata=' + id, ); } - const request = new DeleteRequest(requestId, href); + const request = new DeleteRequest(requestId, deleteHref); if (hasValue(this.responseMsToLive)) { request.responseMsToLive = this.responseMsToLive; } diff --git a/src/app/core/data/base/identifiable-data.service.spec.ts b/src/app/core/data/base/identifiable-data.service.spec.ts index 528d6c4945f..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,10 +5,7 @@ * * http://www.dspace.org/license/ */ -import { - Observable, - of as observableOf, -} from 'rxjs'; +import { of as observableOf } from 'rxjs'; import { TestScheduler } from 'rxjs/testing'; import { getMockRemoteDataBuildService } from '../../../shared/mocks/remote-data-build.service.mock'; @@ -18,14 +15,14 @@ import { followLink } from '../../../shared/utils/follow-link-config.model'; import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service'; import { ObjectCacheService } from '../../cache/object-cache.service'; import { HALEndpointService } from '../../shared/hal-endpoint.service'; -import { FindListOptions } from '../find-list-options.model'; import { RemoteData } from '../remote-data'; import { RequestService } from '../request.service'; import { RequestEntryState } from '../request-entry-state.model'; import { EMBED_SEPARATOR } from './base-data.service'; import { IdentifiableDataService } from './identifiable-data.service'; -const endpoint = 'https://rest.api/core'; +const base = 'https://rest.api/core'; +const endpoint = 'test'; class TestService extends IdentifiableDataService { constructor( @@ -34,11 +31,7 @@ class TestService extends IdentifiableDataService { protected objectCache: ObjectCacheService, protected halService: HALEndpointService, ) { - super(undefined, requestService, rdbService, objectCache, halService); - } - - public getBrowseEndpoint(options: FindListOptions = {}, linkPath: string = this.linkPath): Observable { - return observableOf(endpoint); + super(endpoint, requestService, rdbService, objectCache, halService); } } @@ -55,7 +48,7 @@ describe('IdentifiableDataService', () => { function initTestService(): TestService { requestService = getMockRequestService(); - halService = new HALEndpointServiceStub('url') as any; + halService = new HALEndpointServiceStub(base) as any; rdbService = getMockRemoteDataBuildService(); objectCache = { @@ -147,4 +140,12 @@ describe('IdentifiableDataService', () => { expect(result).toEqual(expected); }); }); + + describe('invalidateById', () => { + it('should invalidate the correct resource by href', () => { + spyOn(service, 'invalidateByHref').and.returnValue(observableOf(true)); + service.invalidateById('123'); + expect(service.invalidateByHref).toHaveBeenCalledWith(`${base}/${endpoint}/123`); + }); + }); }); diff --git a/src/app/core/data/base/identifiable-data.service.ts b/src/app/core/data/base/identifiable-data.service.ts index da3167903e0..ba368d21ca1 100644 --- a/src/app/core/data/base/identifiable-data.service.ts +++ b/src/app/core/data/base/identifiable-data.service.ts @@ -6,7 +6,11 @@ * http://www.dspace.org/license/ */ import { Observable } from 'rxjs'; -import { map } from 'rxjs/operators'; +import { + map, + switchMap, + take, +} from 'rxjs/operators'; import { FollowLinkConfig } from '../../../shared/utils/follow-link-config.model'; import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service'; @@ -81,4 +85,19 @@ export class IdentifiableDataService extends BaseData return this.getEndpoint().pipe( map((endpoint: string) => this.getIDHref(endpoint, resourceID, ...linksToFollow))); } + + /** + * Invalidate a cached resource by its identifier + * @param resourceID the identifier of the resource to invalidate + */ + invalidateById(resourceID: string): Observable { + const ok$ = this.getIDHrefObs(resourceID).pipe( + take(1), + switchMap((href) => this.invalidateByHref(href)), + ); + + ok$.subscribe(); + + return ok$; + } } diff --git a/src/app/core/data/bitstream-data.service.spec.ts b/src/app/core/data/bitstream-data.service.spec.ts index 95fe5f593fd..91b5132ce06 100644 --- a/src/app/core/data/bitstream-data.service.spec.ts +++ b/src/app/core/data/bitstream-data.service.spec.ts @@ -138,27 +138,27 @@ describe('BitstreamDataService', () => { describe('findPrimaryBitstreamByItemAndName', () => { it('should return primary bitstream', () => { - const exprected$ = cold('(a|)', { a: bitstream1 } ); + const expected$ = cold('(a|)', { a: bitstream1 } ); const bundle = Object.assign(new Bundle(), { primaryBitstream: observableOf(createSuccessfulRemoteDataObject(bitstream1)), }); spyOn(bundleDataService, 'findByItemAndName').and.returnValue(observableOf(createSuccessfulRemoteDataObject(bundle))); - expect(service.findPrimaryBitstreamByItemAndName(ItemMock, 'ORIGINAL')).toBeObservable(exprected$); + expect(service.findPrimaryBitstreamByItemAndName(ItemMock, 'ORIGINAL')).toBeObservable(expected$); }); it('should return null if primary bitstream has not be succeeded ', () => { - const exprected$ = cold('(a|)', { a: null } ); + const expected$ = cold('(a|)', { a: null } ); const bundle = Object.assign(new Bundle(), { primaryBitstream: observableOf(createFailedRemoteDataObject()), }); spyOn(bundleDataService, 'findByItemAndName').and.returnValue(observableOf(createSuccessfulRemoteDataObject(bundle))); - expect(service.findPrimaryBitstreamByItemAndName(ItemMock, 'ORIGINAL')).toBeObservable(exprected$); + expect(service.findPrimaryBitstreamByItemAndName(ItemMock, 'ORIGINAL')).toBeObservable(expected$); }); it('should return EMPTY if nothing where found', () => { - const exprected$ = cold('(|)', {} ); + const expected$ = cold('(|)', {} ); spyOn(bundleDataService, 'findByItemAndName').and.returnValue(observableOf(createFailedRemoteDataObject())); - expect(service.findPrimaryBitstreamByItemAndName(ItemMock, 'ORIGINAL')).toBeObservable(exprected$); + expect(service.findPrimaryBitstreamByItemAndName(ItemMock, 'ORIGINAL')).toBeObservable(expected$); }); }); diff --git a/src/app/core/data/bitstream-data.service.ts b/src/app/core/data/bitstream-data.service.ts index 81d1d74535b..b8776d25309 100644 --- a/src/app/core/data/bitstream-data.service.ts +++ b/src/app/core/data/bitstream-data.service.ts @@ -242,7 +242,7 @@ export class BitstreamDataService extends IdentifiableDataService imp * @param reRequestOnStale Whether or not the request should automatically be re- * requested after the response becomes stale * @return {Observable} - * Return an observable that constains primary bitstream information or null + * Return an observable that contains primary bitstream information or null */ public findPrimaryBitstreamByItemAndName(item: Item, bundleName: string, useCachedVersionIfAvailable = true, reRequestOnStale = true): Observable { return this.bundleService.findByItemAndName(item, bundleName, useCachedVersionIfAvailable, reRequestOnStale, followLink('primaryBitstream')).pipe( diff --git a/src/app/core/data/collection-data.service.spec.ts b/src/app/core/data/collection-data.service.spec.ts index 431fe941bba..f27695c3261 100644 --- a/src/app/core/data/collection-data.service.spec.ts +++ b/src/app/core/data/collection-data.service.spec.ts @@ -151,7 +151,7 @@ describe('CollectionDataService', () => { expect(service.getAuthorizedCollection).toHaveBeenCalledWith(queryString); }); - it('should return a RemoteData> for the getAuthorizedCollection', () => { + it('should return a RemoteData> for the getAuthorizedCollection', () => { const result = service.getAuthorizedCollection(queryString); const expected = cold('a|', { a: paginatedListRD, @@ -166,7 +166,7 @@ describe('CollectionDataService', () => { expect(service.getAuthorizedCollectionByCommunity).toHaveBeenCalledWith(communityId, queryString); }); - it('should return a RemoteData> for the getAuthorizedCollectionByCommunity', () => { + it('should return a RemoteData> for the getAuthorizedCollectionByCommunity', () => { const result = service.getAuthorizedCollectionByCommunity(communityId, queryString); const expected = cold('a|', { a: paginatedListRD, @@ -206,12 +206,12 @@ describe('CollectionDataService', () => { /** * Create a CollectionDataService used for testing - * @param reponse$ Supply a RemoteData to be returned by the REST API (optional) + * @param response$ Supply a RemoteData to be returned by the REST API (optional) */ - function createService(reponse$?: Observable>) { + function createService(response$?: Observable>) { requestService = getMockRequestService(); - let buildResponse$ = reponse$; - if (hasNoValue(reponse$)) { + let buildResponse$ = response$; + if (hasNoValue(response$)) { buildResponse$ = createSuccessfulRemoteDataObject$({}); } rdbService = jasmine.createSpyObj('rdbService', { diff --git a/src/app/core/data/dspace-rest-response-parsing.service.ts b/src/app/core/data/dspace-rest-response-parsing.service.ts index 0177a9813aa..1cd286427f3 100644 --- a/src/app/core/data/dspace-rest-response-parsing.service.ts +++ b/src/app/core/data/dspace-rest-response-parsing.service.ts @@ -120,6 +120,13 @@ export class DspaceRestResponseParsingService implements ResponseParsingService if (hasValue(match)) { embedAltUrl = new URLCombiner(embedAltUrl, `?size=${match.size}`).toString(); } + if (data._embedded[property] == null) { + // Embedded object is null, meaning it exists (not undefined), but had an empty response (204) -> cache it as null + this.addToObjectCache(null, request, data, embedAltUrl); + } else if (!isCacheableObject(data._embedded[property])) { + // Embedded object exists, but doesn't contain a self link -> cache it using the alternative link instead + this.objectCache.add(data._embedded[property], hasValue(request.responseMsToLive) ? request.responseMsToLive : environment.cache.msToLive.default, request.uuid, embedAltUrl); + } this.process(data._embedded[property], request, embedAltUrl); }); } @@ -237,7 +244,7 @@ export class DspaceRestResponseParsingService implements ResponseParsingService * @param alternativeURL an alternative url that can be used to retrieve the object */ addToObjectCache(co: CacheableObject, request: RestRequest, data: any, alternativeURL?: string): void { - if (!isCacheableObject(co)) { + if (hasValue(co) && !isCacheableObject(co)) { const type = hasValue(data) && hasValue(data.type) ? data.type : 'object'; let dataJSON: string; if (hasValue(data._embedded)) { @@ -251,7 +258,7 @@ export class DspaceRestResponseParsingService implements ResponseParsingService return; } - if (alternativeURL === co._links.self.href) { + if (hasValue(co) && alternativeURL === co._links.self.href) { alternativeURL = undefined; } diff --git a/src/app/core/data/feature-authorization/authorization-data.service.spec.ts b/src/app/core/data/feature-authorization/authorization-data.service.spec.ts index 4048efe4ff5..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 @@ -72,7 +72,7 @@ describe('AuthorizationDataService', () => { const ePersonUuid = 'fake-eperson-uuid'; function createExpected(providedObjectUrl: string, providedEPersonUuid?: string, providedFeatureId?: FeatureID): FindListOptions { - const searchParams = [new RequestParam('uri', providedObjectUrl)]; + const searchParams = [new RequestParam('uri', providedObjectUrl, false)]; if (hasValue(providedFeatureId)) { searchParams.push(new RequestParam('feature', providedFeatureId)); } diff --git a/src/app/core/data/feature-authorization/authorization-data.service.ts b/src/app/core/data/feature-authorization/authorization-data.service.ts index 93d8f6b3ecf..e5efbae671c 100644 --- a/src/app/core/data/feature-authorization/authorization-data.service.ts +++ b/src/app/core/data/feature-authorization/authorization-data.service.ts @@ -147,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 f763b1a38d8..cd4bc452a5e 100644 --- a/src/app/core/data/feature-authorization/authorization-utils.ts +++ b/src/app/core/data/feature-authorization/authorization-utils.ts @@ -91,5 +91,5 @@ export const oneAuthorizationMatchesFeature = (featureID: FeatureID) => return observableOf([]); } }), - map((features: Feature[]) => features.filter((feature: Feature) => feature.id === featureID).length > 0), + map((features: Feature[]) => features.filter((feature: Feature) => feature.id === featureID.valueOf()).length > 0), ); diff --git a/src/app/core/data/feature-authorization/feature-authorization-guard/collection-administrator.guard.ts b/src/app/core/data/feature-authorization/feature-authorization-guard/collection-administrator.guard.ts index 5af9f18b337..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,35 +1,13 @@ -import { Injectable } from '@angular/core'; -import { - ActivatedRouteSnapshot, - Router, - RouterStateSnapshot, -} from '@angular/router'; -import { - Observable, - of as observableOf, -} from 'rxjs'; +import { CanActivateFn } from '@angular/router'; +import { of as observableOf } from 'rxjs'; -import { AuthService } from '../../../auth/auth.service'; -import { AuthorizationDataService } from '../authorization-data.service'; import { FeatureID } from '../feature-id'; -import { SingleFeatureAuthorizationGuard } from './single-feature-authorization.guard'; +import { singleFeatureAuthorizationGuard } from './single-feature-authorization.guard'; /** * Prevent unauthorized activating and loading of routes when the current authenticated user * isn't a Collection administrator + * Check group management rights */ -@Injectable({ - providedIn: 'root', -}) -export class CollectionAdministratorGuard extends SingleFeatureAuthorizationGuard { - constructor(protected authorizationService: AuthorizationDataService, protected router: Router, protected authService: AuthService) { - super(authorizationService, router, authService); - } - - /** - * Check group management rights - */ - getFeatureID(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { - return observableOf(FeatureID.IsCollectionAdmin); - } -} +export const collectionAdministratorGuard: CanActivateFn = + singleFeatureAuthorizationGuard(() => observableOf(FeatureID.IsCollectionAdmin)); diff --git a/src/app/core/data/feature-authorization/feature-authorization-guard/community-administrator.guard.ts b/src/app/core/data/feature-authorization/feature-authorization-guard/community-administrator.guard.ts index 2092fce1100..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,35 +1,13 @@ -import { Injectable } from '@angular/core'; -import { - ActivatedRouteSnapshot, - Router, - RouterStateSnapshot, -} from '@angular/router'; -import { - Observable, - of as observableOf, -} from 'rxjs'; +import { CanActivateFn } from '@angular/router'; +import { of as observableOf } from 'rxjs'; -import { AuthService } from '../../../auth/auth.service'; -import { AuthorizationDataService } from '../authorization-data.service'; import { FeatureID } from '../feature-id'; -import { SingleFeatureAuthorizationGuard } from './single-feature-authorization.guard'; +import { singleFeatureAuthorizationGuard } from './single-feature-authorization.guard'; /** * Prevent unauthorized activating and loading of routes when the current authenticated user * isn't a Community administrator + * Check group management rights */ -@Injectable({ - providedIn: 'root', -}) -export class CommunityAdministratorGuard extends SingleFeatureAuthorizationGuard { - constructor(protected authorizationService: AuthorizationDataService, protected router: Router, protected authService: AuthService) { - super(authorizationService, router, authService); - } - - /** - * Check group management rights - */ - getFeatureID(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { - return observableOf(FeatureID.IsCommunityAdmin); - } -} +export const communityAdministratorGuard: CanActivateFn = + singleFeatureAuthorizationGuard(() => observableOf(FeatureID.IsCommunityAdmin)); diff --git a/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard.spec.ts b/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard.spec.ts index 2cd9fefa934..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,8 +1,8 @@ +import { TestBed } from '@angular/core/testing'; import { - ActivatedRouteSnapshot, ResolveFn, Router, - RouterStateSnapshot, + UrlTree, } from '@angular/router'; import { Observable, @@ -12,52 +12,39 @@ import { import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils'; import { AuthService } from '../../../auth/auth.service'; import { DSpaceObject } from '../../../shared/dspace-object.model'; -import { Item } from '../../../shared/item.model'; import { RemoteData } from '../../remote-data'; import { AuthorizationDataService } from '../authorization-data.service'; import { FeatureID } from '../feature-id'; -import { DsoPageSingleFeatureGuard } from './dso-page-single-feature.guard'; - -const object = { - self: 'test-selflink', -} as DSpaceObject; - -const testResolver: ResolveFn> = () => createSuccessfulRemoteDataObject$(object); - -/** - * Test implementation of abstract class DsoPageSingleFeatureGuard - */ -class DsoPageSingleFeatureGuardImpl extends DsoPageSingleFeatureGuard { - - protected resolver: ResolveFn> = testResolver; - - constructor(protected authorizationService: AuthorizationDataService, - protected router: Router, - protected authService: AuthService, - protected featureID: FeatureID) { - super(authorizationService, router, authService); - } +import { dsoPageSingleFeatureGuard } from './dso-page-single-feature.guard'; +import { + defaultDSOGetObjectUrl, + getRouteWithDSOId, +} from './dso-page-some-feature.guard'; - 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: ResolveFn>; + let object: DSpaceObject; let route; let parentRoute; + let featureId: FeatureID; + function init() { + object = { + self: 'test-selflink', + } as DSpaceObject; + authorizationService = jasmine.createSpyObj('authorizationService', { isAuthorized: observableOf(true), }); router = jasmine.createSpyObj('router', { parseUrl: {}, }); + resolver = () => createSuccessfulRemoteDataObject$(object); authService = jasmine.createSpyObj('authService', { isAuthenticated: observableOf(true), }); @@ -71,16 +58,25 @@ describe('DsoPageSingleFeatureGuard', () => { }, parent: parentRoute, }; - guard = new DsoPageSingleFeatureGuardImpl(authorizationService, router, authService, undefined); + + featureId = FeatureID.LoginOnBehalfOf; + + TestBed.configureTestingModule({ + providers: [ + { provide: AuthorizationDataService, useValue: authorizationService }, + { provide: Router, useValue: router }, + { provide: AuthService, useValue: authService }, + ], + }); } beforeEach(() => { init(); }); - describe('getObjectUrl', () => { + describe('defaultDSOGetObjectUrl', () => { it('should return the resolved object\'s selflink', (done) => { - guard.getObjectUrl(route, undefined).subscribe((selflink) => { + defaultDSOGetObjectUrl(resolver)(route, undefined).subscribe((selflink) => { expect(selflink).toEqual(object.self); done(); }); @@ -89,8 +85,23 @@ describe('DsoPageSingleFeatureGuard', () => { describe('getRouteWithDSOId', () => { it('should return the route that has the UUID of the DSO', () => { - const foundRoute = (guard as any).getRouteWithDSOId(route); + const foundRoute = getRouteWithDSOId(route); expect(foundRoute).toBe(parentRoute); }); }); + + describe('dsoPageSingleFeatureGuard', () => { + it('should call authorizationService.isAuthenticated with the appropriate arguments', (done) => { + const result$ = TestBed.runInInjectionContext(() => { + return dsoPageSingleFeatureGuard( + () => resolver, () => observableOf(featureId), + )(route, { url: 'current-url' } as any); + }) as Observable; + + result$.subscribe(() => { + expect(authorizationService.isAuthorized).toHaveBeenCalledWith(featureId, object.self, undefined); + done(); + }); + }); + }); }); diff --git a/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard.ts b/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-single-feature.guard.ts index 1f75df846b8..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,31 +1,27 @@ import { ActivatedRouteSnapshot, + CanActivateFn, + ResolveFn, RouterStateSnapshot, } from '@angular/router'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; import { DSpaceObject } from '../../../shared/dspace-object.model'; +import { RemoteData } from '../../remote-data'; import { FeatureID } from '../feature-id'; -import { DsoPageSomeFeatureGuard } from './dso-page-some-feature.guard'; +import { dsoPageSomeFeatureGuard } from './dso-page-some-feature.guard'; +import { SingleFeatureGuardParamFn } from './single-feature-authorization.guard'; /** * Abstract Guard for preventing unauthorized access to {@link DSpaceObject} pages that require rights for a specific feature * This guard utilizes a resolver to retrieve the relevant object to check authorizations for */ -export abstract class DsoPageSingleFeatureGuard extends DsoPageSomeFeatureGuard { - /** - * The features to check authorization for - */ - getFeatureIDs(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { - return this.getFeatureID(route, state).pipe( - map((featureID) => [featureID]), - ); - } - - /** - * The type of feature to check authorization for - * Override this method to define a feature - */ - abstract getFeatureID(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable; -} +export const dsoPageSingleFeatureGuard = ( + getResolveFn: () => ResolveFn>, + getFeatureID: SingleFeatureGuardParamFn, +): CanActivateFn => dsoPageSomeFeatureGuard( + getResolveFn, + (route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable => getFeatureID(route, state).pipe( + map((featureID: FeatureID) => [featureID]), + )); diff --git a/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-some-feature.guard.spec.ts b/src/app/core/data/feature-authorization/feature-authorization-guard/dso-page-some-feature.guard.spec.ts index 4e741b5b710..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,8 +1,8 @@ +import { TestBed } from '@angular/core/testing'; import { - ActivatedRouteSnapshot, ResolveFn, Router, - RouterStateSnapshot, + UrlTree, } from '@angular/router'; import { Observable, @@ -12,53 +12,39 @@ import { import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils'; import { AuthService } from '../../../auth/auth.service'; import { DSpaceObject } from '../../../shared/dspace-object.model'; -import { Item } from '../../../shared/item.model'; import { RemoteData } from '../../remote-data'; import { AuthorizationDataService } from '../authorization-data.service'; import { FeatureID } from '../feature-id'; -import { DsoPageSomeFeatureGuard } from './dso-page-some-feature.guard'; - -const object = { - self: 'test-selflink', -} as DSpaceObject; - -const testResolver: ResolveFn> = () => createSuccessfulRemoteDataObject$(object); - -/** - * Test implementation of abstract class DsoPageSomeFeatureGuard - */ -class DsoPageSomeFeatureGuardImpl extends DsoPageSomeFeatureGuard { - - protected resolver: ResolveFn> = testResolver; - - constructor(protected authorizationService: AuthorizationDataService, - protected router: Router, - protected authService: AuthService, - protected featureIDs: FeatureID[]) { - super(authorizationService, router, authService); - } +import { + defaultDSOGetObjectUrl, + dsoPageSomeFeatureGuard, + getRouteWithDSOId, +} from './dso-page-some-feature.guard'; - 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: ResolveFn>; + let object: DSpaceObject; let route; let parentRoute; + let featureIds: FeatureID[]; + function init() { + object = { + self: 'test-selflink', + } as DSpaceObject; + featureIds = [FeatureID.LoginOnBehalfOf, FeatureID.CanDelete]; authorizationService = jasmine.createSpyObj('authorizationService', { isAuthorized: observableOf(true), }); router = jasmine.createSpyObj('router', { parseUrl: {}, }); + resolver = () => createSuccessfulRemoteDataObject$(object); authService = jasmine.createSpyObj('authService', { isAuthenticated: observableOf(true), }); @@ -72,16 +58,25 @@ describe('DsoPageSomeFeatureGuard', () => { }, parent: parentRoute, }; - guard = new DsoPageSomeFeatureGuardImpl(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(); }); @@ -90,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 c887b8ae2ab..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,7 +1,7 @@ import { ActivatedRouteSnapshot, + CanActivateFn, ResolveFn, - Router, RouterStateSnapshot, } from '@angular/router'; import { Observable } from 'rxjs'; @@ -11,47 +11,50 @@ import { hasNoValue, hasValue, } from '../../../../shared/empty.util'; -import { AuthService } from '../../../auth/auth.service'; import { DSpaceObject } from '../../../shared/dspace-object.model'; import { getAllSucceededRemoteDataPayload } from '../../../shared/operators'; import { RemoteData } from '../../remote-data'; -import { AuthorizationDataService } from '../authorization-data.service'; -import { SomeFeatureAuthorizationGuard } from './some-feature-authorization.guard'; +import { FeatureID } from '../feature-id'; +import { + someFeatureAuthorizationGuard, + SomeFeatureGuardParamFn, + StringGuardParamFn, +} from './some-feature-authorization.guard'; + +export declare type DSOGetObjectURlFn = (resolve: ResolveFn>) => StringGuardParamFn; + /** - * Abstract Guard for preventing unauthorized access to {@link DSpaceObject} pages that require rights for any specific feature in a list - * This guard utilizes a resolver to retrieve the relevant object to check authorizations for + * Method to resolve resolve (parent) route that contains the UUID of the DSO + * @param route The current route */ -export abstract class DsoPageSomeFeatureGuard extends SomeFeatureAuthorizationGuard { +export const getRouteWithDSOId = (route: ActivatedRouteSnapshot): ActivatedRouteSnapshot => { + let routeWithDSOId = route; + while (hasNoValue(routeWithDSOId.params.id) && hasValue(routeWithDSOId.parent)) { + routeWithDSOId = routeWithDSOId.parent; + } + return routeWithDSOId; +}; - protected abstract resolver: ResolveFn>; - constructor(protected authorizationService: AuthorizationDataService, - protected router: Router, - protected authService: AuthService) { - super(authorizationService, router, authService); - } - /** - * 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(routeWithObjectID, state) as Observable>).pipe( +export const defaultDSOGetObjectUrl: DSOGetObjectURlFn = (resolve: ResolveFn>): StringGuardParamFn => { + return (route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable => { + const routeWithObjectID = getRouteWithDSOId(route); + return (resolve(routeWithObjectID, state) as Observable>).pipe( getAllSucceededRemoteDataPayload(), map((dso) => dso.self), ); - } + }; +}; - /** - * Method to resolve (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 5f32e268515..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,35 +1,12 @@ -import { Injectable } from '@angular/core'; -import { - ActivatedRouteSnapshot, - Router, - RouterStateSnapshot, -} from '@angular/router'; -import { - Observable, - of as observableOf, -} from 'rxjs'; +import { CanActivateFn } from '@angular/router'; +import { of as observableOf } from 'rxjs'; -import { AuthService } from '../../../auth/auth.service'; -import { AuthorizationDataService } from '../authorization-data.service'; import { FeatureID } from '../feature-id'; -import { SingleFeatureAuthorizationGuard } from './single-feature-authorization.guard'; +import { singleFeatureAuthorizationGuard } from './single-feature-authorization.guard'; /** * Prevent unauthorized activating and loading of routes when the current authenticated user doesn't have group * management rights */ -@Injectable({ - providedIn: 'root', -}) -export class GroupAdministratorGuard extends SingleFeatureAuthorizationGuard { - constructor(protected authorizationService: AuthorizationDataService, protected router: Router, protected authService: AuthService) { - super(authorizationService, router, authService); - } - - /** - * Check group management rights - */ - getFeatureID(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { - return observableOf(FeatureID.CanManageGroups); - } -} +export const groupAdministratorGuard: CanActivateFn = + singleFeatureAuthorizationGuard(() => observableOf(FeatureID.CanManageGroups)); diff --git a/src/app/core/data/feature-authorization/feature-authorization-guard/single-feature-authorization.guard.spec.ts b/src/app/core/data/feature-authorization/feature-authorization-guard/single-feature-authorization.guard.spec.ts index e789f8c4735..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,7 +1,10 @@ import { - ActivatedRouteSnapshot, + TestBed, + waitForAsync, +} from '@angular/core/testing'; +import { Router, - RouterStateSnapshot, + UrlTree, } from '@angular/router'; import { Observable, @@ -11,37 +14,9 @@ import { import { AuthService } from '../../../auth/auth.service'; import { AuthorizationDataService } from '../authorization-data.service'; import { FeatureID } from '../feature-id'; -import { SingleFeatureAuthorizationGuard } from './single-feature-authorization.guard'; - -/** - * 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; @@ -64,17 +39,36 @@ describe('SingleFeatureAuthorizationGuard', () => { authService = jasmine.createSpyObj('authService', { isAuthenticated: observableOf(true), }); - guard = new SingleFeatureAuthorizationGuardImpl(authorizationService, router, authService, featureId, objectUrl, ePersonUuid); + + TestBed.configureTestingModule({ + providers: [ + { provide: AuthorizationDataService, useValue: authorizationService }, + { provide: Router, useValue: router }, + { provide: AuthService, useValue: authService }, + ], + }); } - beforeEach(() => { + beforeEach(waitForAsync(() => { init(); - }); + })); describe('canActivate', () => { - it('should call authorizationService.isAuthenticated with the appropriate arguments', () => { - guard.canActivate(undefined, { url: 'current-url' } as any).subscribe(); - expect(authorizationService.isAuthorized).toHaveBeenCalledWith(featureId, objectUrl, ePersonUuid); + it('should call authorizationService.isAuthenticated with the appropriate arguments', (done: DoneFn) => { + const result$ = TestBed.runInInjectionContext(() => { + return singleFeatureAuthorizationGuard( + () => observableOf(featureId), + () => observableOf(objectUrl), + () => observableOf(ePersonUuid), + )(undefined, { url: 'current-url' } as any); + }) as Observable; + + + result$.subscribe(() => { + expect(authorizationService.isAuthorized).toHaveBeenCalledWith(featureId, objectUrl, ePersonUuid); + done(); + }); }); + }); }); diff --git a/src/app/core/data/feature-authorization/feature-authorization-guard/single-feature-authorization.guard.ts b/src/app/core/data/feature-authorization/feature-authorization-guard/single-feature-authorization.guard.ts index cd9f615aa7e..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,31 +1,35 @@ import { ActivatedRouteSnapshot, + CanActivateFn, RouterStateSnapshot, } from '@angular/router'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; import { FeatureID } from '../feature-id'; -import { SomeFeatureAuthorizationGuard } from './some-feature-authorization.guard'; +import { + someFeatureAuthorizationGuard, + StringGuardParamFn, +} from './some-feature-authorization.guard'; + +export declare type SingleFeatureGuardParamFn = (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => Observable; /** - * Abstract Guard for preventing unauthorized activating and loading of routes when a user - * doesn't have authorized rights on a specific feature and/or object. - * Override the desired getters in the parent class for checking specific authorization on a feature and/or object. + * Guard for preventing unauthorized activating and loading of routes when a user doesn't have + * authorized rights on a specific feature and/or object. + * + * @param getFeatureID The feature to check authorization for + * @param getObjectUrl The URL of the object to check if the user has authorized rights for, + * Optional, if not provided, the {@link Site}'s URL will be assumed + * @param getEPersonUuid The UUID of the user to check authorization rights for. + * Optional, if not provided, the authenticated user's UUID will be assumed. */ -export abstract class SingleFeatureAuthorizationGuard extends SomeFeatureAuthorizationGuard { - /** - * The features to check authorization for - */ - getFeatureIDs(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { - return this.getFeatureID(route, state).pipe( - map((featureID) => [featureID]), - ); - } - /** - * The type of feature to check authorization for - * Override this method to define a feature - */ - abstract getFeatureID(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable; -} +export const singleFeatureAuthorizationGuard = ( + getFeatureID: SingleFeatureGuardParamFn, + getObjectUrl?: StringGuardParamFn, + getEPersonUuid?: StringGuardParamFn, +): CanActivateFn => someFeatureAuthorizationGuard( + (route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable => getFeatureID(route, state).pipe( + map((featureID: FeatureID) => [featureID]), + ), getObjectUrl, getEPersonUuid); diff --git a/src/app/core/data/feature-authorization/feature-authorization-guard/site-administrator.guard.ts b/src/app/core/data/feature-authorization/feature-authorization-guard/site-administrator.guard.ts index e4f0705defb..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,33 +1,12 @@ -import { Injectable } from '@angular/core'; -import { - ActivatedRouteSnapshot, - Router, - RouterStateSnapshot, -} from '@angular/router'; -import { - Observable, - of as observableOf, -} from 'rxjs'; +import { CanActivateFn } from '@angular/router'; +import { of as observableOf } from 'rxjs'; -import { AuthService } from '../../../auth/auth.service'; -import { AuthorizationDataService } from '../authorization-data.service'; import { FeatureID } from '../feature-id'; -import { SingleFeatureAuthorizationGuard } from './single-feature-authorization.guard'; +import { singleFeatureAuthorizationGuard } from './single-feature-authorization.guard'; /** * Prevent unauthorized activating and loading of routes when the current authenticated user doesn't have administrator * rights to the {@link Site} */ -@Injectable({ providedIn: 'root' }) -export class SiteAdministratorGuard extends SingleFeatureAuthorizationGuard { - constructor(protected authorizationService: AuthorizationDataService, protected router: Router, protected authService: AuthService) { - super(authorizationService, router, authService); - } - - /** - * Check administrator authorization rights - */ - getFeatureID(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { - return observableOf(FeatureID.AdministratorOf); - } -} +export const siteAdministratorGuard: CanActivateFn = + singleFeatureAuthorizationGuard(() => observableOf(FeatureID.AdministratorOf)); diff --git a/src/app/core/data/feature-authorization/feature-authorization-guard/site-register.guard.ts b/src/app/core/data/feature-authorization/feature-authorization-guard/site-register.guard.ts index a1a78dc67d6..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,33 +1,12 @@ -import { Injectable } from '@angular/core'; -import { - ActivatedRouteSnapshot, - Router, - RouterStateSnapshot, -} from '@angular/router'; -import { - Observable, - of as observableOf, -} from 'rxjs'; +import { CanActivateFn } from '@angular/router'; +import { of as observableOf } from 'rxjs'; -import { AuthService } from '../../../auth/auth.service'; -import { AuthorizationDataService } from '../authorization-data.service'; import { FeatureID } from '../feature-id'; -import { SingleFeatureAuthorizationGuard } from './single-feature-authorization.guard'; +import { singleFeatureAuthorizationGuard } from './single-feature-authorization.guard'; /** * Prevent unauthorized activating and loading of routes when the current authenticated user doesn't have registration * rights to the {@link Site} */ -@Injectable({ providedIn: 'root' }) -export class SiteRegisterGuard extends SingleFeatureAuthorizationGuard { - constructor(protected authorizationService: AuthorizationDataService, protected router: Router, protected authService: AuthService) { - super(authorizationService, router, authService); - } - - /** - * Check registration authorization rights - */ - getFeatureID(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { - return observableOf(FeatureID.EPersonRegistration); - } -} +export const siteRegisterGuard: CanActivateFn = + singleFeatureAuthorizationGuard(() => observableOf(FeatureID.EPersonRegistration)); diff --git a/src/app/core/data/feature-authorization/feature-authorization-guard/some-feature-authorization.guard.spec.ts b/src/app/core/data/feature-authorization/feature-authorization-guard/some-feature-authorization.guard.spec.ts index 53d77cadadf..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,7 +1,10 @@ import { - ActivatedRouteSnapshot, + TestBed, + waitForAsync, +} from '@angular/core/testing'; +import { Router, - RouterStateSnapshot, + UrlTree, } from '@angular/router'; import { Observable, @@ -11,37 +14,9 @@ import { import { AuthService } from '../../../auth/auth.service'; import { AuthorizationDataService } from '../authorization-data.service'; import { FeatureID } from '../feature-id'; -import { SomeFeatureAuthorizationGuard } from './some-feature-authorization.guard'; - -/** - * Test implementation of abstract class SomeFeatureAuthorizationGuard - * Provide the return values of the overwritten getters as constructor arguments - */ -class SomeFeatureAuthorizationGuardImpl extends SomeFeatureAuthorizationGuard { - constructor(protected authorizationService: AuthorizationDataService, - protected router: Router, - protected authService: AuthService, - protected featureIds: FeatureID[], - protected objectUrl: string, - protected ePersonUuid: string) { - super(authorizationService, router, authService); - } - - getFeatureIDs(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { - return observableOf(this.featureIds); - } - - getObjectUrl(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { - return observableOf(this.objectUrl); - } - - getEPersonUuid(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { - return observableOf(this.ePersonUuid); - } -} +import { someFeatureAuthorizationGuard } from './some-feature-authorization.guard'; describe('SomeFeatureAuthorizationGuard', () => { - let guard: SomeFeatureAuthorizationGuard; let authorizationService: AuthorizationDataService; let router: Router; let authService: AuthService; @@ -62,18 +37,27 @@ describe('SomeFeatureAuthorizationGuard', () => { return observableOf(authorizedFeatureIds.indexOf(featureId) > -1); }, }); + router = jasmine.createSpyObj('router', { parseUrl: {}, }); + authService = jasmine.createSpyObj('authService', { isAuthenticated: observableOf(true), }); - guard = new SomeFeatureAuthorizationGuardImpl(authorizationService, router, authService, featureIds, objectUrl, ePersonUuid); + + TestBed.configureTestingModule({ + providers: [ + { provide: AuthorizationDataService, useValue: authorizationService }, + { provide: Router, useValue: router }, + { provide: AuthService, useValue: authService }, + ], + }); } - beforeEach(() => { + beforeEach(waitForAsync(() => { init(); - }); + })); describe('canActivate', () => { describe('when the user isn\'t authorized', () => { @@ -82,7 +66,16 @@ describe('SomeFeatureAuthorizationGuard', () => { }); it('should not return true', (done) => { - guard.canActivate(undefined, { url: 'current-url' } as any).subscribe((result) => { + + const result$ = TestBed.runInInjectionContext(() => { + return someFeatureAuthorizationGuard( + () => observableOf(featureIds), + () => observableOf(objectUrl), + () => observableOf(ePersonUuid), + )(undefined, { url: 'current-url' } as any); + }) as Observable; + + result$.subscribe((result) => { expect(result).not.toEqual(true); done(); }); @@ -95,7 +88,16 @@ describe('SomeFeatureAuthorizationGuard', () => { }); it('should return true', (done) => { - guard.canActivate(undefined, { url: 'current-url' } as any).subscribe((result) => { + + const result$ = TestBed.runInInjectionContext(() => { + return someFeatureAuthorizationGuard( + () => observableOf(featureIds), + () => observableOf(objectUrl), + () => observableOf(ePersonUuid), + )(undefined, { url: 'current-url' } as any); + }) as Observable; + + result$.subscribe((result) => { expect(result).toEqual(true); done(); }); @@ -108,7 +110,16 @@ describe('SomeFeatureAuthorizationGuard', () => { }); it('should return true', (done) => { - guard.canActivate(undefined, { url: 'current-url' } as any).subscribe((result) => { + + const result$ = TestBed.runInInjectionContext(() => { + return someFeatureAuthorizationGuard( + () => observableOf(featureIds), + () => observableOf(objectUrl), + () => observableOf(ePersonUuid), + )(undefined, { url: 'current-url' } as any); + }) as Observable; + + result$.subscribe((result) => { expect(result).toEqual(true); done(); }); diff --git a/src/app/core/data/feature-authorization/feature-authorization-guard/some-feature-authorization.guard.ts b/src/app/core/data/feature-authorization/feature-authorization-guard/some-feature-authorization.guard.ts index 229321452f1..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,5 +1,7 @@ +import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, + CanActivateFn, Router, RouterStateSnapshot, UrlTree, @@ -16,49 +18,39 @@ import { returnForbiddenUrlTreeOrLoginOnAllFalse } from '../../../shared/authori import { AuthorizationDataService } from '../authorization-data.service'; import { FeatureID } from '../feature-id'; +export declare type SomeFeatureGuardParamFn = (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => Observable; +export declare type StringGuardParamFn = (route: ActivatedRouteSnapshot, state: RouterStateSnapshot) => Observable; +export const defaultStringGuardParamFn = () => observableOf(undefined); + /** - * Abstract Guard for preventing unauthorized activating and loading of routes when a user - * doesn't have authorized rights on any of the specified features and/or object. - * Override the desired getters in the parent class for checking specific authorization on a list of features and/or object. - */ -export abstract class SomeFeatureAuthorizationGuard { - 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 b301d550a12..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,35 +1,12 @@ -import { Injectable } from '@angular/core'; -import { - ActivatedRouteSnapshot, - Router, - RouterStateSnapshot, -} from '@angular/router'; -import { - Observable, - of as observableOf, -} from 'rxjs'; +import { CanActivateFn } from '@angular/router'; +import { of as observableOf } from 'rxjs'; -import { AuthService } from '../../../auth/auth.service'; -import { AuthorizationDataService } from '../authorization-data.service'; import { FeatureID } from '../feature-id'; -import { SingleFeatureAuthorizationGuard } from './single-feature-authorization.guard'; +import { singleFeatureAuthorizationGuard } from './single-feature-authorization.guard'; /** * Prevent unauthorized activating and loading of routes when the current authenticated user doesn't have group * management rights */ -@Injectable({ - providedIn: 'root', -}) -export class StatisticsAdministratorGuard extends SingleFeatureAuthorizationGuard { - constructor(protected authorizationService: AuthorizationDataService, protected router: Router, protected authService: AuthService) { - super(authorizationService, router, authService); - } - - /** - * Check group management rights - */ - getFeatureID(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { - return observableOf(FeatureID.CanViewUsageStatistics); - } -} +export const statisticsAdministratorGuard: CanActivateFn = + singleFeatureAuthorizationGuard(() => observableOf(FeatureID.CanViewUsageStatistics)); diff --git a/src/app/core/data/object-updates/object-updates.reducer.ts b/src/app/core/data/object-updates/object-updates.reducer.ts index e014889850c..cadae9ae838 100644 --- a/src/app/core/data/object-updates/object-updates.reducer.ts +++ b/src/app/core/data/object-updates/object-updates.reducer.ts @@ -61,6 +61,8 @@ export interface VirtualMetadataSource { export interface RelationshipIdentifiable extends Identifiable { nameVariant?: string; + originalItem: Item; + originalIsLeft: boolean relatedItem: Item; relationship: Relationship; type: RelationshipType; diff --git a/src/app/core/data/object-updates/object-updates.service.stub.ts b/src/app/core/data/object-updates/object-updates.service.stub.ts new file mode 100644 index 00000000000..c41728a338e --- /dev/null +++ b/src/app/core/data/object-updates/object-updates.service.stub.ts @@ -0,0 +1,28 @@ +export class ObjectUpdatesServiceStub { + + initialize = jasmine.createSpy('initialize'); + saveFieldUpdate = jasmine.createSpy('saveFieldUpdate'); + getObjectEntry = jasmine.createSpy('getObjectEntry'); + getFieldState = jasmine.createSpy('getFieldState'); + getFieldUpdates = jasmine.createSpy('getFieldUpdates'); + getFieldUpdatesExclusive = jasmine.createSpy('getFieldUpdatesExclusive'); + isValid = jasmine.createSpy('isValid'); + isValidPage = jasmine.createSpy('isValidPage'); + saveAddFieldUpdate = jasmine.createSpy('saveAddFieldUpdate'); + saveRemoveFieldUpdate = jasmine.createSpy('saveRemoveFieldUpdate'); + saveChangeFieldUpdate = jasmine.createSpy('saveChangeFieldUpdate'); + isSelectedVirtualMetadata = jasmine.createSpy('isSelectedVirtualMetadata'); + setSelectedVirtualMetadata = jasmine.createSpy('setSelectedVirtualMetadata'); + setEditableFieldUpdate = jasmine.createSpy('setEditableFieldUpdate'); + setValidFieldUpdate = jasmine.createSpy('setValidFieldUpdate'); + discardFieldUpdates = jasmine.createSpy('discardFieldUpdates'); + discardAllFieldUpdates = jasmine.createSpy('discardAllFieldUpdates'); + reinstateFieldUpdates = jasmine.createSpy('reinstateFieldUpdates'); + removeSingleFieldUpdate = jasmine.createSpy('removeSingleFieldUpdate'); + getUpdateFields = jasmine.createSpy('getUpdateFields'); + hasUpdates = jasmine.createSpy('hasUpdates'); + isReinstatable = jasmine.createSpy('isReinstatable'); + getLastModified = jasmine.createSpy('getLastModified'); + createPatch = jasmine.createSpy('getPatch'); + +} diff --git a/src/app/core/data/object-updates/object-updates.service.ts b/src/app/core/data/object-updates/object-updates.service.ts index 1f036ddfb1d..75b554d87b1 100644 --- a/src/app/core/data/object-updates/object-updates.service.ts +++ b/src/app/core/data/object-updates/object-updates.service.ts @@ -15,6 +15,7 @@ import { filter, map, switchMap, + take, } from 'rxjs/operators'; import { @@ -212,8 +213,14 @@ export class ObjectUpdatesService { * @param url The page's URL for which the changes are saved * @param field An updated field for the page's object */ - saveAddFieldUpdate(url: string, field: Identifiable) { + saveAddFieldUpdate(url: string, field: Identifiable): Observable { + const update$: Observable = this.getFieldUpdatesExclusive(url, [field]).pipe( + filter((fieldUpdates: FieldUpdates) => fieldUpdates[field.uuid].changeType === FieldChangeType.ADD), + take(1), + map(() => true), + ); this.saveFieldUpdate(url, field, FieldChangeType.ADD); + return update$; } /** @@ -221,8 +228,14 @@ export class ObjectUpdatesService { * @param url The page's URL for which the changes are saved * @param field An updated field for the page's object */ - saveRemoveFieldUpdate(url: string, field: Identifiable) { + saveRemoveFieldUpdate(url: string, field: Identifiable): Observable { + const update$: Observable = this.getFieldUpdatesExclusive(url, [field]).pipe( + filter((fieldUpdates: FieldUpdates) => fieldUpdates[field.uuid].changeType === FieldChangeType.REMOVE), + take(1), + map(() => true), + ); this.saveFieldUpdate(url, field, FieldChangeType.REMOVE); + return update$; } /** diff --git a/src/app/core/data/paginated-list.model.ts b/src/app/core/data/paginated-list.model.ts index e412af49863..ef2819afd8f 100644 --- a/src/app/core/data/paginated-list.model.ts +++ b/src/app/core/data/paginated-list.model.ts @@ -73,13 +73,13 @@ export class PaginatedList extends CacheableObject { * The type of the list */ @excludeFromEquals - type = PAGINATED_LIST; + type = PAGINATED_LIST; /** * The type of objects in the list */ @autoserialize - objectType?: ResourceType; + objectType?: ResourceType; /** * The list of objects that represents the current page @@ -90,13 +90,13 @@ export class PaginatedList extends CacheableObject { * the {@link PageInfo} object */ @autoserialize - pageInfo?: PageInfo; + pageInfo?: PageInfo; /** * The {@link HALLink}s for this PaginatedList */ @deserialize - _links: { + _links: { self: HALLink; page: HALLink[]; first?: HALLink; diff --git a/src/app/core/data/relationship-data.service.spec.ts b/src/app/core/data/relationship-data.service.spec.ts index 21365e52e1b..d4a6658f477 100644 --- a/src/app/core/data/relationship-data.service.spec.ts +++ b/src/app/core/data/relationship-data.service.spec.ts @@ -1,5 +1,11 @@ +import { TestBed } from '@angular/core/testing'; +import { Store } from '@ngrx/store'; +import { provideMockStore } from '@ngrx/store/testing'; import { of as observableOf } from 'rxjs'; +import { APP_CONFIG } from '../../../config/app-config.interface'; +import { environment } from '../../../environments/environment.test'; +import { PAGINATED_RELATIONS_TO_ITEMS_OPERATOR } from '../../item-page/simple/item-types/shared/item-relationships-utils'; import { getMockRemoteDataBuildServiceHrefMap } from '../../shared/mocks/remote-data-build.service.mock'; import { getMockRequestService } from '../../shared/mocks/request.service.mock'; import { @@ -11,7 +17,9 @@ import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-servic 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 { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { ObjectCacheService } from '../cache/object-cache.service'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; import { Item } from '../shared/item.model'; import { Relationship } from '../shared/item-relationships/relationship.model'; import { RelationshipType } from '../shared/item-relationships/relationship-type.model'; @@ -20,6 +28,7 @@ import { MetadataRepresentationType } from '../shared/metadata-representation/me import { PageInfo } from '../shared/page-info.model'; import { testSearchDataImplementation } from './base/search-data.spec'; import { FindListOptions } from './find-list-options.model'; +import { ItemDataService } from './item-data.service'; import { buildPaginatedList } from './paginated-list.model'; import { RelationshipDataService } from './relationship-data.service'; import { DeleteRequest } from './request.models'; @@ -121,20 +130,9 @@ describe('RelationshipDataService', () => { const itemService = jasmine.createSpyObj('itemService', { findById: (uuid) => createSuccessfulRemoteDataObject(relatedItems.find((relatedItem) => relatedItem.id === uuid)), findByHref: createSuccessfulRemoteDataObject$(relatedItems[0]), + getIDHrefObs: (uuid: string) => observableOf(`https://demo.dspace.org/server/api/core/items/${uuid}`), }); - function initTestService() { - return new RelationshipDataService( - requestService, - rdbService, - halService, - objectCache as ObjectCacheService, - itemService, - null, - jasmine.createSpy('paginatedRelationsToItems').and.returnValue((v) => v), - ); - } - const getRequestEntry$ = (successful: boolean) => { return observableOf({ response: { isSuccessful: successful, payload: relationships } as any, @@ -143,11 +141,26 @@ describe('RelationshipDataService', () => { beforeEach(() => { requestService = getMockRequestService(getRequestEntry$(true)); - service = initTestService(); + + TestBed.configureTestingModule({ + providers: [ + { provide: RequestService, useValue: requestService }, + { provide: RemoteDataBuildService, useValue: rdbService }, + { provide: HALEndpointService, useValue: halService }, + { provide: ObjectCacheService, useValue: objectCache }, + { provide: ItemDataService, useValue: itemService }, + { provide: RequestService, useValue: requestService }, + { provide: PAGINATED_RELATIONS_TO_ITEMS_OPERATOR, useValue: jasmine.createSpy('paginatedRelationsToItems').and.returnValue((v) => v) }, + { provide: Store, useValue: provideMockStore() }, + { provide: APP_CONFIG, useValue: environment }, + RelationshipDataService, + ], + }); + service = TestBed.inject(RelationshipDataService); }); describe('composition', () => { - const initService = () => new RelationshipDataService(null, null, null, null, null, null, null); + const initService = () => new RelationshipDataService(null, null, null, null, null, null, null, null, environment); testSearchDataImplementation(initService); }); @@ -235,6 +248,16 @@ describe('RelationshipDataService', () => { }); }); + describe('searchByItemsAndType', () => { + it('should call addDependency for each item to invalidate the request when one of the items is update', () => { + spyOn(service as any, 'addDependency'); + + service.searchByItemsAndType(relationshipType.id, item.id, relationshipType.leftwardType, ['item-id-1', 'item-id-2']); + + expect((service as any).addDependency).toHaveBeenCalledTimes(2); + }); + }); + describe('resolveMetadataRepresentation', () => { const parentItem: Item = Object.assign(new Item(), { id: 'parent-item', diff --git a/src/app/core/data/relationship-data.service.ts b/src/app/core/data/relationship-data.service.ts index 0e8740e2aee..d52c14f0a76 100644 --- a/src/app/core/data/relationship-data.service.ts +++ b/src/app/core/data/relationship-data.service.ts @@ -24,6 +24,10 @@ import { tap, } from 'rxjs/operators'; +import { + APP_CONFIG, + AppConfig, +} from '../../../config/app-config.interface'; import { AppState, keySelector, @@ -54,6 +58,7 @@ import { RemoteDataBuildService } from '../cache/builders/remote-data-build.serv import { RequestParam } from '../cache/models/request-param.model'; import { ObjectCacheService } from '../cache/object-cache.service'; import { HttpOptions } from '../dspace-rest/dspace-rest.service'; +import { MetadataService } from '../metadata/metadata.service'; import { DSpaceObject } from '../shared/dspace-object.model'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { Item } from '../shared/item.model'; @@ -128,9 +133,11 @@ export class RelationshipDataService extends IdentifiableDataService, @Inject(PAGINATED_RELATIONS_TO_ITEMS_OPERATOR) private paginatedRelationsToItems: (thisId: string) => (source: Observable>>) => Observable>>, + @Inject(APP_CONFIG) private appConfig: AppConfig, ) { super('relationships', requestService, rdbService, objectCache, halService, 15 * 60 * 1000); @@ -153,8 +160,11 @@ export class RelationshipDataService extends IdentifiableDataService> { + deleteRelationship(id: string, copyVirtualMetadata: string, shouldRefresh = true): Observable> { return this.getRelationshipEndpoint(id).pipe( isNotEmptyOperator(), take(1), @@ -165,7 +175,11 @@ export class RelationshipDataService extends IdentifiableDataService this.rdbService.buildFromRequestUUID(restRequest.uuid)), getFirstCompletedRemoteData(), - tap(() => this.refreshRelationshipItemsInCacheByRelationship(id)), + tap(() => { + if (shouldRefresh) { + this.refreshRelationshipItemsInCacheByRelationship(id); + } + }), ); } @@ -176,8 +190,11 @@ export class RelationshipDataService extends IdentifiableDataService> { + addRelationship(typeId: string, item1: Item, item2: Item, leftwardValue?: string, rightwardValue?: string, shouldRefresh = true): Observable> { const options: HttpOptions = Object.create({}); let headers = new HttpHeaders(); headers = headers.append('Content-Type', 'text/uri-list'); @@ -192,8 +209,12 @@ export class RelationshipDataService extends IdentifiableDataService this.rdbService.buildFromRequestUUID(restRequest.uuid)), getFirstCompletedRemoteData(), - tap(() => this.refreshRelationshipItemsInCache(item1)), - tap(() => this.refreshRelationshipItemsInCache(item2)), + tap(() => { + if (shouldRefresh) { + this.refreshRelationshipItemsInCache(item1); + this.refreshRelationshipItemsInCache(item2); + } + }), ) as Observable>; } @@ -221,7 +242,7 @@ export class RelationshipDataService extends IdentifiableDataService>> { - const linksToFollow: FollowLinkConfig[] = itemLinksToFollow(options.fetchThumbnail); + const linksToFollow: FollowLinkConfig[] = itemLinksToFollow(options.fetchThumbnail, this.appConfig.item.showAccessStatuses); linksToFollow.push(followLink('relationshipType')); return this.getItemRelationshipsByLabel(item, label, options, true, true, ...linksToFollow).pipe(this.paginatedRelationsToItems(item.uuid)); @@ -334,7 +355,19 @@ export class RelationshipDataService extends IdentifiableDataService + this.getItemRelationshipsByLabel(item, label, options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow)), + ); } /** @@ -531,50 +564,33 @@ export class RelationshipDataService extends IdentifiableDataService>> { - - const searchParams = [ - { - fieldName: 'typeId', - fieldValue: typeId, - }, - { - fieldName: 'focusItem', - fieldValue: itemUuid, - }, - { - fieldName: 'relationshipLabel', - fieldValue: relationshipLabel, - }, - { - fieldName: 'size', - fieldValue: arrayOfItemIds.length, - }, - { - fieldName: 'embed', - fieldValue: 'leftItem', - }, - { - fieldName: 'embed', - fieldValue: 'rightItem', - }, + const searchParams: RequestParam[] = [ + new RequestParam('typeId', typeId), + new RequestParam('focusItem', itemUuid), + new RequestParam('relationshipLabel', relationshipLabel), + new RequestParam('size', arrayOfItemIds.length), + new RequestParam('embed', 'leftItem'), + new RequestParam('embed', 'rightItem'), ]; arrayOfItemIds.forEach( (itemId) => { searchParams.push( - { - fieldName: 'relatedItem', - fieldValue: itemId, - }, + new RequestParam('relatedItem', itemId), ); }); - return this.searchBy( + const searchRD$: Observable>> = this.searchBy( 'byItemsAndType', { searchParams: searchParams, }, ) as Observable>>; + arrayOfItemIds.forEach((itemId: string) => { + this.addDependency(searchRD$, this.itemService.getIDHrefObs(encodeURIComponent(itemId))); + }); + + return searchRD$; } /** @@ -602,8 +618,8 @@ export class RelationshipDataService extends IdentifiableDataService { - if (metadatum.isVirtual) { - return this.findById(metadatum.virtualValue, true, false, followLink('leftItem'), followLink('rightItem')).pipe( + if (this.metadataService.isVirtual(metadatum)) { + return this.findById(this.metadataService.virtualValue(metadatum), true, false, followLink('leftItem'), followLink('rightItem')).pipe( getFirstSucceededRemoteData(), switchMap((relRD: RemoteData) => observableCombineLatest(relRD.payload.leftItem, relRD.payload.rightItem).pipe( diff --git a/src/app/core/data/relationship-type-data.service.ts b/src/app/core/data/relationship-type-data.service.ts index 21d0fcb5f62..43c018bbce2 100644 --- a/src/app/core/data/relationship-type-data.service.ts +++ b/src/app/core/data/relationship-type-data.service.ts @@ -16,6 +16,7 @@ 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 { ItemType } from '../shared/item-relationships/item-type.model'; @@ -143,14 +144,8 @@ export class RelationshipTypeDataService extends BaseDataService { }, }, }); + const version1WithDraft = Object.assign(new Version(), { + ...version1, + versionhistory: createSuccessfulRemoteDataObject$(versionHistoryDraft), + }); const versions = [version1, version2]; versionHistory.versions = createSuccessfulRemoteDataObject$(createPaginatedList(versions)); const item1 = Object.assign(new Item(), { @@ -190,21 +194,18 @@ describe('VersionHistoryDataService', () => { }); describe('hasDraftVersion$', () => { - beforeEach(waitForAsync(() => { - versionService.findByHref.and.returnValue(createSuccessfulRemoteDataObject$(version1)); - })); it('should return false if draftVersion is false', fakeAsync(() => { - versionService.getHistoryFromVersion.and.returnValue(of(versionHistory)); + versionService.findByHref.and.returnValue(createSuccessfulRemoteDataObject$(version1)); service.hasDraftVersion$('href').subscribe((res) => { expect(res).toBeFalse(); }); })); + it('should return true if draftVersion is true', fakeAsync(() => { - versionService.getHistoryFromVersion.and.returnValue(of(versionHistoryDraft)); + versionService.findByHref.and.returnValue(createSuccessfulRemoteDataObject$(version1WithDraft)); service.hasDraftVersion$('href').subscribe((res) => { expect(res).toBeTrue(); }); })); }); - }); diff --git a/src/app/core/data/version-history-data.service.ts b/src/app/core/data/version-history-data.service.ts index 6b9e8551462..e15fcd39077 100644 --- a/src/app/core/data/version-history-data.service.ts +++ b/src/app/core/data/version-history-data.service.ts @@ -1,17 +1,22 @@ import { HttpHeaders } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { + combineLatest, Observable, - of, + of as observableOf, } from 'rxjs'; import { filter, + find, map, switchMap, take, } from 'rxjs/operators'; -import { hasValueOperator } from '../../shared/empty.util'; +import { + hasValue, + hasValueOperator, +} from '../../shared/empty.util'; import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; import { PaginatedSearchOptions } from '../../shared/search/models/paginated-search-options.model'; import { @@ -29,7 +34,6 @@ import { getFirstSucceededRemoteDataPayload, getRemoteDataPayload, } from '../shared/operators'; -import { sendRequest } from '../shared/request.operators'; import { Version } from '../shared/version.model'; import { VersionHistory } from '../shared/version-history.model'; import { IdentifiableDataService } from './base/identifiable-data.service'; @@ -38,7 +42,6 @@ import { PaginatedList } from './paginated-list.model'; import { RemoteData } from './remote-data'; import { PostRequest } from './request.models'; import { RequestService } from './request.service'; -import { RestRequest } from './rest-request.model'; import { VersionDataService } from './version-data.service'; /** @@ -100,19 +103,31 @@ export class VersionHistoryDataService extends IdentifiableDataService> { + const requestId = this.requestService.generateRequestId(); const requestOptions: HttpOptions = Object.create({}); let requestHeaders = new HttpHeaders(); requestHeaders = requestHeaders.append('Content-Type', 'text/uri-list'); requestOptions.headers = requestHeaders; - return this.halService.getEndpoint(this.versionsEndpoint).pipe( + this.halService.getEndpoint(this.versionsEndpoint).pipe( take(1), map((endpointUrl: string) => (summary?.length > 0) ? `${endpointUrl}?summary=${summary}` : `${endpointUrl}`), - map((endpointURL: string) => new PostRequest(this.requestService.generateRequestId(), endpointURL, itemHref, requestOptions)), - sendRequest(this.requestService), - switchMap((restRequest: RestRequest) => this.rdbService.buildFromRequestUUID(restRequest.uuid)), + find((href: string) => hasValue(href)), + ).subscribe((href) => { + const request = new PostRequest(requestId, href, itemHref, requestOptions); + if (hasValue(this.responseMsToLive)) { + request.responseMsToLive = this.responseMsToLive; + } + + this.requestService.send(request); + }); + + return this.rdbService.buildFromRequestUUIDAndAwait(requestId, (versionRD) => combineLatest([ + this.requestService.setStaleByHrefSubstring(versionRD.payload._links.self.href), + this.requestService.setStaleByHrefSubstring(versionRD.payload._links.versionhistory.href), + ])).pipe( getFirstCompletedRemoteData(), - ) as Observable>; + ); } /** @@ -151,7 +166,7 @@ export class VersionHistoryDataService extends IdentifiableDataService res.versionhistory), getFirstSucceededRemoteDataPayload(), switchMap((versionHistoryRD) => this.getLatestVersionFromHistory$(versionHistoryRD)), - ) : of(null); + ) : observableOf(null); } /** @@ -162,8 +177,8 @@ export class VersionHistoryDataService extends IdentifiableDataService { return version ? this.getLatestVersion$(version).pipe( take(1), - switchMap((latestVersion) => of(version.version === latestVersion.version)), - ) : of(null); + switchMap((latestVersion) => observableOf(version.version === latestVersion.version)), + ) : observableOf(null); } /** @@ -172,17 +187,22 @@ export class VersionHistoryDataService extends IdentifiableDataService { - return this.versionDataService.findByHref(versionHref, true, true, followLink('versionhistory')).pipe( + return this.versionDataService.findByHref(versionHref, false, true, followLink('versionhistory')).pipe( getFirstCompletedRemoteData(), - switchMap((res) => { - if (res.hasSucceeded && !res.hasNoContent) { - return of(res).pipe( - getFirstSucceededRemoteDataPayload(), - switchMap((version) => this.versionDataService.getHistoryFromVersion(version)), - map((versionHistory) => versionHistory ? versionHistory.draftVersion : false), + switchMap((versionRD: RemoteData) => { + if (versionRD.hasSucceeded && !versionRD.hasNoContent && hasValue(versionRD.payload)) { + return versionRD.payload.versionhistory.pipe( + getFirstCompletedRemoteData(), + map((versionHistoryRD: RemoteData) => { + if (versionHistoryRD.hasSucceeded && !versionHistoryRD.hasNoContent) { + return versionHistoryRD.payload.draftVersion; + } else { + return false; + } + }), ); } else { - return of(false); + return observableOf(false); } }), ); diff --git a/src/app/core/dspace-rest/dspace-rest.service.ts b/src/app/core/dspace-rest/dspace-rest.service.ts index 67d74e95707..a75cef53bf2 100644 --- a/src/app/core/dspace-rest/dspace-rest.service.ts +++ b/src/app/core/dspace-rest/dspace-rest.service.ts @@ -151,7 +151,7 @@ export class DspaceRestService { return form; } - protected handleHttpError(err: unknown): RequestError | unknown { + protected handleHttpError(err: unknown): unknown { if (err instanceof HttpErrorResponse) { const error = new RequestError( (isNotEmpty(err?.error?.message)) ? err.error.message : err.message, diff --git a/src/app/core/dspace-rest/dspace.serializer.spec.ts b/src/app/core/dspace-rest/dspace.serializer.spec.ts index 9a6635bc98c..169dd97a579 100644 --- a/src/app/core/dspace-rest/dspace.serializer.spec.ts +++ b/src/app/core/dspace-rest/dspace.serializer.spec.ts @@ -9,13 +9,13 @@ import { DSpaceSerializer } from './dspace.serializer'; class TestModel implements HALResource { @autoserialize - id: string; + id: string; @autoserialize - name: string; + name: string; @deserialize - _links: { + _links: { self: HALLink; parents: HALLink; }; diff --git a/src/app/core/end-user-agreement/abstract-end-user-agreement.guard.ts b/src/app/core/end-user-agreement/abstract-end-user-agreement.guard.ts deleted file mode 100644 index 2937011a380..00000000000 --- a/src/app/core/end-user-agreement/abstract-end-user-agreement.guard.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { - ActivatedRouteSnapshot, - Router, - RouterStateSnapshot, - UrlTree, -} from '@angular/router'; -import { - Observable, - of as observableOf, -} from 'rxjs'; - -import { environment } from '../../../environments/environment'; -import { returnEndUserAgreementUrlTreeOnFalse } from '../shared/authorized.operators'; - -/** - * An abstract guard for redirecting users to the user agreement page if a certain condition is met - * That condition is defined by abstract method hasAccepted - */ -export abstract class AbstractEndUserAgreementGuard { - - constructor(protected router: Router) { - } - - /** - * True when the user agreement has been accepted - * The user will be redirected to the End User Agreement page if they haven't accepted it before - * A redirect URL will be provided with the navigation so the component can redirect the user back to the blocked route - * when they're finished accepting the agreement - */ - canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { - if (!environment.info.enableEndUserAgreement) { - return observableOf(true); - } - return this.hasAccepted().pipe( - returnEndUserAgreementUrlTreeOnFalse(this.router, state.url), - ); - } - - /** - * This abstract method determines how the User Agreement has to be accepted before the user is allowed to visit - * the desired route - */ - abstract hasAccepted(): Observable; - -} diff --git a/src/app/core/end-user-agreement/end-user-agreement-cookie.guard.spec.ts b/src/app/core/end-user-agreement/end-user-agreement-cookie.guard.spec.ts index a801fb7a6fa..640859c7a42 100644 --- a/src/app/core/end-user-agreement/end-user-agreement-cookie.guard.spec.ts +++ b/src/app/core/end-user-agreement/end-user-agreement-cookie.guard.spec.ts @@ -1,13 +1,14 @@ +import { TestBed } from '@angular/core/testing'; import { Router, UrlTree, } from '@angular/router'; +import { Observable } from 'rxjs'; import { EndUserAgreementService } from './end-user-agreement.service'; -import { EndUserAgreementCookieGuard } from './end-user-agreement-cookie.guard'; +import { endUserAgreementCookieGuard } from './end-user-agreement-cookie.guard'; -describe('EndUserAgreementCookieGuard', () => { - let guard: EndUserAgreementCookieGuard; +describe('endUserAgreementCookieGuard', () => { let endUserAgreementService: EndUserAgreementService; let router: Router; @@ -21,14 +22,22 @@ describe('EndUserAgreementCookieGuard', () => { parseUrl: new UrlTree(), createUrlTree: new UrlTree(), }); - - guard = new EndUserAgreementCookieGuard(endUserAgreementService, router); + TestBed.configureTestingModule({ + providers: [ + { provide: Router, useValue: router }, + { provide: EndUserAgreementService, useValue: endUserAgreementService }, + ], + }); }); describe('canActivate', () => { describe('when the cookie has been accepted', () => { it('should return true', (done) => { - guard.canActivate(undefined, { url: Object.assign({ url: 'redirect' }) } as any).subscribe((result) => { + const result$ = TestBed.runInInjectionContext(() => { + return endUserAgreementCookieGuard(undefined, { url: Object.assign({ url: 'redirect' }) } as any); + }) as Observable; + + result$.subscribe((result) => { expect(result).toEqual(true); done(); }); @@ -41,7 +50,11 @@ describe('EndUserAgreementCookieGuard', () => { }); it('should return a UrlTree', (done) => { - guard.canActivate(undefined, Object.assign({ url: 'redirect' })).subscribe((result) => { + const result$ = TestBed.runInInjectionContext(() => { + return endUserAgreementCookieGuard(undefined, { url: Object.assign({ url: 'redirect' }) } as any); + }) as Observable; + + result$.subscribe((result) => { expect(result).toEqual(jasmine.any(UrlTree)); done(); }); diff --git a/src/app/core/end-user-agreement/end-user-agreement-cookie.guard.ts b/src/app/core/end-user-agreement/end-user-agreement-cookie.guard.ts index 3eccae3ca9e..2da38fb22dc 100644 --- a/src/app/core/end-user-agreement/end-user-agreement-cookie.guard.ts +++ b/src/app/core/end-user-agreement/end-user-agreement-cookie.guard.ts @@ -1,29 +1,19 @@ -import { Injectable } from '@angular/core'; -import { Router } from '@angular/router'; -import { - Observable, - of as observableOf, -} from 'rxjs'; +import { inject } from '@angular/core'; +import { CanActivateFn } from '@angular/router'; +import { of as observableOf } from 'rxjs'; -import { AbstractEndUserAgreementGuard } from './abstract-end-user-agreement.guard'; +import { endUserAgreementGuard } from './end-user-agreement.guard'; import { EndUserAgreementService } from './end-user-agreement.service'; + /** - * A guard redirecting users to the end agreement page when the user agreement cookie hasn't been accepted + * Guard for preventing unauthorized access to certain pages + * requiring the end user agreement to have been accepted in a cookie */ -@Injectable({ providedIn: 'root' }) -export class EndUserAgreementCookieGuard extends AbstractEndUserAgreementGuard { - - constructor(protected endUserAgreementService: EndUserAgreementService, - protected router: Router) { - super(router); - } - - /** - * True when the user agreement cookie has been accepted - */ - hasAccepted(): Observable { - return observableOf(this.endUserAgreementService.isCookieAccepted()); - } - -} +export const endUserAgreementCookieGuard: CanActivateFn = + endUserAgreementGuard( + () => { + const endUserAgreementService = inject(EndUserAgreementService); + return observableOf(endUserAgreementService.isCookieAccepted()); + }, + ); diff --git a/src/app/core/end-user-agreement/end-user-agreement-current-user.guard.spec.ts b/src/app/core/end-user-agreement/end-user-agreement-current-user.guard.spec.ts index 88722bd799e..ae1f63884f2 100644 --- a/src/app/core/end-user-agreement/end-user-agreement-current-user.guard.spec.ts +++ b/src/app/core/end-user-agreement/end-user-agreement-current-user.guard.spec.ts @@ -1,16 +1,18 @@ +import { TestBed } from '@angular/core/testing'; import { Router, UrlTree, } from '@angular/router'; -import { of as observableOf } from 'rxjs'; +import { + Observable, + of as observableOf, +} from 'rxjs'; import { environment } from '../../../environments/environment.test'; import { EndUserAgreementService } from './end-user-agreement.service'; -import { EndUserAgreementCurrentUserGuard } from './end-user-agreement-current-user.guard'; - -describe('EndUserAgreementGuard', () => { - let guard: EndUserAgreementCurrentUserGuard; +import { endUserAgreementCurrentUserGuard } from './end-user-agreement-current-user.guard'; +describe('endUserAgreementGuard', () => { let endUserAgreementService: EndUserAgreementService; let router: Router; @@ -18,19 +20,30 @@ describe('EndUserAgreementGuard', () => { endUserAgreementService = jasmine.createSpyObj('endUserAgreementService', { hasCurrentUserAcceptedAgreement: observableOf(true), }); + router = jasmine.createSpyObj('router', { navigateByUrl: {}, parseUrl: new UrlTree(), createUrlTree: new UrlTree(), }); - guard = new EndUserAgreementCurrentUserGuard(endUserAgreementService, router); + TestBed.configureTestingModule({ + providers: [ + { provide: Router, useValue: router }, + { provide: EndUserAgreementService, useValue: endUserAgreementService }, + ], + }); + }); describe('canActivate', () => { describe('when the user has accepted the agreement', () => { it('should return true', (done) => { - guard.canActivate(undefined, Object.assign({ url: 'redirect' })).subscribe((result) => { + const result$ = TestBed.runInInjectionContext(() => { + return endUserAgreementCurrentUserGuard(undefined, Object.assign({ url: 'redirect' })); + }) as Observable; + + result$.subscribe((result) => { expect(result).toEqual(true); done(); }); @@ -43,7 +56,11 @@ describe('EndUserAgreementGuard', () => { }); it('should return a UrlTree', (done) => { - guard.canActivate(undefined, Object.assign({ url: 'redirect' })).subscribe((result) => { + const result$ = TestBed.runInInjectionContext(() => { + return endUserAgreementCurrentUserGuard(undefined, Object.assign({ url: 'redirect' })); + }) as Observable; + + result$.subscribe((result) => { expect(result).toEqual(jasmine.any(UrlTree)); done(); }); @@ -53,7 +70,12 @@ describe('EndUserAgreementGuard', () => { describe('when the end user agreement is disabled', () => { it('should return true', (done) => { environment.info.enableEndUserAgreement = false; - guard.canActivate(undefined, Object.assign({ url: 'redirect' })).subscribe((result) => { + + const result$ = TestBed.runInInjectionContext(() => { + return endUserAgreementCurrentUserGuard(undefined, Object.assign({ url: 'redirect' })); + }) as Observable; + + result$.subscribe((result) => { expect(result).toEqual(true); done(); }); @@ -61,7 +83,11 @@ describe('EndUserAgreementGuard', () => { it('should not resolve to the end user agreement page', (done) => { environment.info.enableEndUserAgreement = false; - guard.canActivate(undefined, Object.assign({ url: 'redirect' })).subscribe((result) => { + const result$ = TestBed.runInInjectionContext(() => { + return endUserAgreementCurrentUserGuard(undefined, Object.assign({ url: 'redirect' })); + }) as Observable; + + result$.subscribe((result) => { expect(router.navigateByUrl).not.toHaveBeenCalled(); done(); }); diff --git a/src/app/core/end-user-agreement/end-user-agreement-current-user.guard.ts b/src/app/core/end-user-agreement/end-user-agreement-current-user.guard.ts index 566f0494503..7c190a08b3c 100644 --- a/src/app/core/end-user-agreement/end-user-agreement-current-user.guard.ts +++ b/src/app/core/end-user-agreement/end-user-agreement-current-user.guard.ts @@ -1,34 +1,25 @@ -import { Injectable } from '@angular/core'; -import { Router } from '@angular/router'; -import { - Observable, - of as observableOf, -} from 'rxjs'; +import { inject } from '@angular/core'; +import { CanActivateFn } from '@angular/router'; +import { of as observableOf } from 'rxjs'; import { environment } from '../../../environments/environment'; -import { AbstractEndUserAgreementGuard } from './abstract-end-user-agreement.guard'; +import { endUserAgreementGuard } from './end-user-agreement.guard'; import { EndUserAgreementService } from './end-user-agreement.service'; -/** - * A guard redirecting logged in users to the end agreement page when they haven't accepted the latest user agreement - */ -@Injectable({ providedIn: 'root' }) -export class EndUserAgreementCurrentUserGuard extends AbstractEndUserAgreementGuard { - - constructor(protected endUserAgreementService: EndUserAgreementService, - protected router: Router) { - super(router); - } - /** - * True when the currently logged in user has accepted the agreements or when the user is not currently authenticated - */ - hasAccepted(): Observable { - if (!environment.info.enableEndUserAgreement) { - return observableOf(true); - } +/** + * Guard for preventing unauthorized access to certain pages + * requiring the end user agreement to have been accepted by the current user - return this.endUserAgreementService.hasCurrentUserAcceptedAgreement(true); - } + */ +export const endUserAgreementCurrentUserGuard: CanActivateFn = + endUserAgreementGuard( + () => { + const endUserAgreementService = inject(EndUserAgreementService); + if (!environment.info.enableEndUserAgreement) { + return observableOf(true); + } -} + return endUserAgreementService.hasCurrentUserAcceptedAgreement(true); + }, + ); diff --git a/src/app/core/end-user-agreement/end-user-agreement.guard.ts b/src/app/core/end-user-agreement/end-user-agreement.guard.ts new file mode 100644 index 00000000000..911dd54e259 --- /dev/null +++ b/src/app/core/end-user-agreement/end-user-agreement.guard.ts @@ -0,0 +1,34 @@ +import { inject } from '@angular/core'; +import { + ActivatedRouteSnapshot, + CanActivateFn, + Router, + RouterStateSnapshot, + UrlTree, +} from '@angular/router'; +import { + Observable, + of as observableOf, +} from 'rxjs'; + +import { environment } from '../../../environments/environment'; +import { returnEndUserAgreementUrlTreeOnFalse } from '../shared/authorized.operators'; + +export declare type HasAcceptedGuardParamFn = () => Observable; +/** + * Guard for preventing activating when the user has not accepted the EndUserAgreement + * @param hasAccepted Function determining if the EndUserAgreement has been accepted + */ +export const endUserAgreementGuard = ( + hasAccepted: HasAcceptedGuardParamFn, +): CanActivateFn => { + return (route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable => { + const router = inject(Router); + if (!environment.info.enableEndUserAgreement) { + return observableOf(true); + } + return hasAccepted().pipe( + returnEndUserAgreementUrlTreeOnFalse(router, state.url), + ); + }; +}; diff --git a/src/app/core/eperson/eperson-data.service.spec.ts b/src/app/core/eperson/eperson-data.service.spec.ts index a5deb9ef2a6..749afb5daa3 100644 --- a/src/app/core/eperson/eperson-data.service.spec.ts +++ b/src/app/core/eperson/eperson-data.service.spec.ts @@ -114,7 +114,7 @@ describe('EPersonDataService', () => { it('search by default scope (byMetadata) and no query', () => { service.searchByScope(null, ''); const options = Object.assign(new FindListOptions(), { - searchParams: [Object.assign(new RequestParam('query', encodeURIComponent('')))], + searchParams: [Object.assign(new RequestParam('query', ''))], }); expect(service.searchBy).toHaveBeenCalledWith('byMetadata', options, true, true); }); @@ -122,7 +122,7 @@ describe('EPersonDataService', () => { it('search metadata scope and no query', () => { service.searchByScope('metadata', ''); const options = Object.assign(new FindListOptions(), { - searchParams: [Object.assign(new RequestParam('query', encodeURIComponent('')))], + searchParams: [Object.assign(new RequestParam('query', ''))], }); expect(service.searchBy).toHaveBeenCalledWith('byMetadata', options, true, true); }); @@ -130,7 +130,7 @@ describe('EPersonDataService', () => { it('search metadata scope and with query', () => { service.searchByScope('metadata', 'test'); const options = Object.assign(new FindListOptions(), { - searchParams: [Object.assign(new RequestParam('query', encodeURIComponent('test')))], + searchParams: [Object.assign(new RequestParam('query', 'test'))], }); expect(service.searchBy).toHaveBeenCalledWith('byMetadata', options, true, true); }); @@ -140,7 +140,7 @@ describe('EPersonDataService', () => { spyOn(service, 'findByHref').and.returnValue(createSuccessfulRemoteDataObject$(null)); service.searchByScope('email', ''); const options = Object.assign(new FindListOptions(), { - searchParams: [Object.assign(new RequestParam('email', encodeURIComponent('')))], + searchParams: [Object.assign(new RequestParam('email', ''))], }); expect((service as any).searchData.getSearchByHref).toHaveBeenCalledWith('byEmail', options); expect(service.findByHref).toHaveBeenCalledWith(epersonsEndpoint, true, true); @@ -151,7 +151,7 @@ describe('EPersonDataService', () => { spyOn(service, 'findByHref').and.returnValue(createSuccessfulRemoteDataObject$(EPersonMock)); service.searchByScope('email', EPersonMock.email); const options = Object.assign(new FindListOptions(), { - searchParams: [Object.assign(new RequestParam('email', encodeURIComponent(EPersonMock.email)))], + searchParams: [Object.assign(new RequestParam('email', EPersonMock.email))], }); expect((service as any).searchData.getSearchByHref).toHaveBeenCalledWith('byEmail', options); expect(service.findByHref).toHaveBeenCalledWith(epersonsEndpoint, true, true); diff --git a/src/app/core/eperson/eperson-data.service.ts b/src/app/core/eperson/eperson-data.service.ts index 4b033436848..0de6de7407e 100644 --- a/src/app/core/eperson/eperson-data.service.ts +++ b/src/app/core/eperson/eperson-data.service.ts @@ -107,13 +107,17 @@ export class EPersonDataService extends IdentifiableDataService impleme * @param scope Scope of the EPeople search, default byMetadata * @param query Query of search * @param options Options of search request + * @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 */ - public searchByScope(scope: string, query: string, options: FindListOptions = {}, useCachedVersionIfAvailable?: boolean): Observable>> { + public searchByScope(scope: string, query: string, options: FindListOptions = {}, useCachedVersionIfAvailable?: boolean, reRequestOnStale = true): Observable>> { switch (scope) { case 'metadata': - return this.getEpeopleByMetadata(query.trim(), options, useCachedVersionIfAvailable); + return this.getEpeopleByMetadata(query.trim(), options, useCachedVersionIfAvailable, reRequestOnStale); case 'email': - return this.getEPersonByEmail(query.trim()).pipe( + return this.getEPersonByEmail(query.trim(), useCachedVersionIfAvailable, reRequestOnStale).pipe( map((rd: RemoteData) => { if (rd.hasSucceeded) { // Turn the single EPerson or NoContent in to a PaginatedList @@ -145,7 +149,7 @@ export class EPersonDataService extends IdentifiableDataService impleme }), ); default: - return this.getEpeopleByMetadata(query.trim(), options, useCachedVersionIfAvailable); + return this.getEpeopleByMetadata(query.trim(), options, useCachedVersionIfAvailable, reRequestOnStale); } } @@ -163,7 +167,7 @@ export class EPersonDataService extends IdentifiableDataService impleme */ public getEPersonByEmail(query: string, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]): Observable> { const findListOptions = new FindListOptions(); - findListOptions.searchParams = [new RequestParam('email', encodeURIComponent(query))]; + findListOptions.searchParams = [new RequestParam('email', query)]; const href$ = this.searchData.getSearchByHref(this.searchByEmailPath, findListOptions, ...linksToFollow); return this.findByHref(href$, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow); } @@ -180,7 +184,7 @@ export class EPersonDataService extends IdentifiableDataService impleme * {@link HALLink}s should be automatically resolved */ private getEpeopleByMetadata(query: string, options?: FindListOptions, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]): Observable>> { - const searchParams = [new RequestParam('query', encodeURIComponent(query))]; + const searchParams = [new RequestParam('query', query)]; return this.getEPeopleBy(searchParams, this.searchByMetadataPath, options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow); } diff --git a/src/app/core/eperson/models/eperson.model.ts b/src/app/core/eperson/models/eperson.model.ts index ab84d9c113a..71d33fcafbb 100644 --- a/src/app/core/eperson/models/eperson.model.ts +++ b/src/app/core/eperson/models/eperson.model.ts @@ -91,7 +91,7 @@ export class EPerson extends DSpaceObject { public groups?: Observable>>; getRenderTypes(): (string | GenericConstructor)[] { - return [this.constructor.name, ...super.getRenderTypes()]; + return ['EPerson', this.constructor.name, ...super.getRenderTypes()]; } } diff --git a/src/app/core/eperson/models/group.model.ts b/src/app/core/eperson/models/group.model.ts index bed55af0a2d..71923d6c55c 100644 --- a/src/app/core/eperson/models/group.model.ts +++ b/src/app/core/eperson/models/group.model.ts @@ -48,7 +48,7 @@ export class Group extends DSpaceObject { * The {@link HALLink}s for this Group */ @deserialize - _links: { + _links: { self: HALLink; subgroups: HALLink; epersons: HALLink; diff --git a/src/app/core/forward-client-ip/forward-client-ip.interceptor.ts b/src/app/core/forward-client-ip/forward-client-ip.interceptor.ts index 037f690057f..7fe4263d464 100644 --- a/src/app/core/forward-client-ip/forward-client-ip.interceptor.ts +++ b/src/app/core/forward-client-ip/forward-client-ip.interceptor.ts @@ -27,6 +27,14 @@ export class ForwardClientIpInterceptor implements HttpInterceptor { */ intercept(httpRequest: HttpRequest, next: HttpHandler): Observable> { const clientIp = this.req.get('x-forwarded-for') || this.req.connection.remoteAddress; - return next.handle(httpRequest.clone({ setHeaders: { 'X-Forwarded-For': clientIp } })); + const headers = { 'X-Forwarded-For': clientIp }; + + // if the request has a user-agent retain it + const userAgent = this.req.get('user-agent'); + if (userAgent) { + headers['User-Agent'] = userAgent; + } + + return next.handle(httpRequest.clone({ setHeaders: headers })); } } diff --git a/src/app/core/google-recaptcha/google-recaptcha.service.spec.ts b/src/app/core/google-recaptcha/google-recaptcha.service.spec.ts index 957cc649bf8..94b03bba463 100644 --- a/src/app/core/google-recaptcha/google-recaptcha.service.spec.ts +++ b/src/app/core/google-recaptcha/google-recaptcha.service.spec.ts @@ -30,7 +30,7 @@ describe('GoogleRecaptchaService', () => { findByPropertyName: createSuccessfulRemoteDataObject$({ values: ['googleRecaptchaToken'] }), }); cookieService = jasmine.createSpyObj('cookieService', { - get: '{%22token_item%22:true%2C%22impersonation%22:true%2C%22redirect%22:true%2C%22language%22:true%2C%22klaro%22:true%2C%22has_agreed_end_user%22:true%2C%22google-analytics%22:true}', + get: '{%22token_item%22:true%2C%22impersonation%22:true%2C%22redirect%22:true%2C%22language%22:true%2C%22orejime%22:true%2C%22has_agreed_end_user%22:true%2C%22google-analytics%22:true}', set: () => { /* empty */ }, diff --git a/src/app/core/google-recaptcha/google-recaptcha.service.ts b/src/app/core/google-recaptcha/google-recaptcha.service.ts index b4028f98358..c23223b6785 100644 --- a/src/app/core/google-recaptcha/google-recaptcha.service.ts +++ b/src/app/core/google-recaptcha/google-recaptcha.service.ts @@ -105,7 +105,7 @@ export class GoogleRecaptchaService { tap(([recaptchaVersionRD, recaptchaModeRD, recaptchaKeyRD]) => { if ( - this.cookieService.get('klaro-anonymous') && this.cookieService.get('klaro-anonymous')[CAPTCHA_NAME] && + this.cookieService.get('orejime-anonymous') && this.cookieService.get('orejime-anonymous')[CAPTCHA_NAME] && recaptchaKeyRD.hasSucceeded && recaptchaVersionRD.hasSucceeded && isNotEmpty(recaptchaVersionRD.payload?.values) && isNotEmpty(recaptchaKeyRD.payload?.values) ) { diff --git a/src/app/core/index/index.effects.ts b/src/app/core/index/index.effects.ts index 2c3b9ad89b2..8fc33145ff7 100644 --- a/src/app/core/index/index.effects.ts +++ b/src/app/core/index/index.effects.ts @@ -45,7 +45,7 @@ export class UUIDIndexEffects { addObject$ = createEffect(() => this.actions$ .pipe( ofType(ObjectCacheActionTypes.ADD), - filter((action: AddToObjectCacheAction) => hasValue(action.payload.objectToCache.uuid)), + filter((action: AddToObjectCacheAction) => hasValue(action.payload.objectToCache) && hasValue(action.payload.objectToCache.uuid)), map((action: AddToObjectCacheAction) => { return new AddToIndexAction( IndexName.OBJECT, @@ -64,7 +64,7 @@ export class UUIDIndexEffects { ofType(ObjectCacheActionTypes.ADD), map((action: AddToObjectCacheAction) => { const alternativeLink = action.payload.alternativeLink; - const selfLink = action.payload.objectToCache._links.self.href; + const selfLink = hasValue(action.payload.objectToCache?._links?.self) ? action.payload.objectToCache._links.self.href : alternativeLink; if (hasValue(alternativeLink) && alternativeLink !== selfLink) { return new AddToIndexAction( IndexName.ALTERNATIVE_OBJECT_LINK, diff --git a/src/app/core/json-patch/json-patch-operations.reducer.spec.ts b/src/app/core/json-patch/json-patch-operations.reducer.spec.ts index dba5794ffcc..6d513120dcd 100644 --- a/src/app/core/json-patch/json-patch-operations.reducer.spec.ts +++ b/src/app/core/json-patch/json-patch-operations.reducer.spec.ts @@ -340,4 +340,384 @@ describe('jsonPatchOperationsReducer test suite', () => { }); }); + describe('dedupeOperationEntries', () => { + it('should not remove duplicated keys if operations are not sequential', () => { + initState = { + sections: { + children: { + publicationStep: { + body: [ + { + operation: { + op: 'add', + path: '/sections/publicationStep/dc.date.issued', + value: [ + { + value: '2024-06', + language: null, + authority: null, + display: '2024-06', + confidence: -1, + place: 0, + otherInformation: null, + }, + ], + }, + timeCompleted: timestampBeforeStart, + }, + { + operation: { + op: 'replace', + path: '/sections/publicationStep/dc.date.issued/0', + value: { + value: '2023-06-19', + language: null, + authority: null, + display: '2023-06-19', + confidence: -1, + place: 0, + otherInformation: null, + }, + }, + timeCompleted: timestampBeforeStart, + }, + ], + } as JsonPatchOperationsEntry, + }, + transactionStartTime: null, + commitPending: false, + } as JsonPatchOperationsResourceEntry, + }; + + const value = [ + { + value: '2024-06-19', + language: null, + authority: null, + display: '2024-06-19', + confidence: -1, + place: 0, + otherInformation: null, + }, + ]; + const action = new NewPatchAddOperationAction( + 'sections', + 'publicationStep', + '/sections/publicationStep/dc.date.issued', + value); + const newState = jsonPatchOperationsReducer(initState, action); + + const expectedBody: any = [ + { + 'operation': { + 'op': 'add', + 'path': '/sections/publicationStep/dc.date.issued', + 'value': [ + { + 'value': '2024-06', + 'language': null, + 'authority': null, + 'display': '2024-06', + 'confidence': -1, + 'place': 0, + 'otherInformation': null, + }, + ], + }, + 'timeCompleted': timestampBeforeStart, + }, + { + 'operation': { + 'op': 'replace', + 'path': '/sections/publicationStep/dc.date.issued/0', + 'value': { + 'value': '2023-06-19', + 'language': null, + 'authority': null, + 'display': '2023-06-19', + 'confidence': -1, + 'place': 0, + 'otherInformation': null, + }, + }, + 'timeCompleted': timestampBeforeStart, + }, + { + 'operation': { + 'op': 'add', + 'path': '/sections/publicationStep/dc.date.issued', + 'value': [ + { + 'value': '2024-06-19', + 'language': null, + 'authority': null, + 'display': '2024-06-19', + 'confidence': -1, + 'place': 0, + 'otherInformation': null, + }, + ], + }, + 'timeCompleted': timestampBeforeStart, + }, + ]; + + expect(newState.sections.children.publicationStep.body).toEqual(expectedBody); + + }); + + it('should remove duplicated keys if operations are sequential', () => { + initState = { + sections: { + children: { + publicationStep: { + body: [ + { + operation: { + op: 'add', + path: '/sections/publicationStep/dc.date.issued', + value: [ + { + value: '2024-06', + language: null, + authority: null, + display: '2024-06', + confidence: -1, + place: 0, + otherInformation: null, + }, + ], + }, + timeCompleted: timestampBeforeStart, + }, + { + operation: { + op: 'replace', + path: '/sections/publicationStep/dc.date.issued/0', + value: { + value: '2023-06-19', + language: null, + authority: null, + display: '2023-06-19', + confidence: -1, + place: 0, + otherInformation: null, + }, + }, + timeCompleted: timestampBeforeStart, + }, + { + 'operation': { + 'op': 'add', + 'path': '/sections/publicationStep/dc.date.issued', + 'value': [ + { + 'value': '2024-06-19', + 'language': null, + 'authority': null, + 'display': '2024-06-19', + 'confidence': -1, + 'place': 0, + 'otherInformation': null, + }, + ], + }, + 'timeCompleted': timestampBeforeStart, + }, + ], + } as JsonPatchOperationsEntry, + }, + transactionStartTime: null, + commitPending: false, + } as JsonPatchOperationsResourceEntry, + }; + + const value = [ + { + value: '2024-06-20', + language: null, + authority: null, + display: '2024-06-20', + confidence: -1, + place: 0, + otherInformation: null, + }, + ]; + const action = new NewPatchAddOperationAction( + 'sections', + 'publicationStep', + '/sections/publicationStep/dc.date.issued', + value); + const newState = jsonPatchOperationsReducer(initState, action); + + const expectedBody: any = [ + { + 'operation': { + 'op': 'add', + 'path': '/sections/publicationStep/dc.date.issued', + 'value': [ + { + 'value': '2024-06', + 'language': null, + 'authority': null, + 'display': '2024-06', + 'confidence': -1, + 'place': 0, + 'otherInformation': null, + }, + ], + }, + 'timeCompleted': timestampBeforeStart, + }, + { + 'operation': { + 'op': 'replace', + 'path': '/sections/publicationStep/dc.date.issued/0', + 'value': { + 'value': '2023-06-19', + 'language': null, + 'authority': null, + 'display': '2023-06-19', + 'confidence': -1, + 'place': 0, + 'otherInformation': null, + }, + }, + 'timeCompleted': timestampBeforeStart, + }, + { + 'operation': { + 'op': 'add', + 'path': '/sections/publicationStep/dc.date.issued', + 'value': [ + { + 'value': '2024-06-20', + 'language': null, + 'authority': null, + 'display': '2024-06-20', + 'confidence': -1, + 'place': 0, + 'otherInformation': null, + }, + ], + }, + 'timeCompleted': timestampBeforeStart, + }, + ]; + + expect(newState.sections.children.publicationStep.body).toEqual(expectedBody); + + }); + + it('should remove duplicated keys if all operations have same key', () => { + initState = { + sections: { + children: { + publicationStep: { + body: [ + { + operation: { + op: 'add', + path: '/sections/publicationStep/dc.date.issued', + value: [ + { + value: '2024', + language: null, + authority: null, + display: '2024-06', + confidence: -1, + place: 0, + otherInformation: null, + }, + ], + }, + timeCompleted: timestampBeforeStart, + }, + { + 'operation': { + 'op': 'add', + 'path': '/sections/publicationStep/dc.date.issued', + 'value': [ + { + 'value': '2024-06', + 'language': null, + 'authority': null, + 'display': '2024-06', + 'confidence': -1, + 'place': 0, + 'otherInformation': null, + }, + ], + }, + 'timeCompleted': timestampBeforeStart, + }, + { + 'operation': { + 'op': 'add', + 'path': '/sections/publicationStep/dc.date.issued', + 'value': [ + { + 'value': '2024-06-19', + 'language': null, + 'authority': null, + 'display': '2024-06-19', + 'confidence': -1, + 'place': 0, + 'otherInformation': null, + }, + ], + }, + 'timeCompleted': timestampBeforeStart, + }, + ], + } as JsonPatchOperationsEntry, + }, + transactionStartTime: null, + commitPending: false, + } as JsonPatchOperationsResourceEntry, + }; + + const value = [ + { + value: '2024-06-20', + language: null, + authority: null, + display: '2024-06-20', + confidence: -1, + place: 0, + otherInformation: null, + }, + ]; + const action = new NewPatchAddOperationAction( + 'sections', + 'publicationStep', + '/sections/publicationStep/dc.date.issued', + value); + const newState = jsonPatchOperationsReducer(initState, action); + + const expectedBody: any = [ + { + 'operation': { + 'op': 'add', + 'path': '/sections/publicationStep/dc.date.issued', + 'value': [ + { + 'value': '2024-06-20', + 'language': null, + 'authority': null, + 'display': '2024-06-20', + 'confidence': -1, + 'place': 0, + 'otherInformation': null, + }, + ], + }, + 'timeCompleted': timestampBeforeStart, + }, + ]; + + expect(newState.sections.children.publicationStep.body).toEqual(expectedBody); + + }); + }); }); diff --git a/src/app/core/json-patch/json-patch-operations.reducer.ts b/src/app/core/json-patch/json-patch-operations.reducer.ts index 53466d284db..149d81f3e1f 100644 --- a/src/app/core/json-patch/json-patch-operations.reducer.ts +++ b/src/app/core/json-patch/json-patch-operations.reducer.ts @@ -406,14 +406,18 @@ function addOperationToList(body: JsonPatchOperationObject[], actionType, target * @returns deduped JSON patch operation object entries */ function dedupeOperationEntries(body: JsonPatchOperationObject[]): JsonPatchOperationObject[] { - const ops = new Map(); + const ops = new Map(); for (let i = body.length - 1; i >= 0; i--) { const patch = body[i].operation; const key = `${patch.op}-${patch.path}`; if (!ops.has(key)) { - ops.set(key, patch); + ops.set(key, i); } else { - body.splice(i, 1); + const entry = ops.get(key); + if (entry - 1 === i) { + body.splice(i, 1); + ops.set(key, i); + } } } diff --git a/src/app/core/metadata/head-tag.service.spec.ts b/src/app/core/metadata/head-tag.service.spec.ts new file mode 100644 index 00000000000..2fbae88f120 --- /dev/null +++ b/src/app/core/metadata/head-tag.service.spec.ts @@ -0,0 +1,508 @@ +import { + fakeAsync, + tick, +} from '@angular/core/testing'; +import { + Meta, + Title, +} from '@angular/platform-browser'; +import { + NavigationEnd, + Router, +} from '@angular/router'; +import { createMockStore } from '@ngrx/store/testing'; +import { TranslateService } from '@ngx-translate/core'; +import { + Observable, + of as observableOf, + of, +} from 'rxjs'; + +import { AppConfig } from '../../../config/app-config.interface'; +import { + ItemMock, + MockBitstream1, + MockBitstream2, + MockBitstream3, +} from '../../shared/mocks/item.mock'; +import { getMockTranslateService } from '../../shared/mocks/translate.service.mock'; +import { + createSuccessfulRemoteDataObject, + createSuccessfulRemoteDataObject$, +} from '../../shared/remote-data.utils'; +import { createPaginatedList } from '../../shared/testing/utils.test'; +import { DSONameService } from '../breadcrumbs/dso-name.service'; +import { AuthorizationDataService } from '../data/feature-authorization/authorization-data.service'; +import { PaginatedList } from '../data/paginated-list.model'; +import { RemoteData } from '../data/remote-data'; +import { RootDataService } from '../data/root-data.service'; +import { HardRedirectService } from '../services/hard-redirect.service'; +import { Bitstream } from '../shared/bitstream.model'; +import { Bundle } from '../shared/bundle.model'; +import { Item } from '../shared/item.model'; +import { MetadataValue } from '../shared/metadata.models'; +import { HeadTagService } from './head-tag.service'; +import { + AddMetaTagAction, + ClearMetaTagAction, +} from './meta-tag.actions'; + +describe('HeadTagService', () => { + let headTagService: HeadTagService; + + let meta: Meta; + + let title: Title; + + let dsoNameService: DSONameService; + + let bundleDataService; + let rootService: RootDataService; + let translateService: TranslateService; + let hardRedirectService: HardRedirectService; + let authorizationService: AuthorizationDataService; + + let router: Router; + let store; + + let appConfig: AppConfig; + + const initialState = { 'core': { metaTag: { tagsInUse: ['title', 'description'] } } }; + + + beforeEach(() => { + rootService = jasmine.createSpyObj({ + findRoot: createSuccessfulRemoteDataObject$({ dspaceVersion: 'mock-dspace-version' }), + }); + bundleDataService = jasmine.createSpyObj({ + findByItemAndName: mockBundleRD$([MockBitstream3]), + }); + translateService = getMockTranslateService(); + meta = jasmine.createSpyObj('meta', { + addTag: {}, + removeTag: {}, + }); + title = jasmine.createSpyObj({ + setTitle: {}, + }); + dsoNameService = jasmine.createSpyObj({ + getName: ItemMock.firstMetadataValue('dc.title'), + }); + router = { + url: '/items/0ec7ff22-f211-40ab-a69e-c819b0b1f357', + events: of(new NavigationEnd(1, '', '')), + routerState: { + root: {}, + }, + } as any as Router; + hardRedirectService = jasmine.createSpyObj( { + getCurrentOrigin: 'https://request.org', + }); + authorizationService = jasmine.createSpyObj('authorizationService', { + isAuthorized: observableOf(true), + }); + + store = createMockStore({ initialState }); + spyOn(store, 'dispatch'); + + appConfig = { + item: { + bitstream: { + pageSize: 5, + }, + }, + } as any; + + headTagService = new HeadTagService( + router, + translateService, + meta, + title, + dsoNameService, + bundleDataService, + rootService, + store, + hardRedirectService, + appConfig, + authorizationService, + ); + }); + + it('items page should set meta tags', fakeAsync(() => { + (headTagService as any).processRouteChange({ + data: { + value: { + dso: createSuccessfulRemoteDataObject(ItemMock), + }, + }, + }); + tick(); + expect(title.setTitle).toHaveBeenCalledWith('Test PowerPoint Document'); + expect(meta.addTag).toHaveBeenCalledWith({ + name: 'citation_title', + content: 'Test PowerPoint Document', + }); + expect(meta.addTag).toHaveBeenCalledWith({ name: 'citation_author', content: 'Doe, Jane' }); + expect(meta.addTag).toHaveBeenCalledWith({ + name: 'citation_publication_date', + content: '1650-06-26', + }); + expect(meta.addTag).toHaveBeenCalledWith({ name: 'citation_issn', content: '123456789' }); + expect(meta.addTag).toHaveBeenCalledWith({ name: 'citation_language', content: 'en' }); + expect(meta.addTag).toHaveBeenCalledWith({ + name: 'citation_keywords', + content: 'keyword1; keyword2; keyword3', + }); + })); + + it('items page should set meta tags as published Thesis', fakeAsync(() => { + (headTagService as any).processRouteChange({ + data: { + value: { + dso: createSuccessfulRemoteDataObject(mockPublisher(mockType(ItemMock, 'Thesis'))), + }, + }, + }); + tick(); + expect(meta.addTag).toHaveBeenCalledWith({ + name: 'citation_dissertation_name', + content: 'Test PowerPoint Document', + }); + expect(meta.addTag).toHaveBeenCalledWith({ + name: 'citation_pdf_url', + content: 'https://request.org/bitstreams/4db100c1-e1f5-4055-9404-9bc3e2d15f29/download', + }); + })); + + it('items page should set meta tags as published Technical Report', fakeAsync(() => { + (headTagService as any).processRouteChange({ + data: { + value: { + dso: createSuccessfulRemoteDataObject(mockPublisher(mockType(ItemMock, 'Technical Report'))), + }, + }, + }); + tick(); + expect(meta.addTag).toHaveBeenCalledWith({ + name: 'citation_technical_report_institution', + content: 'Mock Publisher', + }); + })); + + it('route titles should overwrite dso titles', fakeAsync(() => { + (translateService.get as jasmine.Spy).and.returnValues(of('DSpace :: '), of('Translated Route Title')); + (headTagService as any).processRouteChange({ + data: { + value: { + dso: createSuccessfulRemoteDataObject(ItemMock), + title: 'route.title.key', + }, + }, + }); + tick(); + expect(title.setTitle).toHaveBeenCalledTimes(2); + expect((title.setTitle as jasmine.Spy).calls.argsFor(0)).toEqual(['Test PowerPoint Document']); + expect((title.setTitle as jasmine.Spy).calls.argsFor(1)).toEqual(['DSpace :: Translated Route Title']); + })); + + it('other navigation should add title and description', fakeAsync(() => { + (translateService.get as jasmine.Spy).and.returnValues(of('DSpace :: '), of('Dummy Title'), of('This is a dummy item component for testing!')); + (headTagService as any).processRouteChange({ + data: { + value: { + title: 'Dummy Title', + description: 'This is a dummy item component for testing!', + }, + }, + }); + tick(); + expect(title.setTitle).toHaveBeenCalledWith('DSpace :: Dummy Title'); + expect(meta.addTag).toHaveBeenCalledWith({ + name: 'title', + content: 'DSpace :: Dummy Title', + }); + expect(meta.addTag).toHaveBeenCalledWith({ + name: 'description', + content: 'This is a dummy item component for testing!', + }); + })); + + describe(`listenForRouteChange`, () => { + it(`should call processRouteChange`, fakeAsync(() => { + spyOn(headTagService as any, 'processRouteChange').and.callFake(() => undefined); + headTagService.listenForRouteChange(); + tick(); + expect((headTagService as any).processRouteChange).toHaveBeenCalled(); + })); + it(`should add Generator`, fakeAsync(() => { + spyOn(headTagService as any, 'processRouteChange').and.callFake(() => undefined); + headTagService.listenForRouteChange(); + tick(); + expect(meta.addTag).toHaveBeenCalledWith({ + name: 'Generator', + content: 'mock-dspace-version', + }); + })); + }); + + describe('citation_abstract_html_url', () => { + it('should use dc.identifier.uri if available', fakeAsync(() => { + (headTagService as any).processRouteChange({ + data: { + value: { + dso: createSuccessfulRemoteDataObject(mockUri(ItemMock, 'https://ddg.gg')), + }, + }, + }); + tick(); + expect(meta.addTag).toHaveBeenCalledWith({ + name: 'citation_abstract_html_url', + content: 'https://ddg.gg', + }); + })); + + it('should use current route as fallback', fakeAsync(() => { + (headTagService as any).processRouteChange({ + data: { + value: { + dso: createSuccessfulRemoteDataObject(mockUri(ItemMock)), + }, + }, + }); + tick(); + expect(meta.addTag).toHaveBeenCalledWith({ + name: 'citation_abstract_html_url', + content: 'https://request.org/items/0ec7ff22-f211-40ab-a69e-c819b0b1f357', + }); + })); + }); + + describe('citation_*_institution / citation_publisher', () => { + it('should use citation_dissertation_institution tag for dissertations', fakeAsync(() => { + (headTagService as any).processRouteChange({ + data: { + value: { + dso: createSuccessfulRemoteDataObject(mockPublisher(mockType(ItemMock, 'Thesis'))), + }, + }, + }); + tick(); + expect(meta.addTag).toHaveBeenCalledWith({ + name: 'citation_dissertation_institution', + content: 'Mock Publisher', + }); + expect(meta.addTag).not.toHaveBeenCalledWith(jasmine.objectContaining({ name: 'citation_technical_report_institution' })); + expect(meta.addTag).not.toHaveBeenCalledWith(jasmine.objectContaining({ name: 'citation_publisher' })); + })); + + it('should use citation_tech_report_institution tag for tech reports', fakeAsync(() => { + (headTagService as any).processRouteChange({ + data: { + value: { + dso: createSuccessfulRemoteDataObject(mockPublisher(mockType(ItemMock, 'Technical Report'))), + }, + }, + }); + tick(); + expect(meta.addTag).not.toHaveBeenCalledWith(jasmine.objectContaining({ name: 'citation_dissertation_institution' })); + expect(meta.addTag).toHaveBeenCalledWith({ + name: 'citation_technical_report_institution', + content: 'Mock Publisher', + }); + expect(meta.addTag).not.toHaveBeenCalledWith(jasmine.objectContaining({ name: 'citation_publisher' })); + })); + + it('should use citation_publisher for other item types', fakeAsync(() => { + (headTagService as any).processRouteChange({ + data: { + value: { + dso: createSuccessfulRemoteDataObject(mockPublisher(mockType(ItemMock, 'Some Other Type'))), + }, + }, + }); + tick(); + expect(meta.addTag).not.toHaveBeenCalledWith(jasmine.objectContaining({ name: 'citation_dissertation_institution' })); + expect(meta.addTag).not.toHaveBeenCalledWith(jasmine.objectContaining({ name: 'citation_technical_report_institution' })); + expect(meta.addTag).toHaveBeenCalledWith({ + name: 'citation_publisher', + content: 'Mock Publisher', + }); + })); + }); + + describe('citation_pdf_url', () => { + it('should link to primary Bitstream URL regardless of format', fakeAsync(() => { + (bundleDataService.findByItemAndName as jasmine.Spy).and.returnValue(mockBundleRD$([], MockBitstream3)); + + (headTagService as any).processRouteChange({ + data: { + value: { + dso: createSuccessfulRemoteDataObject(ItemMock), + }, + }, + }); + tick(); + expect(meta.addTag).toHaveBeenCalledWith({ + name: 'citation_pdf_url', + content: 'https://request.org/bitstreams/4db100c1-e1f5-4055-9404-9bc3e2d15f29/download', + }); + })); + + describe('bitstream not download allowed', () => { + it('should not have citation_pdf_url', fakeAsync(() => { + (bundleDataService.findByItemAndName as jasmine.Spy).and.returnValue(mockBundleRD$([MockBitstream3])); + (authorizationService.isAuthorized as jasmine.Spy).and.returnValue(observableOf(false)); + + (headTagService as any).processRouteChange({ + data: { + value: { + dso: createSuccessfulRemoteDataObject(ItemMock), + }, + }, + }); + tick(); + expect(meta.addTag).not.toHaveBeenCalledWith(jasmine.objectContaining({ name: 'citation_pdf_url' })); + })); + + }); + + describe('no primary Bitstream', () => { + it('should link to first and only Bitstream regardless of format', fakeAsync(() => { + (bundleDataService.findByItemAndName as jasmine.Spy).and.returnValue(mockBundleRD$([MockBitstream3])); + + (headTagService as any).processRouteChange({ + data: { + value: { + dso: createSuccessfulRemoteDataObject(ItemMock), + }, + }, + }); + tick(); + expect(meta.addTag).toHaveBeenCalledWith({ + name: 'citation_pdf_url', + content: 'https://request.org/bitstreams/4db100c1-e1f5-4055-9404-9bc3e2d15f29/download', + }); + })); + + describe(`when there's a bitstream with an allowed format on the first page`, () => { + let bitstreams; + + beforeEach(() => { + bitstreams = [MockBitstream2, MockBitstream3, MockBitstream1]; + (bundleDataService.findByItemAndName as jasmine.Spy).and.returnValue(mockBundleRD$(bitstreams)); + }); + + it('should link to first Bitstream with allowed format', fakeAsync(() => { + (headTagService as any).processRouteChange({ + data: { + value: { + dso: createSuccessfulRemoteDataObject(ItemMock), + }, + }, + }); + tick(); + expect(meta.addTag).toHaveBeenCalledWith({ + name: 'citation_pdf_url', + content: 'https://request.org/bitstreams/99b00f3c-1cc6-4689-8158-91965bee6b28/download', + }); + })); + + }); + + }); + }); + + describe(`when there's no bitstream with an allowed format on the first page`, () => { + let bitstreams; + + beforeEach(() => { + bitstreams = [MockBitstream1, MockBitstream3, MockBitstream2]; + (bundleDataService.findByItemAndName as jasmine.Spy).and.returnValue(mockBundleRD$(bitstreams)); + }); + + it(`shouldn't add a citation_pdf_url meta tag`, fakeAsync(() => { + (headTagService as any).processRouteChange({ + data: { + value: { + dso: createSuccessfulRemoteDataObject(ItemMock), + }, + }, + }); + tick(); + expect(meta.addTag).not.toHaveBeenCalledWith({ + name: 'citation_pdf_url', + content: 'https://request.org/bitstreams/99b00f3c-1cc6-4689-8158-91965bee6b28/download', + }); + })); + + }); + + + describe('tagstore', () => { + beforeEach(fakeAsync(() => { + (headTagService as any).processRouteChange({ + data: { + value: { + dso: createSuccessfulRemoteDataObject(ItemMock), + }, + }, + }); + tick(); + })); + + it('should remove previous tags on route change', fakeAsync(() => { + expect(meta.removeTag).toHaveBeenCalledWith('name=\'title\''); + expect(meta.removeTag).toHaveBeenCalledWith('name=\'description\''); + })); + + it('should clear all tags and add new ones on route change', () => { + expect(store.dispatch.calls.argsFor(0)).toEqual([new ClearMetaTagAction()]); + expect(store.dispatch.calls.argsFor(1)).toEqual([new AddMetaTagAction('title')]); + expect(store.dispatch.calls.argsFor(2)).toEqual([new AddMetaTagAction('description')]); + }); + }); + + const mockType = (mockItem: Item, type: string): Item => { + const typedMockItem = Object.assign(new Item(), mockItem) as Item; + typedMockItem.metadata['dc.type'] = [{ value: type }] as MetadataValue[]; + return typedMockItem; + }; + + const mockPublisher = (mockItem: Item): Item => { + const publishedMockItem = Object.assign(new Item(), mockItem) as Item; + publishedMockItem.metadata['dc.publisher'] = [ + { + language: 'en_US', + value: 'Mock Publisher', + }, + ] as MetadataValue[]; + return publishedMockItem; + }; + + const mockUri = (mockItem: Item, uri?: string): Item => { + const publishedMockItem = Object.assign(new Item(), mockItem) as Item; + publishedMockItem.metadata['dc.identifier.uri'] = [{ value: uri }] as MetadataValue[]; + return publishedMockItem; + }; + + const mockBundleRD$ = (bitstreams: Bitstream[], primary?: Bitstream): Observable> => { + return createSuccessfulRemoteDataObject$( + Object.assign(new Bundle(), { + name: 'ORIGINAL', + bitstreams: createSuccessfulRemoteDataObject$(mockBitstreamPages$(bitstreams)[0]), + primaryBitstream: createSuccessfulRemoteDataObject$(primary), + }), + ); + }; + + const mockBitstreamPages$ = (bitstreams: Bitstream[]): PaginatedList[] => { + return bitstreams.map((bitstream, index) => Object.assign(createPaginatedList([bitstream]), { + pageInfo: { + totalElements: bitstreams.length, // announce multiple elements/pages + }, + _links: index < bitstreams.length - 1 + ? { next: { href: 'not empty' } } // fake link to the next bitstream page + : { next: { href: undefined } }, // last page has no link + })); + }; +}); diff --git a/src/app/core/metadata/head-tag.service.ts b/src/app/core/metadata/head-tag.service.ts new file mode 100644 index 00000000000..270e5fde723 --- /dev/null +++ b/src/app/core/metadata/head-tag.service.ts @@ -0,0 +1,527 @@ +import { + Inject, + Injectable, +} from '@angular/core'; +import { + Meta, + MetaDefinition, + Title, +} from '@angular/platform-browser'; +import { + ActivatedRoute, + NavigationEnd, + Router, +} from '@angular/router'; +import { + createSelector, + select, + Store, +} from '@ngrx/store'; +import { TranslateService } from '@ngx-translate/core'; +import { + BehaviorSubject, + combineLatest, + concat as observableConcat, + EMPTY, + Observable, + of as observableOf, +} from 'rxjs'; +import { + filter, + map, + mergeMap, + switchMap, + take, +} from 'rxjs/operators'; + +import { + APP_CONFIG, + AppConfig, +} from '../../../config/app-config.interface'; +import { getBitstreamDownloadRoute } from '../../app-routing-paths'; +import { + hasNoValue, + hasValue, + isNotEmpty, +} from '../../shared/empty.util'; +import { followLink } from '../../shared/utils/follow-link-config.model'; +import { DSONameService } from '../breadcrumbs/dso-name.service'; +import { coreSelector } from '../core.selectors'; +import { CoreState } from '../core-state.model'; +import { BundleDataService } from '../data/bundle-data.service'; +import { AuthorizationDataService } from '../data/feature-authorization/authorization-data.service'; +import { PaginatedList } from '../data/paginated-list.model'; +import { RemoteData } from '../data/remote-data'; +import { RootDataService } from '../data/root-data.service'; +import { HardRedirectService } from '../services/hard-redirect.service'; +import { Bitstream } from '../shared/bitstream.model'; +import { getDownloadableBitstream } from '../shared/bitstream.operators'; +import { BitstreamFormat } from '../shared/bitstream-format.model'; +import { Bundle } from '../shared/bundle.model'; +import { DSpaceObject } from '../shared/dspace-object.model'; +import { Item } from '../shared/item.model'; +import { + getFirstCompletedRemoteData, + getFirstSucceededRemoteDataPayload, +} from '../shared/operators'; +import { URLCombiner } from '../url-combiner/url-combiner'; +import { + AddMetaTagAction, + ClearMetaTagAction, +} from './meta-tag.actions'; +import { MetaTagState } from './meta-tag.reducer'; + +/** + * The base selector function to select the metaTag section in the store + */ +const metaTagSelector = createSelector( + coreSelector, + (state: CoreState) => state.metaTag, +); + +/** + * Selector function to select the tags in use from the MetaTagState + */ +const tagsInUseSelector = + createSelector( + metaTagSelector, + (state: MetaTagState) => state.tagsInUse, + ); + +@Injectable({ + providedIn: 'root', +}) +export class HeadTagService { + + private currentObject: BehaviorSubject = new BehaviorSubject(undefined); + + /** + * When generating the citation_pdf_url meta tag for Items with more than one Bitstream (and no primary Bitstream), + * the first Bitstream to match one of the following MIME types is selected. + * See {@linkcode getFirstAllowedFormatBitstreamLink} + * @private + */ + private readonly CITATION_PDF_URL_MIMETYPES = [ + 'application/pdf', // .pdf + 'application/postscript', // .ps + 'application/msword', // .doc + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', // .docx + 'application/rtf', // .rtf + 'application/epub+zip', // .epub + ]; + + constructor( + protected router: Router, + protected translate: TranslateService, + protected meta: Meta, + protected title: Title, + protected dsoNameService: DSONameService, + protected bundleDataService: BundleDataService, + protected rootService: RootDataService, + protected store: Store, + protected hardRedirectService: HardRedirectService, + @Inject(APP_CONFIG) protected appConfig: AppConfig, + protected authorizationService: AuthorizationDataService, + ) { + } + + public listenForRouteChange(): void { + // This never changes, set it only once + this.setGenerator(); + + this.router.events.pipe( + filter((event) => event instanceof NavigationEnd), + map(() => this.router.routerState.root), + map((route: ActivatedRoute) => { + route = this.getCurrentRoute(route); + return { params: route.params, data: route.data }; + })).subscribe((routeInfo: any) => { + this.processRouteChange(routeInfo); + }); + } + + protected processRouteChange(routeInfo: any): void { + this.clearMetaTags(); + + if (hasValue(routeInfo.data.value.dso) && hasValue(routeInfo.data.value.dso.payload)) { + this.currentObject.next(routeInfo.data.value.dso.payload); + this.setDSOMetaTags(); + } + + if (routeInfo.data.value.title) { + const titlePrefix = this.translate.get('repository.title.prefix'); + const title = this.translate.get(routeInfo.data.value.title, routeInfo.data.value); + combineLatest([titlePrefix, title]).pipe(take(1)).subscribe(([translatedTitlePrefix, translatedTitle]: [string, string]) => { + this.addMetaTag('title', translatedTitlePrefix + translatedTitle); + this.title.setTitle(translatedTitlePrefix + translatedTitle); + }); + } + if (routeInfo.data.value.description) { + this.translate.get(routeInfo.data.value.description).pipe(take(1)).subscribe((translatedDescription: string) => { + this.addMetaTag('description', translatedDescription); + }); + } + } + + protected getCurrentRoute(route: ActivatedRoute): ActivatedRoute { + while (route.firstChild) { + route = route.firstChild; + } + return route; + } + + protected setDSOMetaTags(): void { + + this.setTitleTag(); + this.setDescriptionTag(); + + this.setCitationTitleTag(); + this.setCitationAuthorTags(); + this.setCitationPublicationDateTag(); + this.setCitationISSNTag(); + this.setCitationISBNTag(); + + this.setCitationLanguageTag(); + this.setCitationKeywordsTag(); + + this.setCitationAbstractUrlTag(); + this.setCitationPdfUrlTag(); + this.setCitationPublisherTag(); + + if (this.isDissertation()) { + this.setCitationDissertationNameTag(); + } + + // this.setCitationJournalTitleTag(); + // this.setCitationVolumeTag(); + // this.setCitationIssueTag(); + // this.setCitationFirstPageTag(); + // this.setCitationLastPageTag(); + // this.setCitationDOITag(); + // this.setCitationPMIDTag(); + + // this.setCitationFullTextTag(); + + // this.setCitationConferenceTag(); + + // this.setCitationPatentCountryTag(); + // this.setCitationPatentNumberTag(); + + } + + /** + * Add to the + */ + protected setTitleTag(): void { + const value = this.dsoNameService.getName(this.currentObject.getValue()); + this.addMetaTag('title', value); + this.title.setTitle(value); + } + + /** + * Add to the + */ + protected setDescriptionTag(): void { + // TODO: truncate abstract + const value = this.getMetaTagValue('dc.description.abstract'); + this.addMetaTag('description', value); + } + + /** + * Add to the + */ + protected setCitationTitleTag(): void { + const value = this.getMetaTagValue('dc.title'); + this.addMetaTag('citation_title', value); + } + + /** + * Add to the + */ + protected setCitationAuthorTags(): void { + const values: string[] = this.getMetaTagValues(['dc.author', 'dc.contributor.author', 'dc.creator']); + this.addMetaTags('citation_author', values); + } + + /** + * Add to the + */ + protected setCitationPublicationDateTag(): void { + const value = this.getFirstMetaTagValue(['dc.date.copyright', 'dc.date.issued', 'dc.date.available', 'dc.date.accessioned']); + this.addMetaTag('citation_publication_date', value); + } + + /** + * Add to the + */ + protected setCitationISSNTag(): void { + const value = this.getMetaTagValue('dc.identifier.issn'); + this.addMetaTag('citation_issn', value); + } + + /** + * Add to the + */ + protected setCitationISBNTag(): void { + const value = this.getMetaTagValue('dc.identifier.isbn'); + this.addMetaTag('citation_isbn', value); + } + + /** + * Add to the + */ + protected setCitationLanguageTag(): void { + const value = this.getFirstMetaTagValue(['dc.language', 'dc.language.iso']); + this.addMetaTag('citation_language', value); + } + + /** + * Add to the + */ + protected setCitationDissertationNameTag(): void { + const value = this.getMetaTagValue('dc.title'); + this.addMetaTag('citation_dissertation_name', value); + } + + /** + * Add dc.publisher to the . The tag name depends on the item type. + */ + protected setCitationPublisherTag(): void { + const value = this.getMetaTagValue('dc.publisher'); + if (this.isDissertation()) { + this.addMetaTag('citation_dissertation_institution', value); + } else if (this.isTechReport()) { + this.addMetaTag('citation_technical_report_institution', value); + } else { + this.addMetaTag('citation_publisher', value); + } + } + + /** + * Add to the + */ + protected setCitationKeywordsTag(): void { + const value = this.getMetaTagValuesAndCombine('dc.subject'); + this.addMetaTag('citation_keywords', value); + } + + /** + * Add to the + */ + protected setCitationAbstractUrlTag(): void { + if (this.currentObject.value instanceof Item) { + let url = this.getMetaTagValue('dc.identifier.uri'); + if (hasNoValue(url)) { + url = new URLCombiner(this.hardRedirectService.getCurrentOrigin(), this.router.url).toString(); + } + this.addMetaTag('citation_abstract_html_url', url); + } + } + + /** + * Add to the + */ + protected setCitationPdfUrlTag(): void { + if (this.currentObject.value instanceof Item) { + const item = this.currentObject.value as Item; + + // Retrieve the ORIGINAL bundle for the item + this.bundleDataService.findByItemAndName( + item, + 'ORIGINAL', + true, + true, + followLink('primaryBitstream'), + followLink('bitstreams', { + findListOptions: { + // limit the number of bitstreams used to find the citation pdf url to the number + // shown by default on an item page + elementsPerPage: this.appConfig.item.bitstream.pageSize, + }, + }, followLink('format')), + ).pipe( + getFirstSucceededRemoteDataPayload(), + switchMap((bundle: Bundle) => + // First try the primary bitstream + bundle.primaryBitstream.pipe( + getFirstCompletedRemoteData(), + map((rd: RemoteData) => { + if (hasValue(rd.payload)) { + return rd.payload; + } else { + return null; + } + }), + getDownloadableBitstream(this.authorizationService), + // return the bundle as well so we can use it again if there's no primary bitstream + map((bitstream: Bitstream) => [bundle, bitstream]), + ), + ), + switchMap(([bundle, primaryBitstream]: [Bundle, Bitstream]) => { + if (hasValue(primaryBitstream)) { + // If there was a downloadable primary bitstream, emit its link + return [getBitstreamDownloadRoute(primaryBitstream)]; + } else { + // Otherwise consider the regular bitstreams in the bundle + return bundle.bitstreams.pipe( + getFirstCompletedRemoteData(), + switchMap((bitstreamRd: RemoteData>) => { + if (hasValue(bitstreamRd.payload) && bitstreamRd.payload.totalElements === 1) { + // If there's only one bitstream in the bundle, emit its link if its downloadable + return this.getBitLinkIfDownloadable(bitstreamRd.payload.page[0], bitstreamRd); + } else { + // Otherwise check all bitstreams to see if one matches the format whitelist + return this.getFirstAllowedFormatBitstreamLink(bitstreamRd); + } + }), + ); + } + }), + take(1), + ).subscribe((link: string) => { + // Use the found link to set the tag + this.addMetaTag( + 'citation_pdf_url', + new URLCombiner(this.hardRedirectService.getCurrentOrigin(), link).toString(), + ); + }); + } + } + + getBitLinkIfDownloadable(bitstream: Bitstream, bitstreamRd: RemoteData>): Observable { + return observableOf(bitstream).pipe( + getDownloadableBitstream(this.authorizationService), + switchMap((bit: Bitstream) => { + if (hasValue(bit)) { + return [getBitstreamDownloadRoute(bit)]; + } else { + // Otherwise check all bitstreams to see if one matches the format whitelist + return this.getFirstAllowedFormatBitstreamLink(bitstreamRd); + } + }), + ); + } + + /** + * For Items with more than one Bitstream (and no primary Bitstream), link to the first Bitstream + * with a MIME type. + * + * Note this will only check the current page (page size determined item.bitstream.pageSize in the + * config) of bitstreams for performance reasons. + * See https://github.com/DSpace/DSpace/issues/8648 for more info + * + * included in {@linkcode CITATION_PDF_URL_MIMETYPES} + * @param bitstreamRd + * @private + */ + protected getFirstAllowedFormatBitstreamLink(bitstreamRd: RemoteData>): Observable { + if (hasValue(bitstreamRd.payload) && isNotEmpty(bitstreamRd.payload.page)) { + // Retrieve the formats of all bitstreams in the page sequentially + return observableConcat( + ...bitstreamRd.payload.page.map((bitstream: Bitstream) => bitstream.format.pipe( + getFirstSucceededRemoteDataPayload(), + // Keep the original bitstream, because it, not the format, is what we'll need + // for the link at the end + map((format: BitstreamFormat) => [bitstream, format]), + )), + ).pipe( + // Verify that the bitstream is downloadable + mergeMap(([bitstream, format]: [Bitstream, BitstreamFormat]) => observableOf(bitstream).pipe( + getDownloadableBitstream(this.authorizationService), + map((bit: Bitstream) => [bit, format]), + )), + // Filter out only pairs with whitelisted formats and non-null bitstreams, null from download check + filter(([bitstream, format]: [Bitstream, BitstreamFormat]) => + hasValue(format) && hasValue(bitstream) && this.CITATION_PDF_URL_MIMETYPES.includes(format.mimetype)), + // We only need 1 + take(1), + // Emit the link of the match + // tap((v) => console.log('result', v)), + map(([bitstream, format]: [Bitstream, BitstreamFormat]) => getBitstreamDownloadRoute(bitstream)), + ); + } else { + return EMPTY; + } + } + + /** + * Add to the containing the current DSpace version + */ + protected setGenerator(): void { + this.rootService.findRoot().pipe(getFirstSucceededRemoteDataPayload()).subscribe((root) => { + this.meta.addTag({ name: 'Generator', content: root.dspaceVersion }); + }); + } + + protected hasType(value: string): boolean { + return this.currentObject.value.hasMetadata('dc.type', { value: value, ignoreCase: true }); + } + + /** + * Returns true if this._item is a dissertation + * + * @returns {boolean} + * true if this._item has a dc.type equal to 'Thesis' + */ + protected isDissertation(): boolean { + return this.hasType('thesis'); + } + + /** + * Returns true if this._item is a technical report + * + * @returns {boolean} + * true if this._item has a dc.type equal to 'Technical Report' + */ + protected isTechReport(): boolean { + return this.hasType('technical report'); + } + + protected getMetaTagValue(key: string): string { + return this.currentObject.value.firstMetadataValue(key); + } + + protected getFirstMetaTagValue(keys: string[]): string { + return this.currentObject.value.firstMetadataValue(keys); + } + + protected getMetaTagValuesAndCombine(key: string): string { + return this.getMetaTagValues([key]).join('; '); + } + + protected getMetaTagValues(keys: string[]): string[] { + return this.currentObject.value.allMetadataValues(keys); + } + + protected addMetaTag(name: string, content: string): void { + if (content) { + const tag = { name, content } as MetaDefinition; + this.meta.addTag(tag); + this.storeTag(name); + } + } + + protected addMetaTags(name: string, content: string[]): void { + for (const value of content) { + this.addMetaTag(name, value); + } + } + + protected storeTag(key: string): void { + this.store.dispatch(new AddMetaTagAction(key)); + } + + protected clearMetaTags(): void { + this.store.pipe( + select(tagsInUseSelector), + take(1), + ).subscribe((tagsInUse: string[]) => { + for (const name of tagsInUse) { + this.meta.removeTag('name=\'' + name + '\''); + } + this.store.dispatch(new ClearMetaTagAction()); + }); + } + + +} diff --git a/src/app/core/metadata/metadata-field.model.ts b/src/app/core/metadata/metadata-field.model.ts index 1084b4009fd..11b6e1c74e2 100644 --- a/src/app/core/metadata/metadata-field.model.ts +++ b/src/app/core/metadata/metadata-field.model.ts @@ -32,37 +32,37 @@ export class MetadataField extends ListableObject implements HALResource { */ @excludeFromEquals @autoserialize - type: ResourceType; + type: ResourceType; /** * The identifier of this metadata field */ @autoserialize - id: number; + id: number; /** * The element of this metadata field */ @autoserialize - element: string; + element: string; /** * The qualifier of this metadata field */ @autoserialize - qualifier: string; + qualifier: string; /** * The scope note of this metadata field */ @autoserialize - scopeNote: string; + scopeNote: string; /** * The {@link HALLink}s for this MetadataField */ @deserialize - _links: { + _links: { self: HALLink, schema: HALLink }; @@ -72,7 +72,7 @@ export class MetadataField extends ListableObject implements HALResource { * Will be undefined unless the schema {@link HALLink} has been resolved. */ @link(METADATA_SCHEMA) - schema?: Observable>; + schema?: Observable>; /** * Method to print this metadata field as a string without the schema diff --git a/src/app/core/metadata/metadata-schema.model.ts b/src/app/core/metadata/metadata-schema.model.ts index 85faf5d2ce7..656638b4c63 100644 --- a/src/app/core/metadata/metadata-schema.model.ts +++ b/src/app/core/metadata/metadata-schema.model.ts @@ -23,29 +23,29 @@ export class MetadataSchema extends ListableObject implements HALResource { * The unique identifier for this metadata schema */ @autoserialize - id: number; + id: number; /** * The object type */ @excludeFromEquals @autoserialize - type: ResourceType; + type: ResourceType; /** * A unique prefix that defines this schema */ @autoserialize - prefix: string; + prefix: string; /** * The namespace of this metadata schema */ @autoserialize - namespace: string; + namespace: string; @deserialize - _links: { + _links: { self: HALLink, }; diff --git a/src/app/core/metadata/metadata.service.spec.ts b/src/app/core/metadata/metadata.service.spec.ts index 66715f71f0c..79158c7ec0f 100644 --- a/src/app/core/metadata/metadata.service.spec.ts +++ b/src/app/core/metadata/metadata.service.spec.ts @@ -1,520 +1,16 @@ -import { - fakeAsync, - tick, -} from '@angular/core/testing'; -import { - Meta, - Title, -} from '@angular/platform-browser'; -import { - NavigationEnd, - Router, -} from '@angular/router'; -import { createMockStore } from '@ngrx/store/testing'; -import { TranslateService } from '@ngx-translate/core'; -import { - Observable, - of as observableOf, - of, -} from 'rxjs'; +import { TestBed } from '@angular/core/testing'; -import { AppConfig } from '../../../config/app-config.interface'; -import { - ItemMock, - MockBitstream1, - MockBitstream2, - MockBitstream3, -} from '../../shared/mocks/item.mock'; -import { getMockTranslateService } from '../../shared/mocks/translate.service.mock'; -import { - createSuccessfulRemoteDataObject, - createSuccessfulRemoteDataObject$, -} from '../../shared/remote-data.utils'; -import { createPaginatedList } from '../../shared/testing/utils.test'; -import { DSONameService } from '../breadcrumbs/dso-name.service'; -import { AuthorizationDataService } from '../data/feature-authorization/authorization-data.service'; -import { PaginatedList } from '../data/paginated-list.model'; -import { RemoteData } from '../data/remote-data'; -import { RootDataService } from '../data/root-data.service'; -import { HardRedirectService } from '../services/hard-redirect.service'; -import { Bitstream } from '../shared/bitstream.model'; -import { Bundle } from '../shared/bundle.model'; -import { Item } from '../shared/item.model'; -import { MetadataValue } from '../shared/metadata.models'; -import { - AddMetaTagAction, - ClearMetaTagAction, -} from './meta-tag.actions'; import { MetadataService } from './metadata.service'; describe('MetadataService', () => { - let metadataService: MetadataService; - - let meta: Meta; - - let title: Title; - - let dsoNameService: DSONameService; - - let bundleDataService; - let bitstreamDataService; - let rootService: RootDataService; - let translateService: TranslateService; - let hardRedirectService: HardRedirectService; - let authorizationService: AuthorizationDataService; - - let router: Router; - let store; - - let appConfig: AppConfig; - - const initialState = { 'core': { metaTag: { tagsInUse: ['title', 'description'] } } }; - + let service: MetadataService; beforeEach(() => { - rootService = jasmine.createSpyObj({ - findRoot: createSuccessfulRemoteDataObject$({ dspaceVersion: 'mock-dspace-version' }), - }); - bitstreamDataService = jasmine.createSpyObj({ - findListByHref: createSuccessfulRemoteDataObject$(createPaginatedList([MockBitstream3])), - }); - bundleDataService = jasmine.createSpyObj({ - findByItemAndName: mockBundleRD$([MockBitstream3]), - }); - translateService = getMockTranslateService(); - meta = jasmine.createSpyObj('meta', { - addTag: {}, - removeTag: {}, - }); - title = jasmine.createSpyObj({ - setTitle: {}, - }); - dsoNameService = jasmine.createSpyObj({ - getName: ItemMock.firstMetadataValue('dc.title'), - }); - router = { - url: '/items/0ec7ff22-f211-40ab-a69e-c819b0b1f357', - events: of(new NavigationEnd(1, '', '')), - routerState: { - root: {}, - }, - } as any as Router; - hardRedirectService = jasmine.createSpyObj( { - getCurrentOrigin: 'https://request.org', - }); - authorizationService = jasmine.createSpyObj('authorizationService', { - isAuthorized: observableOf(true), - }); - - store = createMockStore({ initialState }); - spyOn(store, 'dispatch'); - - appConfig = { - item: { - bitstream: { - pageSize: 5, - }, - }, - } as any; - - metadataService = new MetadataService( - router, - translateService, - meta, - title, - dsoNameService, - bundleDataService, - bitstreamDataService, - undefined, - rootService, - store, - hardRedirectService, - appConfig, - authorizationService, - ); - }); - - it('items page should set meta tags', fakeAsync(() => { - (metadataService as any).processRouteChange({ - data: { - value: { - dso: createSuccessfulRemoteDataObject(ItemMock), - }, - }, - }); - tick(); - expect(title.setTitle).toHaveBeenCalledWith('Test PowerPoint Document'); - expect(meta.addTag).toHaveBeenCalledWith({ - name: 'citation_title', - content: 'Test PowerPoint Document', - }); - expect(meta.addTag).toHaveBeenCalledWith({ name: 'citation_author', content: 'Doe, Jane' }); - expect(meta.addTag).toHaveBeenCalledWith({ - name: 'citation_publication_date', - content: '1650-06-26', - }); - expect(meta.addTag).toHaveBeenCalledWith({ name: 'citation_issn', content: '123456789' }); - expect(meta.addTag).toHaveBeenCalledWith({ name: 'citation_language', content: 'en' }); - expect(meta.addTag).toHaveBeenCalledWith({ - name: 'citation_keywords', - content: 'keyword1; keyword2; keyword3', - }); - })); - - it('items page should set meta tags as published Thesis', fakeAsync(() => { - (metadataService as any).processRouteChange({ - data: { - value: { - dso: createSuccessfulRemoteDataObject(mockPublisher(mockType(ItemMock, 'Thesis'))), - }, - }, - }); - tick(); - expect(meta.addTag).toHaveBeenCalledWith({ - name: 'citation_dissertation_name', - content: 'Test PowerPoint Document', - }); - expect(meta.addTag).toHaveBeenCalledWith({ - name: 'citation_pdf_url', - content: 'https://request.org/bitstreams/4db100c1-e1f5-4055-9404-9bc3e2d15f29/download', - }); - })); - - it('items page should set meta tags as published Technical Report', fakeAsync(() => { - (metadataService as any).processRouteChange({ - data: { - value: { - dso: createSuccessfulRemoteDataObject(mockPublisher(mockType(ItemMock, 'Technical Report'))), - }, - }, - }); - tick(); - expect(meta.addTag).toHaveBeenCalledWith({ - name: 'citation_technical_report_institution', - content: 'Mock Publisher', - }); - })); - - it('route titles should overwrite dso titles', fakeAsync(() => { - (translateService.get as jasmine.Spy).and.returnValues(of('DSpace :: '), of('Translated Route Title')); - (metadataService as any).processRouteChange({ - data: { - value: { - dso: createSuccessfulRemoteDataObject(ItemMock), - title: 'route.title.key', - }, - }, - }); - tick(); - expect(title.setTitle).toHaveBeenCalledTimes(2); - expect((title.setTitle as jasmine.Spy).calls.argsFor(0)).toEqual(['Test PowerPoint Document']); - expect((title.setTitle as jasmine.Spy).calls.argsFor(1)).toEqual(['DSpace :: Translated Route Title']); - })); - - it('other navigation should add title and description', fakeAsync(() => { - (translateService.get as jasmine.Spy).and.returnValues(of('DSpace :: '), of('Dummy Title'), of('This is a dummy item component for testing!')); - (metadataService as any).processRouteChange({ - data: { - value: { - title: 'Dummy Title', - description: 'This is a dummy item component for testing!', - }, - }, - }); - tick(); - expect(title.setTitle).toHaveBeenCalledWith('DSpace :: Dummy Title'); - expect(meta.addTag).toHaveBeenCalledWith({ - name: 'title', - content: 'DSpace :: Dummy Title', - }); - expect(meta.addTag).toHaveBeenCalledWith({ - name: 'description', - content: 'This is a dummy item component for testing!', - }); - })); - - describe(`listenForRouteChange`, () => { - it(`should call processRouteChange`, fakeAsync(() => { - spyOn(metadataService as any, 'processRouteChange').and.callFake(() => undefined); - metadataService.listenForRouteChange(); - tick(); - expect((metadataService as any).processRouteChange).toHaveBeenCalled(); - })); - it(`should add Generator`, fakeAsync(() => { - spyOn(metadataService as any, 'processRouteChange').and.callFake(() => undefined); - metadataService.listenForRouteChange(); - tick(); - expect(meta.addTag).toHaveBeenCalledWith({ - name: 'Generator', - content: 'mock-dspace-version', - }); - })); + TestBed.configureTestingModule({}); + service = TestBed.inject(MetadataService); }); - describe('citation_abstract_html_url', () => { - it('should use dc.identifier.uri if available', fakeAsync(() => { - (metadataService as any).processRouteChange({ - data: { - value: { - dso: createSuccessfulRemoteDataObject(mockUri(ItemMock, 'https://ddg.gg')), - }, - }, - }); - tick(); - expect(meta.addTag).toHaveBeenCalledWith({ - name: 'citation_abstract_html_url', - content: 'https://ddg.gg', - }); - })); - - it('should use current route as fallback', fakeAsync(() => { - (metadataService as any).processRouteChange({ - data: { - value: { - dso: createSuccessfulRemoteDataObject(mockUri(ItemMock)), - }, - }, - }); - tick(); - expect(meta.addTag).toHaveBeenCalledWith({ - name: 'citation_abstract_html_url', - content: 'https://request.org/items/0ec7ff22-f211-40ab-a69e-c819b0b1f357', - }); - })); + it('should be created', () => { + expect(service).toBeTruthy(); }); - - describe('citation_*_institution / citation_publisher', () => { - it('should use citation_dissertation_institution tag for dissertations', fakeAsync(() => { - (metadataService as any).processRouteChange({ - data: { - value: { - dso: createSuccessfulRemoteDataObject(mockPublisher(mockType(ItemMock, 'Thesis'))), - }, - }, - }); - tick(); - expect(meta.addTag).toHaveBeenCalledWith({ - name: 'citation_dissertation_institution', - content: 'Mock Publisher', - }); - expect(meta.addTag).not.toHaveBeenCalledWith(jasmine.objectContaining({ name: 'citation_technical_report_institution' })); - expect(meta.addTag).not.toHaveBeenCalledWith(jasmine.objectContaining({ name: 'citation_publisher' })); - })); - - it('should use citation_tech_report_institution tag for tech reports', fakeAsync(() => { - (metadataService as any).processRouteChange({ - data: { - value: { - dso: createSuccessfulRemoteDataObject(mockPublisher(mockType(ItemMock, 'Technical Report'))), - }, - }, - }); - tick(); - expect(meta.addTag).not.toHaveBeenCalledWith(jasmine.objectContaining({ name: 'citation_dissertation_institution' })); - expect(meta.addTag).toHaveBeenCalledWith({ - name: 'citation_technical_report_institution', - content: 'Mock Publisher', - }); - expect(meta.addTag).not.toHaveBeenCalledWith(jasmine.objectContaining({ name: 'citation_publisher' })); - })); - - it('should use citation_publisher for other item types', fakeAsync(() => { - (metadataService as any).processRouteChange({ - data: { - value: { - dso: createSuccessfulRemoteDataObject(mockPublisher(mockType(ItemMock, 'Some Other Type'))), - }, - }, - }); - tick(); - expect(meta.addTag).not.toHaveBeenCalledWith(jasmine.objectContaining({ name: 'citation_dissertation_institution' })); - expect(meta.addTag).not.toHaveBeenCalledWith(jasmine.objectContaining({ name: 'citation_technical_report_institution' })); - expect(meta.addTag).toHaveBeenCalledWith({ - name: 'citation_publisher', - content: 'Mock Publisher', - }); - })); - }); - - describe('citation_pdf_url', () => { - it('should link to primary Bitstream URL regardless of format', fakeAsync(() => { - (bundleDataService.findByItemAndName as jasmine.Spy).and.returnValue(mockBundleRD$([], MockBitstream3)); - - (metadataService as any).processRouteChange({ - data: { - value: { - dso: createSuccessfulRemoteDataObject(ItemMock), - }, - }, - }); - tick(); - expect(meta.addTag).toHaveBeenCalledWith({ - name: 'citation_pdf_url', - content: 'https://request.org/bitstreams/4db100c1-e1f5-4055-9404-9bc3e2d15f29/download', - }); - })); - - describe('bitstream not download allowed', () => { - it('should not have citation_pdf_url', fakeAsync(() => { - (bundleDataService.findByItemAndName as jasmine.Spy).and.returnValue(mockBundleRD$([MockBitstream3])); - (authorizationService.isAuthorized as jasmine.Spy).and.returnValue(observableOf(false)); - - (metadataService as any).processRouteChange({ - data: { - value: { - dso: createSuccessfulRemoteDataObject(ItemMock), - }, - }, - }); - tick(); - expect(meta.addTag).not.toHaveBeenCalledWith(jasmine.objectContaining({ name: 'citation_pdf_url' })); - })); - - }); - - describe('no primary Bitstream', () => { - it('should link to first and only Bitstream regardless of format', fakeAsync(() => { - (bundleDataService.findByItemAndName as jasmine.Spy).and.returnValue(mockBundleRD$([MockBitstream3])); - - (metadataService as any).processRouteChange({ - data: { - value: { - dso: createSuccessfulRemoteDataObject(ItemMock), - }, - }, - }); - tick(); - expect(meta.addTag).toHaveBeenCalledWith({ - name: 'citation_pdf_url', - content: 'https://request.org/bitstreams/4db100c1-e1f5-4055-9404-9bc3e2d15f29/download', - }); - })); - - describe(`when there's a bitstream with an allowed format on the first page`, () => { - let bitstreams; - - beforeEach(() => { - bitstreams = [MockBitstream2, MockBitstream3, MockBitstream1]; - (bundleDataService.findByItemAndName as jasmine.Spy).and.returnValue(mockBundleRD$(bitstreams)); - (bitstreamDataService.findListByHref as jasmine.Spy).and.returnValues( - ...mockBitstreamPages$(bitstreams).map(bp => createSuccessfulRemoteDataObject$(bp)), - ); - }); - - it('should link to first Bitstream with allowed format', fakeAsync(() => { - (metadataService as any).processRouteChange({ - data: { - value: { - dso: createSuccessfulRemoteDataObject(ItemMock), - }, - }, - }); - tick(); - expect(meta.addTag).toHaveBeenCalledWith({ - name: 'citation_pdf_url', - content: 'https://request.org/bitstreams/99b00f3c-1cc6-4689-8158-91965bee6b28/download', - }); - })); - - }); - - }); - }); - - describe(`when there's no bitstream with an allowed format on the first page`, () => { - let bitstreams; - - beforeEach(() => { - bitstreams = [MockBitstream1, MockBitstream3, MockBitstream2]; - (bundleDataService.findByItemAndName as jasmine.Spy).and.returnValue(mockBundleRD$(bitstreams)); - (bitstreamDataService.findListByHref as jasmine.Spy).and.returnValues( - ...mockBitstreamPages$(bitstreams).map(bp => createSuccessfulRemoteDataObject$(bp)), - ); - }); - - it(`shouldn't add a citation_pdf_url meta tag`, fakeAsync(() => { - (metadataService as any).processRouteChange({ - data: { - value: { - dso: createSuccessfulRemoteDataObject(ItemMock), - }, - }, - }); - tick(); - expect(meta.addTag).not.toHaveBeenCalledWith({ - name: 'citation_pdf_url', - content: 'https://request.org/bitstreams/99b00f3c-1cc6-4689-8158-91965bee6b28/download', - }); - })); - - }); - - - describe('tagstore', () => { - beforeEach(fakeAsync(() => { - (metadataService as any).processRouteChange({ - data: { - value: { - dso: createSuccessfulRemoteDataObject(ItemMock), - }, - }, - }); - tick(); - })); - - it('should remove previous tags on route change', fakeAsync(() => { - expect(meta.removeTag).toHaveBeenCalledWith('name=\'title\''); - expect(meta.removeTag).toHaveBeenCalledWith('name=\'description\''); - })); - - it('should clear all tags and add new ones on route change', () => { - expect(store.dispatch.calls.argsFor(0)).toEqual([new ClearMetaTagAction()]); - expect(store.dispatch.calls.argsFor(1)).toEqual([new AddMetaTagAction('title')]); - expect(store.dispatch.calls.argsFor(2)).toEqual([new AddMetaTagAction('description')]); - }); - }); - - const mockType = (mockItem: Item, type: string): Item => { - const typedMockItem = Object.assign(new Item(), mockItem) as Item; - typedMockItem.metadata['dc.type'] = [{ value: type }] as MetadataValue[]; - return typedMockItem; - }; - - const mockPublisher = (mockItem: Item): Item => { - const publishedMockItem = Object.assign(new Item(), mockItem) as Item; - publishedMockItem.metadata['dc.publisher'] = [ - { - language: 'en_US', - value: 'Mock Publisher', - }, - ] as MetadataValue[]; - return publishedMockItem; - }; - - const mockUri = (mockItem: Item, uri?: string): Item => { - const publishedMockItem = Object.assign(new Item(), mockItem) as Item; - publishedMockItem.metadata['dc.identifier.uri'] = [{ value: uri }] as MetadataValue[]; - return publishedMockItem; - }; - - const mockBundleRD$ = (bitstreams: Bitstream[], primary?: Bitstream): Observable> => { - return createSuccessfulRemoteDataObject$( - Object.assign(new Bundle(), { - name: 'ORIGINAL', - bitstreams: createSuccessfulRemoteDataObject$(mockBitstreamPages$(bitstreams)[0]), - primaryBitstream: createSuccessfulRemoteDataObject$(primary), - }), - ); - }; - - const mockBitstreamPages$ = (bitstreams: Bitstream[]): PaginatedList[] => { - return bitstreams.map((bitstream, index) => Object.assign(createPaginatedList([bitstream]), { - pageInfo: { - totalElements: bitstreams.length, // announce multiple elements/pages - }, - _links: index < bitstreams.length - 1 - ? { next: { href: 'not empty' } } // fake link to the next bitstream page - : { next: { href: undefined } }, // last page has no link - })); - }; }); diff --git a/src/app/core/metadata/metadata.service.ts b/src/app/core/metadata/metadata.service.ts index ea4b25b774e..dcc355edd0f 100644 --- a/src/app/core/metadata/metadata.service.ts +++ b/src/app/core/metadata/metadata.service.ts @@ -1,529 +1,37 @@ -import { - Inject, - Injectable, -} from '@angular/core'; -import { - Meta, - MetaDefinition, - Title, -} from '@angular/platform-browser'; -import { - ActivatedRoute, - NavigationEnd, - Router, -} from '@angular/router'; -import { - createSelector, - select, - Store, -} from '@ngrx/store'; -import { TranslateService } from '@ngx-translate/core'; -import { - BehaviorSubject, - combineLatest, - concat as observableConcat, - EMPTY, - Observable, - of as observableOf, -} from 'rxjs'; -import { - filter, - map, - mergeMap, - switchMap, - take, -} from 'rxjs/operators'; +import { Injectable } from '@angular/core'; +import { hasValue } from '../../shared/empty.util'; import { - APP_CONFIG, - AppConfig, -} from '../../../config/app-config.interface'; -import { getBitstreamDownloadRoute } from '../../app-routing-paths'; -import { - hasNoValue, - hasValue, - isNotEmpty, -} from '../../shared/empty.util'; -import { followLink } from '../../shared/utils/follow-link-config.model'; -import { DSONameService } from '../breadcrumbs/dso-name.service'; -import { coreSelector } from '../core.selectors'; -import { CoreState } from '../core-state.model'; -import { BitstreamDataService } from '../data/bitstream-data.service'; -import { BitstreamFormatDataService } from '../data/bitstream-format-data.service'; -import { BundleDataService } from '../data/bundle-data.service'; -import { AuthorizationDataService } from '../data/feature-authorization/authorization-data.service'; -import { PaginatedList } from '../data/paginated-list.model'; -import { RemoteData } from '../data/remote-data'; -import { RootDataService } from '../data/root-data.service'; -import { HardRedirectService } from '../services/hard-redirect.service'; -import { Bitstream } from '../shared/bitstream.model'; -import { getDownloadableBitstream } from '../shared/bitstream.operators'; -import { BitstreamFormat } from '../shared/bitstream-format.model'; -import { Bundle } from '../shared/bundle.model'; -import { DSpaceObject } from '../shared/dspace-object.model'; -import { Item } from '../shared/item.model'; -import { - getFirstCompletedRemoteData, - getFirstSucceededRemoteDataPayload, -} from '../shared/operators'; -import { URLCombiner } from '../url-combiner/url-combiner'; -import { - AddMetaTagAction, - ClearMetaTagAction, -} from './meta-tag.actions'; -import { MetaTagState } from './meta-tag.reducer'; + MetadataValue, + VIRTUAL_METADATA_PREFIX, +} from '../shared/metadata.models'; /** - * The base selector function to select the metaTag section in the store + * Service for working with DSpace object metadata. */ -const metaTagSelector = createSelector( - coreSelector, - (state: CoreState) => state.metaTag, -); - -/** - * Selector function to select the tags in use from the MetaTagState - */ -const tagsInUseSelector = - createSelector( - metaTagSelector, - (state: MetaTagState) => state.tagsInUse, - ); - -@Injectable({ providedIn: 'root' }) +@Injectable({ + providedIn: 'root', +}) export class MetadataService { - private currentObject: BehaviorSubject = new BehaviorSubject(undefined); - - /** - * When generating the citation_pdf_url meta tag for Items with more than one Bitstream (and no primary Bitstream), - * the first Bitstream to match one of the following MIME types is selected. - * See {@linkcode getFirstAllowedFormatBitstreamLink} - * @private - */ - private readonly CITATION_PDF_URL_MIMETYPES = [ - 'application/pdf', // .pdf - 'application/postscript', // .ps - 'application/msword', // .doc - 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', // .docx - 'application/rtf', // .rtf - 'application/epub+zip', // .epub - ]; - - constructor( - private router: Router, - private translate: TranslateService, - private meta: Meta, - private title: Title, - private dsoNameService: DSONameService, - private bundleDataService: BundleDataService, - private bitstreamDataService: BitstreamDataService, - private bitstreamFormatDataService: BitstreamFormatDataService, - private rootService: RootDataService, - private store: Store, - private hardRedirectService: HardRedirectService, - @Inject(APP_CONFIG) private appConfig: AppConfig, - private authorizationService: AuthorizationDataService, - ) { - } - - public listenForRouteChange(): void { - // This never changes, set it only once - this.setGenerator(); - - this.router.events.pipe( - filter((event) => event instanceof NavigationEnd), - map(() => this.router.routerState.root), - map((route: ActivatedRoute) => { - route = this.getCurrentRoute(route); - return { params: route.params, data: route.data }; - })).subscribe((routeInfo: any) => { - this.processRouteChange(routeInfo); - }); - } - - private processRouteChange(routeInfo: any): void { - this.clearMetaTags(); - - if (hasValue(routeInfo.data.value.dso) && hasValue(routeInfo.data.value.dso.payload)) { - this.currentObject.next(routeInfo.data.value.dso.payload); - this.setDSOMetaTags(); - } - - if (routeInfo.data.value.title) { - const titlePrefix = this.translate.get('repository.title.prefix'); - const title = this.translate.get(routeInfo.data.value.title, routeInfo.data.value); - combineLatest([titlePrefix, title]).pipe(take(1)).subscribe(([translatedTitlePrefix, translatedTitle]: [string, string]) => { - this.addMetaTag('title', translatedTitlePrefix + translatedTitle); - this.title.setTitle(translatedTitlePrefix + translatedTitle); - }); - } - if (routeInfo.data.value.description) { - this.translate.get(routeInfo.data.value.description).pipe(take(1)).subscribe((translatedDescription: string) => { - this.addMetaTag('description', translatedDescription); - }); - } - } - - private getCurrentRoute(route: ActivatedRoute): ActivatedRoute { - while (route.firstChild) { - route = route.firstChild; - } - return route; - } - - private setDSOMetaTags(): void { - - this.setTitleTag(); - this.setDescriptionTag(); - - this.setCitationTitleTag(); - this.setCitationAuthorTags(); - this.setCitationPublicationDateTag(); - this.setCitationISSNTag(); - this.setCitationISBNTag(); - - this.setCitationLanguageTag(); - this.setCitationKeywordsTag(); - - this.setCitationAbstractUrlTag(); - this.setCitationPdfUrlTag(); - this.setCitationPublisherTag(); - - if (this.isDissertation()) { - this.setCitationDissertationNameTag(); - } - - // this.setCitationJournalTitleTag(); - // this.setCitationVolumeTag(); - // this.setCitationIssueTag(); - // this.setCitationFirstPageTag(); - // this.setCitationLastPageTag(); - // this.setCitationDOITag(); - // this.setCitationPMIDTag(); - - // this.setCitationFullTextTag(); - - // this.setCitationConferenceTag(); - - // this.setCitationPatentCountryTag(); - // this.setCitationPatentNumberTag(); - - } - - /** - * Add to the - */ - private setTitleTag(): void { - const value = this.dsoNameService.getName(this.currentObject.getValue()); - this.addMetaTag('title', value); - this.title.setTitle(value); - } - - /** - * Add to the - */ - private setDescriptionTag(): void { - // TODO: truncate abstract - const value = this.getMetaTagValue('dc.description.abstract'); - this.addMetaTag('description', value); - } - - /** - * Add to the - */ - private setCitationTitleTag(): void { - const value = this.getMetaTagValue('dc.title'); - this.addMetaTag('citation_title', value); - } - - /** - * Add to the - */ - private setCitationAuthorTags(): void { - const values: string[] = this.getMetaTagValues(['dc.author', 'dc.contributor.author', 'dc.creator']); - this.addMetaTags('citation_author', values); - } - - /** - * Add to the - */ - private setCitationPublicationDateTag(): void { - const value = this.getFirstMetaTagValue(['dc.date.copyright', 'dc.date.issued', 'dc.date.available', 'dc.date.accessioned']); - this.addMetaTag('citation_publication_date', value); - } - - /** - * Add to the - */ - private setCitationISSNTag(): void { - const value = this.getMetaTagValue('dc.identifier.issn'); - this.addMetaTag('citation_issn', value); - } - - /** - * Add to the - */ - private setCitationISBNTag(): void { - const value = this.getMetaTagValue('dc.identifier.isbn'); - this.addMetaTag('citation_isbn', value); - } - - /** - * Add to the - */ - private setCitationLanguageTag(): void { - const value = this.getFirstMetaTagValue(['dc.language', 'dc.language.iso']); - this.addMetaTag('citation_language', value); - } - - /** - * Add to the - */ - private setCitationDissertationNameTag(): void { - const value = this.getMetaTagValue('dc.title'); - this.addMetaTag('citation_dissertation_name', value); - } - - /** - * Add dc.publisher to the . The tag name depends on the item type. - */ - private setCitationPublisherTag(): void { - const value = this.getMetaTagValue('dc.publisher'); - if (this.isDissertation()) { - this.addMetaTag('citation_dissertation_institution', value); - } else if (this.isTechReport()) { - this.addMetaTag('citation_technical_report_institution', value); - } else { - this.addMetaTag('citation_publisher', value); - } - } - /** - * Add to the + * Returns true if this Metadata authority key starts with 'virtual::' */ - private setCitationKeywordsTag(): void { - const value = this.getMetaTagValuesAndCombine('dc.subject'); - this.addMetaTag('citation_keywords', value); + public isVirtual(metadataValue: MetadataValue | undefined): boolean { + return hasValue(metadataValue?.authority) && metadataValue.authority.startsWith(VIRTUAL_METADATA_PREFIX); } /** - * Add to the - */ - private setCitationAbstractUrlTag(): void { - if (this.currentObject.value instanceof Item) { - let url = this.getMetaTagValue('dc.identifier.uri'); - if (hasNoValue(url)) { - url = new URLCombiner(this.hardRedirectService.getCurrentOrigin(), this.router.url).toString(); - } - this.addMetaTag('citation_abstract_html_url', url); - } - } - - /** - * Add to the - */ - private setCitationPdfUrlTag(): void { - if (this.currentObject.value instanceof Item) { - const item = this.currentObject.value as Item; - - // Retrieve the ORIGINAL bundle for the item - this.bundleDataService.findByItemAndName( - item, - 'ORIGINAL', - true, - true, - followLink('primaryBitstream'), - followLink('bitstreams', { - findListOptions: { - // limit the number of bitstreams used to find the citation pdf url to the number - // shown by default on an item page - elementsPerPage: this.appConfig.item.bitstream.pageSize, - }, - }, followLink('format')), - ).pipe( - getFirstSucceededRemoteDataPayload(), - switchMap((bundle: Bundle) => - // First try the primary bitstream - bundle.primaryBitstream.pipe( - getFirstCompletedRemoteData(), - map((rd: RemoteData) => { - if (hasValue(rd.payload)) { - return rd.payload; - } else { - return null; - } - }), - getDownloadableBitstream(this.authorizationService), - // return the bundle as well so we can use it again if there's no primary bitstream - map((bitstream: Bitstream) => [bundle, bitstream]), - ), - ), - switchMap(([bundle, primaryBitstream]: [Bundle, Bitstream]) => { - if (hasValue(primaryBitstream)) { - // If there was a downloadable primary bitstream, emit its link - return [getBitstreamDownloadRoute(primaryBitstream)]; - } else { - // Otherwise consider the regular bitstreams in the bundle - return bundle.bitstreams.pipe( - getFirstCompletedRemoteData(), - switchMap((bitstreamRd: RemoteData>) => { - if (hasValue(bitstreamRd.payload) && bitstreamRd.payload.totalElements === 1) { - // If there's only one bitstream in the bundle, emit its link if its downloadable - return this.getBitLinkIfDownloadable(bitstreamRd.payload.page[0], bitstreamRd); - } else { - // Otherwise check all bitstreams to see if one matches the format whitelist - return this.getFirstAllowedFormatBitstreamLink(bitstreamRd); - } - }), - ); - } - }), - take(1), - ).subscribe((link: string) => { - // Use the found link to set the tag - this.addMetaTag( - 'citation_pdf_url', - new URLCombiner(this.hardRedirectService.getCurrentOrigin(), link).toString(), - ); - }); - } - } - - getBitLinkIfDownloadable(bitstream: Bitstream, bitstreamRd: RemoteData>): Observable { - return observableOf(bitstream).pipe( - getDownloadableBitstream(this.authorizationService), - switchMap((bit: Bitstream) => { - if (hasValue(bit)) { - return [getBitstreamDownloadRoute(bit)]; - } else { - // Otherwise check all bitstreams to see if one matches the format whitelist - return this.getFirstAllowedFormatBitstreamLink(bitstreamRd); - } - }), - ); - } - - /** - * For Items with more than one Bitstream (and no primary Bitstream), link to the first Bitstream - * with a MIME type. + * If this is a virtual Metadata, it returns everything in the authority key after 'virtual::'. * - * Note this will only check the current page (page size determined item.bitstream.pageSize in the - * config) of bitstreams for performance reasons. - * See https://github.com/DSpace/DSpace/issues/8648 for more info - * - * included in {@linkcode CITATION_PDF_URL_MIMETYPES} - * @param bitstreamRd - * @private + * Returns undefined otherwise. */ - private getFirstAllowedFormatBitstreamLink(bitstreamRd: RemoteData>): Observable { - if (hasValue(bitstreamRd.payload) && isNotEmpty(bitstreamRd.payload.page)) { - // Retrieve the formats of all bitstreams in the page sequentially - return observableConcat( - ...bitstreamRd.payload.page.map((bitstream: Bitstream) => bitstream.format.pipe( - getFirstSucceededRemoteDataPayload(), - // Keep the original bitstream, because it, not the format, is what we'll need - // for the link at the end - map((format: BitstreamFormat) => [bitstream, format]), - )), - ).pipe( - // Verify that the bitstream is downloadable - mergeMap(([bitstream, format]: [Bitstream, BitstreamFormat]) => observableOf(bitstream).pipe( - getDownloadableBitstream(this.authorizationService), - map((bit: Bitstream) => [bit, format]), - )), - // Filter out only pairs with whitelisted formats and non-null bitstreams, null from download check - filter(([bitstream, format]: [Bitstream, BitstreamFormat]) => - hasValue(format) && hasValue(bitstream) && this.CITATION_PDF_URL_MIMETYPES.includes(format.mimetype)), - // We only need 1 - take(1), - // Emit the link of the match - // tap((v) => console.log('result', v)), - map(([bitstream ]: [Bitstream, BitstreamFormat]) => getBitstreamDownloadRoute(bitstream)), - ); + public virtualValue(metadataValue: MetadataValue | undefined): string { + if (this.isVirtual(metadataValue)) { + return metadataValue.authority.substring(metadataValue.authority.indexOf(VIRTUAL_METADATA_PREFIX) + VIRTUAL_METADATA_PREFIX.length); } else { - return EMPTY; - } - } - - /** - * Add to the containing the current DSpace version - */ - private setGenerator(): void { - this.rootService.findRoot().pipe(getFirstSucceededRemoteDataPayload()).subscribe((root) => { - this.meta.addTag({ name: 'Generator', content: root.dspaceVersion }); - }); - } - - private hasType(value: string): boolean { - return this.currentObject.value.hasMetadata('dc.type', { value: value, ignoreCase: true }); - } - - /** - * Returns true if this._item is a dissertation - * - * @returns {boolean} - * true if this._item has a dc.type equal to 'Thesis' - */ - private isDissertation(): boolean { - return this.hasType('thesis'); - } - - /** - * Returns true if this._item is a technical report - * - * @returns {boolean} - * true if this._item has a dc.type equal to 'Technical Report' - */ - private isTechReport(): boolean { - return this.hasType('technical report'); - } - - private getMetaTagValue(key: string): string { - return this.currentObject.value.firstMetadataValue(key); - } - - private getFirstMetaTagValue(keys: string[]): string { - return this.currentObject.value.firstMetadataValue(keys); - } - - private getMetaTagValuesAndCombine(key: string): string { - return this.getMetaTagValues([key]).join('; '); - } - - private getMetaTagValues(keys: string[]): string[] { - return this.currentObject.value.allMetadataValues(keys); - } - - private addMetaTag(name: string, content: string): void { - if (content) { - const tag = { name, content } as MetaDefinition; - this.meta.addTag(tag); - this.storeTag(name); + return undefined; } } - private addMetaTags(name: string, content: string[]): void { - for (const value of content) { - this.addMetaTag(name, value); - } - } - - private storeTag(key: string): void { - this.store.dispatch(new AddMetaTagAction(key)); - } - - public clearMetaTags() { - this.store.pipe( - select(tagsInUseSelector), - take(1), - ).subscribe((tagsInUse: string[]) => { - for (const name of tagsInUse) { - this.meta.removeTag('name=\'' + name + '\''); - } - this.store.dispatch(new ClearMetaTagAction()); - }); - } - - } diff --git a/src/app/core/notifications/qa/events/quality-assurance-event-data.service.spec.ts b/src/app/core/notifications/qa/events/quality-assurance-event-data.service.spec.ts index 847eea7c7e6..c981afbe57a 100644 --- a/src/app/core/notifications/qa/events/quality-assurance-event-data.service.spec.ts +++ b/src/app/core/notifications/qa/events/quality-assurance-event-data.service.spec.ts @@ -16,6 +16,7 @@ import { NotificationsService } from '../../../../shared/notifications/notificat import { createSuccessfulRemoteDataObject } from '../../../../shared/remote-data.utils'; import { ObjectCacheServiceStub } from '../../../../shared/testing/object-cache-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 { RestResponse } from '../../../cache/response.models'; import { FindListOptions } from '../../../data/find-list-options.model'; @@ -131,10 +132,7 @@ describe('QualityAssuranceEventDataService', () => { it('should proxy the call to searchData.searchBy', () => { const options: FindListOptions = { searchParams: [ - { - fieldName: 'topic', - fieldValue: topic, - }, + new RequestParam('topic', topic), ], }; service.getEventsByTopic(topic); diff --git a/src/app/core/notifications/qa/events/quality-assurance-event-data.service.ts b/src/app/core/notifications/qa/events/quality-assurance-event-data.service.ts index 787de7b636c..7e6e8884173 100644 --- a/src/app/core/notifications/qa/events/quality-assurance-event-data.service.ts +++ b/src/app/core/notifications/qa/events/quality-assurance-event-data.service.ts @@ -16,6 +16,7 @@ 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 { RequestParam } from '../../../cache/models/request-param.model'; import { ObjectCacheService } from '../../../cache/object-cache.service'; import { CreateData, @@ -97,10 +98,7 @@ export class QualityAssuranceEventDataService extends IdentifiableDataService[]): Observable>> { options.searchParams = [ - { - fieldName: 'topic', - fieldValue: topic, - }, + new RequestParam('topic', topic), ]; return this.searchData.searchBy('findByTopic', options, true, true, ...linksToFollow); } diff --git a/src/app/core/notifications/qa/models/quality-assurance-event.model.ts b/src/app/core/notifications/qa/models/quality-assurance-event.model.ts index 9210d8f6f90..e015dd01d92 100644 --- a/src/app/core/notifications/qa/models/quality-assurance-event.model.ts +++ b/src/app/core/notifications/qa/models/quality-assurance-event.model.ts @@ -104,62 +104,62 @@ export class QualityAssuranceEventObject implements CacheableObject { * The Quality Assurance event uuid inside DSpace */ @autoserialize - id: string; + id: string; /** * The universally unique identifier of this Quality Assurance event */ @autoserializeAs(String, 'id') - uuid: string; + uuid: string; /** * The Quality Assurance event original id (ex.: the source archive OAI-PMH identifier) */ @autoserialize - originalId: string; + originalId: string; /** * The title of the article to which the suggestion refers */ @autoserialize - title: string; + title: string; /** * Reliability of the suggestion (of the data inside 'message') */ @autoserialize - trust: number; + trust: number; /** * The timestamp Quality Assurance event was saved in DSpace */ @autoserialize - eventDate: string; + eventDate: string; /** * The Quality Assurance event status (ACCEPTED, REJECTED, DISCARDED, PENDING) */ @autoserialize - status: string; + status: string; /** * The suggestion data. Data may vary depending on the source */ @autoserialize - message: SourceQualityAssuranceEventMessageObject; + message: SourceQualityAssuranceEventMessageObject; /** * The type of this ConfigObject */ @excludeFromEquals @autoserialize - type: ResourceType; + type: ResourceType; /** * The links to all related resources returned by the rest api. */ @deserialize - _links: { + _links: { self: HALLink, target: HALLink, related: HALLink @@ -170,12 +170,12 @@ export class QualityAssuranceEventObject implements CacheableObject { * Will be undefined unless the {@item HALLink} has been resolved. */ @link(ITEM) - target?: Observable>; + target?: Observable>; /** * The related project for this Event * Will be undefined unless the {@related HALLink} has been resolved. */ @link(ITEM) - related?: Observable>; + related?: Observable>; } diff --git a/src/app/core/notifications/qa/models/quality-assurance-source.model.ts b/src/app/core/notifications/qa/models/quality-assurance-source.model.ts index cc5f874119c..56c674b031c 100644 --- a/src/app/core/notifications/qa/models/quality-assurance-source.model.ts +++ b/src/app/core/notifications/qa/models/quality-assurance-source.model.ts @@ -24,32 +24,32 @@ export class QualityAssuranceSourceObject implements CacheableObject { * The Quality Assurance source id */ @autoserialize - id: string; + id: string; /** * The date of the last udate from Notifications */ @autoserialize - lastEvent: string; + lastEvent: string; /** * The total number of suggestions provided by Notifications for this source */ @autoserialize - totalEvents: number; + totalEvents: number; /** * The type of this ConfigObject */ @excludeFromEquals @autoserialize - type: ResourceType; + type: ResourceType; /** * The links to all related resources returned by the rest api. */ @deserialize - _links: { + _links: { self: HALLink, }; } diff --git a/src/app/core/notifications/qa/models/quality-assurance-topic.model.ts b/src/app/core/notifications/qa/models/quality-assurance-topic.model.ts index ddc027d1942..0d4235a5311 100644 --- a/src/app/core/notifications/qa/models/quality-assurance-topic.model.ts +++ b/src/app/core/notifications/qa/models/quality-assurance-topic.model.ts @@ -24,38 +24,38 @@ export class QualityAssuranceTopicObject implements CacheableObject { * The Quality Assurance topic id */ @autoserialize - id: string; + id: string; /** * The Quality Assurance topic name to display */ @autoserialize - name: string; + name: string; /** * The date of the last udate from Notifications */ @autoserialize - lastEvent: string; + lastEvent: string; /** * The total number of suggestions provided by Notifications for this topic */ @autoserialize - totalEvents: number; + totalEvents: number; /** * The type of this ConfigObject */ @excludeFromEquals @autoserialize - type: ResourceType; + type: ResourceType; /** * The links to all related resources returned by the rest api. */ @deserialize - _links: { + _links: { self: HALLink, }; } diff --git a/src/app/core/notifications/models/suggestion-objects.resource-type.ts b/src/app/core/notifications/suggestions/models/suggestion-objects.resource-type.ts similarity index 76% rename from src/app/core/notifications/models/suggestion-objects.resource-type.ts rename to src/app/core/notifications/suggestions/models/suggestion-objects.resource-type.ts index 8f83d86376b..8f87027a8c5 100644 --- a/src/app/core/notifications/models/suggestion-objects.resource-type.ts +++ b/src/app/core/notifications/suggestions/models/suggestion-objects.resource-type.ts @@ -1,4 +1,4 @@ -import { ResourceType } from '../../shared/resource-type'; +import { ResourceType } from '../../../shared/resource-type'; /** * The resource type for the Suggestion object diff --git a/src/app/core/notifications/models/suggestion-source-object.resource-type.ts b/src/app/core/notifications/suggestions/models/suggestion-source-object.resource-type.ts similarity index 77% rename from src/app/core/notifications/models/suggestion-source-object.resource-type.ts rename to src/app/core/notifications/suggestions/models/suggestion-source-object.resource-type.ts index e319ed5109b..2e26fe4301c 100644 --- a/src/app/core/notifications/models/suggestion-source-object.resource-type.ts +++ b/src/app/core/notifications/suggestions/models/suggestion-source-object.resource-type.ts @@ -1,4 +1,4 @@ -import { ResourceType } from '../../shared/resource-type'; +import { ResourceType } from '../../../shared/resource-type'; /** * The resource type for the Suggestion Source object diff --git a/src/app/core/notifications/models/suggestion-source.model.ts b/src/app/core/notifications/suggestions/models/suggestion-source.model.ts similarity index 66% rename from src/app/core/notifications/models/suggestion-source.model.ts rename to src/app/core/notifications/suggestions/models/suggestion-source.model.ts index f8e7c70cf5a..0218ab4ae14 100644 --- a/src/app/core/notifications/models/suggestion-source.model.ts +++ b/src/app/core/notifications/suggestions/models/suggestion-source.model.ts @@ -3,11 +3,11 @@ import { deserialize, } from 'cerialize'; -import { typedObject } from '../../cache/builders/build-decorators'; -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 { typedObject } from '../../../cache/builders/build-decorators'; +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 { SUGGESTION_SOURCE } from './suggestion-source-object.resource-type'; /** @@ -24,26 +24,26 @@ export class SuggestionSource implements CacheableObject { * The Suggestion Target id */ @autoserialize - id: string; + id: string; /** * The total number of suggestions provided by Suggestion Target for */ @autoserialize - total: number; + total: number; /** * The type of this ConfigObject */ @excludeFromEquals @autoserialize - type: ResourceType; + type: ResourceType; /** * The links to all related resources returned by the rest api. */ @deserialize - _links: { + _links: { self: HALLink, suggestiontargets: HALLink }; diff --git a/src/app/core/notifications/models/suggestion-target-object.resource-type.ts b/src/app/core/notifications/suggestions/models/suggestion-target-object.resource-type.ts similarity index 77% rename from src/app/core/notifications/models/suggestion-target-object.resource-type.ts rename to src/app/core/notifications/suggestions/models/suggestion-target-object.resource-type.ts index 81b1b5c2619..71dd41912ac 100644 --- a/src/app/core/notifications/models/suggestion-target-object.resource-type.ts +++ b/src/app/core/notifications/suggestions/models/suggestion-target-object.resource-type.ts @@ -1,4 +1,4 @@ -import { ResourceType } from '../../shared/resource-type'; +import { ResourceType } from '../../../shared/resource-type'; /** * The resource type for the Suggestion Target object diff --git a/src/app/core/notifications/models/suggestion-target.model.ts b/src/app/core/notifications/suggestions/models/suggestion-target.model.ts similarity index 68% rename from src/app/core/notifications/models/suggestion-target.model.ts rename to src/app/core/notifications/suggestions/models/suggestion-target.model.ts index 1cbd78ae6b9..94d6b7bfa10 100644 --- a/src/app/core/notifications/models/suggestion-target.model.ts +++ b/src/app/core/notifications/suggestions/models/suggestion-target.model.ts @@ -3,11 +3,11 @@ import { deserialize, } from 'cerialize'; -import { typedObject } from '../../cache/builders/build-decorators'; -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 { typedObject } from '../../../cache/builders/build-decorators'; +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 { SUGGESTION_TARGET } from './suggestion-target-object.resource-type'; /** @@ -24,38 +24,38 @@ export class SuggestionTarget implements CacheableObject { * The Suggestion Target id */ @autoserialize - id: string; + id: string; /** * The Suggestion Target name to display */ @autoserialize - display: string; + display: string; /** * The Suggestion Target source to display */ @autoserialize - source: string; + source: string; /** * The total number of suggestions provided by Suggestion Target for */ @autoserialize - total: number; + total: number; /** * The type of this ConfigObject */ @excludeFromEquals @autoserialize - type: ResourceType; + type: ResourceType; /** * The links to all related resources returned by the rest api. */ @deserialize - _links: { + _links: { self: HALLink, suggestions: HALLink, target: HALLink diff --git a/src/app/core/notifications/models/suggestion.model.ts b/src/app/core/notifications/suggestions/models/suggestion.model.ts similarity index 71% rename from src/app/core/notifications/models/suggestion.model.ts rename to src/app/core/notifications/suggestions/models/suggestion.model.ts index a84fe9ffe28..82bc46750e6 100644 --- a/src/app/core/notifications/models/suggestion.model.ts +++ b/src/app/core/notifications/suggestions/models/suggestion.model.ts @@ -4,15 +4,15 @@ import { deserialize, } from 'cerialize'; -import { typedObject } from '../../cache/builders/build-decorators'; -import { CacheableObject } from '../../cache/cacheable-object.model'; -import { HALLink } from '../../shared/hal-link.model'; +import { typedObject } from '../../../cache/builders/build-decorators'; +import { CacheableObject } from '../../../cache/cacheable-object.model'; +import { HALLink } from '../../../shared/hal-link.model'; import { MetadataMap, MetadataMapSerializer, -} from '../../shared/metadata.models'; -import { ResourceType } from '../../shared/resource-type'; -import { excludeFromEquals } from '../../utilities/equals.decorators'; +} from '../../../shared/metadata.models'; +import { ResourceType } from '../../../shared/resource-type'; +import { excludeFromEquals } from '../../../utilities/equals.decorators'; import { SUGGESTION } from './suggestion-objects.resource-type'; /** @@ -38,57 +38,57 @@ export class Suggestion implements CacheableObject { * The Suggestion id */ @autoserialize - id: string; + id: string; /** * The Suggestion name to display */ @autoserialize - display: string; + display: string; /** * The Suggestion source to display */ @autoserialize - source: string; + source: string; /** * The Suggestion external source uri */ @autoserialize - externalSourceUri: string; + externalSourceUri: string; /** * The Total Score of the suggestion */ @autoserialize - score: string; + score: string; /** * The total number of suggestions provided by Suggestion Target for */ @autoserialize - evidences: SuggestionEvidences; + evidences: SuggestionEvidences; /** * All metadata of this suggestion object */ @excludeFromEquals @autoserializeAs(MetadataMapSerializer) - metadata: MetadataMap; + metadata: MetadataMap; /** * The type of this ConfigObject */ @excludeFromEquals @autoserialize - type: ResourceType; + type: ResourceType; /** * The links to all related resources returned by the rest api. */ @deserialize - _links: { + _links: { self: HALLink, target: HALLink }; diff --git a/src/app/core/notifications/source/suggestions-source-data.service.spec.ts b/src/app/core/notifications/suggestions/source/suggestion-source-data.service.spec.ts similarity index 75% rename from src/app/core/notifications/source/suggestions-source-data.service.spec.ts rename to src/app/core/notifications/suggestions/source/suggestion-source-data.service.spec.ts index 3026c2d8b42..b5c71589794 100644 --- a/src/app/core/notifications/source/suggestions-source-data.service.spec.ts +++ b/src/app/core/notifications/suggestions/source/suggestion-source-data.service.spec.ts @@ -7,21 +7,21 @@ import { import { of as observableOf } from 'rxjs'; import { TestScheduler } from 'rxjs/testing'; -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 { FindAllData } from '../../data/base/find-all-data'; -import { testFindAllDataImplementation } from '../../data/base/find-all-data.spec'; -import { DefaultChangeAnalyzer } from '../../data/default-change-analyzer.service'; -import { RemoteData } from '../../data/remote-data'; -import { GetRequest } from '../../data/request.models'; -import { RequestService } from '../../data/request.service'; -import { RequestEntry } from '../../data/request-entry.model'; -import { RequestEntryState } from '../../data/request-entry-state.model'; -import { HALEndpointService } from '../../shared/hal-endpoint.service'; +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 { FindAllData } from '../../../data/base/find-all-data'; +import { testFindAllDataImplementation } from '../../../data/base/find-all-data.spec'; +import { DefaultChangeAnalyzer } from '../../../data/default-change-analyzer.service'; +import { RemoteData } from '../../../data/remote-data'; +import { GetRequest } from '../../../data/request.models'; +import { RequestService } from '../../../data/request.service'; +import { RequestEntry } from '../../../data/request-entry.model'; +import { RequestEntryState } from '../../../data/request-entry-state.model'; +import { HALEndpointService } from '../../../shared/hal-endpoint.service'; import { SuggestionSource } from '../models/suggestion-source.model'; import { SuggestionSourceDataService } from './suggestion-source-data.service'; diff --git a/src/app/core/notifications/source/suggestion-source-data.service.ts b/src/app/core/notifications/suggestions/source/suggestion-source-data.service.ts similarity index 82% rename from src/app/core/notifications/source/suggestion-source-data.service.ts rename to src/app/core/notifications/suggestions/source/suggestion-source-data.service.ts index f9e21ec49dd..d90c1a8e8c4 100644 --- a/src/app/core/notifications/source/suggestion-source-data.service.ts +++ b/src/app/core/notifications/suggestions/source/suggestion-source-data.service.ts @@ -3,22 +3,22 @@ import { Injectable } from '@angular/core'; import { Store } from '@ngrx/store'; import { Observable } from 'rxjs'; -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 { CoreState } from '../../core-state.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 { ObjectCacheService } from '../../../cache/object-cache.service'; +import { CoreState } from '../../../core-state.model'; import { FindAllData, FindAllDataImpl, -} from '../../data/base/find-all-data'; -import { IdentifiableDataService } from '../../data/base/identifiable-data.service'; -import { DefaultChangeAnalyzer } from '../../data/default-change-analyzer.service'; -import { FindListOptions } from '../../data/find-list-options.model'; -import { PaginatedList } from '../../data/paginated-list.model'; -import { RemoteData } from '../../data/remote-data'; -import { RequestService } from '../../data/request.service'; -import { HALEndpointService } from '../../shared/hal-endpoint.service'; +} from '../../../data/base/find-all-data'; +import { IdentifiableDataService } from '../../../data/base/identifiable-data.service'; +import { DefaultChangeAnalyzer } from '../../../data/default-change-analyzer.service'; +import { FindListOptions } from '../../../data/find-list-options.model'; +import { PaginatedList } from '../../../data/paginated-list.model'; +import { RemoteData } from '../../../data/remote-data'; +import { RequestService } from '../../../data/request.service'; +import { HALEndpointService } from '../../../shared/hal-endpoint.service'; import { SuggestionSource } from '../models/suggestion-source.model'; /** diff --git a/src/app/core/notifications/suggestions-data.service.spec.ts b/src/app/core/notifications/suggestions/suggestion-data.service.spec.ts similarity index 76% rename from src/app/core/notifications/suggestions-data.service.spec.ts rename to src/app/core/notifications/suggestions/suggestion-data.service.spec.ts index 7d32cb3f9b9..5b3bf0b0aee 100644 --- a/src/app/core/notifications/suggestions-data.service.spec.ts +++ b/src/app/core/notifications/suggestions/suggestion-data.service.spec.ts @@ -6,22 +6,22 @@ import { import { of as observableOf } from 'rxjs'; import { TestScheduler } from 'rxjs/testing'; -import { NotificationsService } from '../../shared/notifications/notifications.service'; -import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; -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 { RestResponse } from '../cache/response.models'; -import { RemoteData } from '../data/remote-data'; -import { RequestService } from '../data/request.service'; -import { RequestEntry } from '../data/request-entry.model'; -import { RequestEntryState } from '../data/request-entry-state.model'; -import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { SuggestionsDataService } from './suggestions-data.service'; +import { NotificationsService } from '../../../shared/notifications/notifications.service'; +import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; +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 { RestResponse } from '../../cache/response.models'; +import { RemoteData } from '../../data/remote-data'; +import { RequestService } from '../../data/request.service'; +import { RequestEntry } from '../../data/request-entry.model'; +import { RequestEntryState } from '../../data/request-entry-state.model'; +import { HALEndpointService } from '../../shared/hal-endpoint.service'; +import { SuggestionDataService } from './suggestion-data.service'; describe('SuggestionDataService test', () => { let scheduler: TestScheduler; - let service: SuggestionsDataService; + let service: SuggestionDataService; let requestService: RequestService; let rdbService: RemoteDataBuildService; let objectCache: ObjectCacheService; @@ -40,7 +40,7 @@ describe('SuggestionDataService test', () => { }; function initTestService() { - return new SuggestionsDataService( + return new SuggestionDataService( requestService, rdbService, objectCache, diff --git a/src/app/core/notifications/suggestions-data.service.ts b/src/app/core/notifications/suggestions/suggestion-data.service.ts similarity index 73% rename from src/app/core/notifications/suggestions-data.service.ts rename to src/app/core/notifications/suggestions/suggestion-data.service.ts index 9d4b3da95da..328862dbaf9 100644 --- a/src/app/core/notifications/suggestions-data.service.ts +++ b/src/app/core/notifications/suggestions/suggestion-data.service.ts @@ -2,32 +2,31 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; -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 { 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 { DeleteData, DeleteDataImpl, -} from '../data/base/delete-data'; -import { IdentifiableDataService } from '../data/base/identifiable-data.service'; -import { 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 { RequestService } from '../data/request.service'; -import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { NoContent } from '../shared/NoContent.model'; +} from '../../data/base/delete-data'; +import { IdentifiableDataService } from '../../data/base/identifiable-data.service'; +import { 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 { RequestService } from '../../data/request.service'; +import { HALEndpointService } from '../../shared/hal-endpoint.service'; +import { NoContent } from '../../shared/NoContent.model'; import { Suggestion } from './models/suggestion.model'; /** * The service handling all Suggestion Target REST requests. */ @Injectable({ providedIn: 'root' }) -export class SuggestionsDataService extends IdentifiableDataService { +export class SuggestionDataService extends IdentifiableDataService { - protected searchFindBySourceMethod = 'findBySource'; protected searchFindByTargetAndSourceMethod = 'findByTargetAndSource'; private deleteData: DeleteData; diff --git a/src/app/core/notifications/target/suggestions-target-data.service.spec.ts b/src/app/core/notifications/suggestions/target/suggestion-target-data.service.spec.ts similarity index 75% rename from src/app/core/notifications/target/suggestions-target-data.service.spec.ts rename to src/app/core/notifications/suggestions/target/suggestion-target-data.service.spec.ts index 7b4ed3e82ae..6aa5aad3ea8 100644 --- a/src/app/core/notifications/target/suggestions-target-data.service.spec.ts +++ b/src/app/core/notifications/suggestions/target/suggestion-target-data.service.spec.ts @@ -7,24 +7,24 @@ import { import { of as observableOf } from 'rxjs'; import { TestScheduler } from 'rxjs/testing'; -import { NotificationsService } from '../../../shared/notifications/notifications.service'; -import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; -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 { RestResponse } from '../../cache/response.models'; -import { CoreState } from '../../core-state.model'; -import { FindAllData } from '../../data/base/find-all-data'; -import { testFindAllDataImplementation } from '../../data/base/find-all-data.spec'; -import { SearchData } from '../../data/base/search-data'; -import { testSearchDataImplementation } from '../../data/base/search-data.spec'; -import { DefaultChangeAnalyzer } from '../../data/default-change-analyzer.service'; -import { RemoteData } from '../../data/remote-data'; -import { GetRequest } from '../../data/request.models'; -import { RequestService } from '../../data/request.service'; -import { RequestEntry } from '../../data/request-entry.model'; -import { RequestEntryState } from '../../data/request-entry-state.model'; -import { HALEndpointService } from '../../shared/hal-endpoint.service'; +import { NotificationsService } from '../../../../shared/notifications/notifications.service'; +import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils'; +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 { RestResponse } from '../../../cache/response.models'; +import { CoreState } from '../../../core-state.model'; +import { FindAllData } from '../../../data/base/find-all-data'; +import { testFindAllDataImplementation } from '../../../data/base/find-all-data.spec'; +import { SearchData } from '../../../data/base/search-data'; +import { testSearchDataImplementation } from '../../../data/base/search-data.spec'; +import { DefaultChangeAnalyzer } from '../../../data/default-change-analyzer.service'; +import { RemoteData } from '../../../data/remote-data'; +import { GetRequest } from '../../../data/request.models'; +import { RequestService } from '../../../data/request.service'; +import { RequestEntry } from '../../../data/request-entry.model'; +import { RequestEntryState } from '../../../data/request-entry-state.model'; +import { HALEndpointService } from '../../../shared/hal-endpoint.service'; import { SuggestionTarget } from '../models/suggestion-target.model'; import { SuggestionTargetDataService } from './suggestion-target-data.service'; @@ -131,7 +131,7 @@ describe('SuggestionTargetDataService test', () => { }; const searchFindBySourceMethod = 'findBySource'; const expected = new GetRequest(requestService.generateRequestId(), `${endpointURL}/search/${searchFindBySourceMethod}?source=testId`); - scheduler.schedule(() => service.getTargets('testId', options).subscribe()); + scheduler.schedule(() => service.getTargetsBySource('testId', options).subscribe()); scheduler.flush(); expect(requestService.send).toHaveBeenCalledWith(expected, true); diff --git a/src/app/core/notifications/target/suggestion-target-data.service.ts b/src/app/core/notifications/suggestions/target/suggestion-target-data.service.ts similarity index 79% rename from src/app/core/notifications/target/suggestion-target-data.service.ts rename to src/app/core/notifications/suggestions/target/suggestion-target-data.service.ts index 67f8b1349ce..a83b24f0853 100644 --- a/src/app/core/notifications/target/suggestion-target-data.service.ts +++ b/src/app/core/notifications/suggestions/target/suggestion-target-data.service.ts @@ -3,27 +3,27 @@ import { Injectable } from '@angular/core'; import { Store } from '@ngrx/store'; import { Observable } from 'rxjs'; -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 { CoreState } from '../../core-state.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 { CoreState } from '../../../core-state.model'; import { FindAllData, FindAllDataImpl, -} from '../../data/base/find-all-data'; -import { IdentifiableDataService } from '../../data/base/identifiable-data.service'; +} from '../../../data/base/find-all-data'; +import { IdentifiableDataService } from '../../../data/base/identifiable-data.service'; import { SearchData, SearchDataImpl, -} from '../../data/base/search-data'; -import { DefaultChangeAnalyzer } from '../../data/default-change-analyzer.service'; -import { FindListOptions } from '../../data/find-list-options.model'; -import { PaginatedList } from '../../data/paginated-list.model'; -import { RemoteData } from '../../data/remote-data'; -import { RequestService } from '../../data/request.service'; -import { HALEndpointService } from '../../shared/hal-endpoint.service'; +} from '../../../data/base/search-data'; +import { DefaultChangeAnalyzer } from '../../../data/default-change-analyzer.service'; +import { FindListOptions } from '../../../data/find-list-options.model'; +import { PaginatedList } from '../../../data/paginated-list.model'; +import { RemoteData } from '../../../data/remote-data'; +import { RequestService } from '../../../data/request.service'; +import { HALEndpointService } from '../../../shared/hal-endpoint.service'; import { SuggestionTarget } from '../models/suggestion-target.model'; @Injectable({ providedIn: 'root' }) @@ -55,19 +55,26 @@ export class SuggestionTargetDataService extends IdentifiableDataService>> * The list of Suggestion Target. */ - public getTargets( + public getTargetsBySource( source: string, options: FindListOptions = {}, + useCachedVersionIfAvailable = true, + reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[] ): Observable>> { options.searchParams = [new RequestParam('source', source)]; - return this.searchBy(this.searchFindBySourceMethod, options, true, true, ...linksToFollow); + return this.searchBy(this.searchFindBySourceMethod, options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow); } /** diff --git a/src/app/core/orcid/model/orcid-history.model.ts b/src/app/core/orcid/model/orcid-history.model.ts index 62f5353ed26..aa8c4b41ba9 100644 --- a/src/app/core/orcid/model/orcid-history.model.ts +++ b/src/app/core/orcid/model/orcid-history.model.ts @@ -23,49 +23,49 @@ export class OrcidHistory extends CacheableObject { */ @excludeFromEquals @autoserialize - type: ResourceType; + type: ResourceType; /** * The identifier of this Orcid History record */ @autoserialize - id: number; + id: number; /** * The name of the related entity */ @autoserialize - entityName: string; + entityName: string; /** * The identifier of the profileItem of this Orcid History record. */ @autoserialize - profileItemId: string; + profileItemId: string; /** * The identifier of the entity related to this Orcid History record. */ @autoserialize - entityId: string; + entityId: string; /** * The type of the entity related to this Orcid History record. */ @autoserialize - entityType: string; + entityType: string; /** * The response status coming from ORCID api. */ @autoserialize - status: number; + status: number; /** * The putCode assigned by ORCID to the entity. */ @autoserialize - putCode: string; + putCode: string; /** * The last send attempt timestamp. @@ -86,7 +86,7 @@ export class OrcidHistory extends CacheableObject { * The {@link HALLink}s for this Orcid History record */ @deserialize - _links: { + _links: { self: HALLink, }; diff --git a/src/app/core/orcid/model/orcid-queue.model.ts b/src/app/core/orcid/model/orcid-queue.model.ts index fedb078cc56..2735114f22d 100644 --- a/src/app/core/orcid/model/orcid-queue.model.ts +++ b/src/app/core/orcid/model/orcid-queue.model.ts @@ -23,49 +23,49 @@ export class OrcidQueue extends CacheableObject { */ @excludeFromEquals @autoserialize - type: ResourceType; + type: ResourceType; /** * The identifier of this Orcid Queue record */ @autoserialize - id: number; + id: number; /** * The record description. */ @autoserialize - description: string; + description: string; /** * The identifier of the profileItem of this Orcid Queue record. */ @autoserialize - profileItemId: string; + profileItemId: string; /** * The identifier of the entity related to this Orcid Queue record. */ @autoserialize - entityId: string; + entityId: string; /** * The type of this Orcid Queue record. */ @autoserialize - recordType: string; + recordType: string; /** * The operation related to this Orcid Queue record. */ @autoserialize - operation: string; + operation: string; /** * The {@link HALLink}s for this Orcid Queue record */ @deserialize - _links: { + _links: { self: HALLink, }; diff --git a/src/app/core/profile/model/researcher-profile.model.ts b/src/app/core/profile/model/researcher-profile.model.ts index d26156123c1..28ffd09a7b8 100644 --- a/src/app/core/profile/model/researcher-profile.model.ts +++ b/src/app/core/profile/model/researcher-profile.model.ts @@ -31,28 +31,28 @@ export class ResearcherProfile extends CacheableObject { */ @excludeFromEquals @autoserialize - type: ResourceType; + type: ResourceType; /** * The identifier of this Researcher Profile */ @autoserialize - id: string; + id: string; @deserializeAs('id') - uuid: string; + uuid: string; /** * The visibility of this Researcher Profile */ @autoserialize - visible: boolean; + visible: boolean; /** * The {@link HALLink}s for this Researcher Profile */ @deserialize - _links: { + _links: { self: HALLink, item: HALLink, eperson: HALLink @@ -63,6 +63,6 @@ export class ResearcherProfile extends CacheableObject { * Will be undefined unless the item {@link HALLink} has been resolved. */ @link(ITEM) - item?: Observable>; + item?: Observable>; } diff --git a/src/app/core/provide-core.ts b/src/app/core/provide-core.ts index 5d9a4df61ee..78629f9d95a 100644 --- a/src/app/core/provide-core.ts +++ b/src/app/core/provide-core.ts @@ -16,6 +16,7 @@ import { AccessStatusObject } from '../shared/object-collection/shared/badges/ac import { IdentifierData } from '../shared/object-list/identifier-data/identifier-data.model'; import { Subscription } from '../shared/subscriptions/models/subscription.model'; import { SubmissionCoarNotifyConfig } from '../submission/sections/section-coar-notify/submission-coar-notify.config'; +import { SystemWideAlert } from '../system-wide-alert/system-wide-alert.model'; import { AuthStatus } from './auth/models/auth-status.model'; import { ShortLivedToken } from './auth/models/short-lived-token.model'; import { BulkAccessConditionOptions } from './config/models/bulk-access-condition-options.model'; @@ -30,11 +31,11 @@ import { EPerson } from './eperson/models/eperson.model'; import { Group } from './eperson/models/group.model'; import { MetadataField } from './metadata/metadata-field.model'; import { MetadataSchema } from './metadata/metadata-schema.model'; -import { SuggestionSource } from './notifications/models/suggestion-source.model'; -import { SuggestionTarget } from './notifications/models/suggestion-target.model'; import { QualityAssuranceEventObject } from './notifications/qa/models/quality-assurance-event.model'; import { QualityAssuranceSourceObject } from './notifications/qa/models/quality-assurance-source.model'; import { QualityAssuranceTopicObject } from './notifications/qa/models/quality-assurance-topic.model'; +import { SuggestionSource } from './notifications/suggestions/models/suggestion-source.model'; +import { SuggestionTarget } from './notifications/suggestions/models/suggestion-target.model'; import { OrcidHistory } from './orcid/model/orcid-history.model'; import { OrcidQueue } from './orcid/model/orcid-queue.model'; import { ResearcherProfile } from './profile/model/researcher-profile.model'; @@ -186,4 +187,5 @@ export const models = Itemfilter, SubmissionCoarNotifyConfig, NotifyRequestsStatus, + SystemWideAlert, ]; diff --git a/src/app/core/resource-policy/models/resource-policy.model.ts b/src/app/core/resource-policy/models/resource-policy.model.ts index d4558cfe3ad..669844690bc 100644 --- a/src/app/core/resource-policy/models/resource-policy.model.ts +++ b/src/app/core/resource-policy/models/resource-policy.model.ts @@ -34,50 +34,50 @@ export class ResourcePolicy implements CacheableObject { * The identifier for this Resource Policy */ @autoserialize - id: string; + id: string; /** * The name for this Resource Policy */ @autoserialize - name: string; + name: string; /** * The description for this Resource Policy */ @autoserialize - description: string; + description: string; /** * The classification or this Resource Policy */ @autoserialize - policyType: PolicyType; + policyType: PolicyType; /** * The action that is allowed by this Resource Policy */ @autoserialize - action: ActionType; + action: ActionType; /** * The first day of validity of the policy (format YYYY-MM-DD) */ @autoserialize - startDate: string; + startDate: string; /** * The last day of validity of the policy (format YYYY-MM-DD) */ @autoserialize - endDate: string; + endDate: string; /** * The object type */ @excludeFromEquals @autoserialize - type: ResourceType; + type: ResourceType; /** * The universally unique identifier for this Resource Policy @@ -91,7 +91,7 @@ export class ResourcePolicy implements CacheableObject { * The {@link HALLink}s for this ResourcePolicy */ @deserialize - _links: { + _links: { eperson: HALLink, group: HALLink, self: HALLink, @@ -102,12 +102,12 @@ export class ResourcePolicy implements CacheableObject { * Will be undefined unless the version {@link HALLink} has been resolved. */ @link(EPERSON) - eperson?: Observable>; + eperson?: Observable>; /** * The group linked by this resource policy * Will be undefined unless the version {@link HALLink} has been resolved. */ @link(GROUP) - group?: Observable>; + group?: Observable>; } diff --git a/src/app/core/rest-property/forgot-password-check-guard.guard.ts b/src/app/core/rest-property/forgot-password-check-guard.guard.ts index cc74e8039f0..49727dda116 100644 --- a/src/app/core/rest-property/forgot-password-check-guard.guard.ts +++ b/src/app/core/rest-property/forgot-password-check-guard.guard.ts @@ -1,37 +1,11 @@ -import { Injectable } from '@angular/core'; -import { - ActivatedRouteSnapshot, - Router, - RouterStateSnapshot, -} from '@angular/router'; -import { - Observable, - of, -} from 'rxjs'; +import { CanActivateFn } from '@angular/router'; +import { of as observableOf } from 'rxjs'; -import { AuthService } from '../auth/auth.service'; -import { AuthorizationDataService } from '../data/feature-authorization/authorization-data.service'; -import { SingleFeatureAuthorizationGuard } from '../data/feature-authorization/feature-authorization-guard/single-feature-authorization.guard'; +import { singleFeatureAuthorizationGuard } from '../data/feature-authorization/feature-authorization-guard/single-feature-authorization.guard'; import { FeatureID } from '../data/feature-authorization/feature-id'; -@Injectable({ - providedIn: 'root', -}) /** * Guard that checks if the forgot-password feature is enabled */ -export class ForgotPasswordCheckGuard extends SingleFeatureAuthorizationGuard { - - constructor( - protected readonly authorizationService: AuthorizationDataService, - protected readonly router: Router, - protected readonly authService: AuthService, - ) { - super(authorizationService, router, authService); - } - - getFeatureID(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { - return of(FeatureID.EPersonForgotPassword); - } - -} +export const forgotPasswordCheckGuard: CanActivateFn = + singleFeatureAuthorizationGuard(() => observableOf(FeatureID.EPersonForgotPassword)); diff --git a/src/app/core/services/link-head.service.ts b/src/app/core/services/link-head.service.ts index 78eddf31c5a..723f647108d 100644 --- a/src/app/core/services/link-head.service.ts +++ b/src/app/core/services/link-head.service.ts @@ -20,7 +20,7 @@ export class LinkHeadService { /** * Method to create a Link tag in the HEAD of the html. - * @param tag LinkDefition is the paramaters to define a link tag. + * @param tag LinkDefition is the parameters to define a link tag. * @returns Link tag that was created */ addTag(tag: LinkDefinition) { diff --git a/src/app/core/services/route.service.spec.ts b/src/app/core/services/route.service.spec.ts index 7d2b3c1169f..2f69156cbc6 100644 --- a/src/app/core/services/route.service.spec.ts +++ b/src/app/core/services/route.service.spec.ts @@ -15,9 +15,11 @@ import { hot, } from 'jasmine-marbles'; import { of as observableOf } from 'rxjs'; +import { take } from 'rxjs/operators'; import { TestScheduler } from 'rxjs/testing'; import { RouterMock } from '../../shared/mocks/router.mock'; +import { ActivatedRouteStub } from '../../shared/testing/active-router.stub'; import { AddUrlToHistoryAction } from '../history/history.actions'; import { RouteService } from './route.service'; @@ -40,6 +42,7 @@ describe('RouteService', () => { select: jasmine.createSpy('select'), }); + let route: ActivatedRouteStub; const router = new RouterMock(); router.setParams(convertToParamMap(paramObject)); @@ -47,16 +50,11 @@ describe('RouteService', () => { paramObject[paramName2] = [paramValue2a, paramValue2b]; beforeEach(waitForAsync(() => { + route = new ActivatedRouteStub(paramObject); + return TestBed.configureTestingModule({ providers: [ - { - provide: ActivatedRoute, - useValue: { - queryParams: observableOf(paramObject), - params: observableOf(paramObject), - queryParamMap: observableOf(convertToParamMap(paramObject)), - }, - }, + { provide: ActivatedRoute, useValue: route }, { provide: Router, useValue: router }, { provide: Store, useValue: store }, ], @@ -192,4 +190,51 @@ describe('RouteService', () => { }); }); }); + + describe('getParamsWithoutAppliedFilter', () => { + beforeEach(() => { + route.testParams = { + 'query': '', + 'spc.page': '1', + 'f.author': '1282121b-5394-4689-ab93-78d537764052,authority', + 'f.has_content_in_original_bundle': 'true,equals', + }; + }); + + it('should remove the parameter completely if only one value is defined', (done: DoneFn) => { + service.getParamsExceptValue('f.author', '1282121b-5394-4689-ab93-78d537764052,authority').pipe(take(1)).subscribe((params: Params) => { + expect(params).toEqual({ + 'query': '', + 'spc.page': '1', + 'f.has_content_in_original_bundle': 'true,equals', + }); + done(); + }); + }); + + it('should remove the parameter completely if only one value is defined in an array', (done: DoneFn) => { + route.testParams['f.author'] = ['1282121b-5394-4689-ab93-78d537764052,authority']; + service.getParamsExceptValue('f.author', '1282121b-5394-4689-ab93-78d537764052,authority').pipe(take(1)).subscribe((params: Params) => { + expect(params).toEqual({ + 'query': '', + 'spc.page': '1', + 'f.has_content_in_original_bundle': 'true,equals', + }); + done(); + }); + }); + + it('should return all params except the applied filter even when multiple filters of the same type are selected', (done: DoneFn) => { + route.testParams['f.author'] = ['1282121b-5394-4689-ab93-78d537764052,authority', '71b91a28-c280-4352-a199-bd7fc3312501,authority']; + service.getParamsExceptValue('f.author', '1282121b-5394-4689-ab93-78d537764052,authority').pipe(take(1)).subscribe((params: Params) => { + expect(params).toEqual({ + 'query': '', + 'spc.page': '1', + 'f.author': ['71b91a28-c280-4352-a199-bd7fc3312501,authority'], + 'f.has_content_in_original_bundle': 'true,equals', + }); + done(); + }); + }); + }); }); diff --git a/src/app/core/services/route.service.ts b/src/app/core/services/route.service.ts index 6564da03828..40a5d6bb878 100644 --- a/src/app/core/services/route.service.ts +++ b/src/app/core/services/route.service.ts @@ -249,4 +249,56 @@ export class RouteService { }, ); } + + /** + * Returns all the query parameters except for the one with the given name & value. + * + * @param name The name of the query param to exclude + * @param value The optional value that the query param needs to have to be excluded + */ + getParamsExceptValue(name: string, value?: string): Observable { + return this.route.queryParams.pipe( + map((params: Params) => { + const newParams: Params = Object.assign({}, params); + const queryParamValues: string | string[] = newParams[name]; + + if (queryParamValues === value || value === undefined) { + delete newParams[name]; + } else if (Array.isArray(queryParamValues) && queryParamValues.includes(value)) { + newParams[name] = (queryParamValues as string[]).filter((paramValue: string) => paramValue !== value); + if (newParams[name].length === 0) { + delete newParams[name]; + } + } + return newParams; + }), + ); + } + + /** + * Returns all the existing query parameters and the new value pair with the given name & value. + * + * @param name The name of the query param for which you need to add the value + * @param value The optional value that the query param needs to have in addition to the current ones + */ + getParamsWithAdditionalValue(name: string, value: string): Observable { + return this.route.queryParams.pipe( + map((params: Params) => { + const newParams: Params = Object.assign({}, params); + const queryParamValues: string | string[] = newParams[name]; + + if (queryParamValues === undefined) { + newParams[name] = value; + } else { + if (Array.isArray(queryParamValues)) { + newParams[name] = [...queryParamValues, value]; + } else { + newParams[name] = [queryParamValues, value]; + } + } + return newParams; + }), + ); + } + } diff --git a/src/app/core/services/server-xhr.service.ts b/src/app/core/services/server-xhr.service.ts index e4c50c24ea0..4b58c6b127c 100644 --- a/src/app/core/services/server-xhr.service.ts +++ b/src/app/core/services/server-xhr.service.ts @@ -33,7 +33,7 @@ const httpAgent = new HttpAgent(agentOptions); const httpsAgent = new HttpsAgent(agentOptions); /** - * Contructs the XMLHttpRequest instances used for all HttpClient requests. + * Constructs the XMLHttpRequest instances used for all HttpClient requests. * Emulated by https://github.com/pwnall/node-xhr2 on the server. * This class overrides the built-in Angular implementation to set additional configuration. * diff --git a/src/app/core/shared/authorization.model.ts b/src/app/core/shared/authorization.model.ts index 0031b054f6e..1712db9a078 100644 --- a/src/app/core/shared/authorization.model.ts +++ b/src/app/core/shared/authorization.model.ts @@ -31,10 +31,10 @@ export class Authorization extends DSpaceObject { * Unique identifier for this authorization */ @autoserialize - id: string; + id: string; @deserialize - _links: { + _links: { self: HALLink; eperson: HALLink; feature: HALLink; @@ -46,17 +46,17 @@ export class Authorization extends DSpaceObject { * Null if the authorization grants access to anonymous users */ @link(EPERSON) - eperson?: Observable>; + eperson?: Observable>; /** * The Feature enabled by this Authorization */ @link(FEATURE) - feature?: Observable>; + feature?: Observable>; /** * The Object this authorization applies to */ @link(ITEM) - object?: Observable>; + object?: Observable>; } diff --git a/src/app/core/shared/bitstream-format.model.ts b/src/app/core/shared/bitstream-format.model.ts index 354edb4cbd3..44ba85c9745 100644 --- a/src/app/core/shared/bitstream-format.model.ts +++ b/src/app/core/shared/bitstream-format.model.ts @@ -25,43 +25,43 @@ export class BitstreamFormat implements CacheableObject { */ @excludeFromEquals @autoserialize - type: ResourceType; + type: ResourceType; /** * Short description of this Bitstream Format */ @autoserialize - shortDescription: string; + shortDescription: string; /** * Description of this Bitstream Format */ @autoserialize - description: string; + description: string; /** * String representing the MIME type of this Bitstream Format */ @autoserialize - mimetype: string; + mimetype: string; /** * The level of support the system offers for this Bitstream Format */ @autoserialize - supportLevel: BitstreamFormatSupportLevel; + supportLevel: BitstreamFormatSupportLevel; /** * True if the Bitstream Format is used to store system information, rather than the content of items in the system */ @autoserialize - internal: boolean; + internal: boolean; /** * String representing this Bitstream Format's file extension */ @autoserialize - extensions: string[]; + extensions: string[]; /** * Universally unique identifier for this Bitstream Format @@ -77,13 +77,13 @@ export class BitstreamFormat implements CacheableObject { * but might not be unique across different object types */ @autoserialize - id: string; + id: string; /** * The {@link HALLink}s for this BitstreamFormat */ @deserialize - _links: { + _links: { self: HALLink; }; } diff --git a/src/app/core/shared/bitstream.model.ts b/src/app/core/shared/bitstream.model.ts index d1b821e8cd9..73e5e04b364 100644 --- a/src/app/core/shared/bitstream.model.ts +++ b/src/app/core/shared/bitstream.model.ts @@ -28,25 +28,25 @@ export class Bitstream extends DSpaceObject implements ChildHALResource { * The size of this bitstream in bytes */ @autoserialize - sizeBytes: number; + sizeBytes: number; /** * The description of this Bitstream */ @autoserialize - description: string; + description: string; /** * The name of the Bundle this Bitstream is part of */ @autoserialize - bundleName: string; + bundleName: string; /** * The {@link HALLink}s for this Bitstream */ @deserialize - _links: { + _links: { self: HALLink; bundle: HALLink; format: HALLink; @@ -59,21 +59,21 @@ export class Bitstream extends DSpaceObject implements ChildHALResource { * Will be undefined unless the thumbnail {@link HALLink} has been resolved. */ @link(BITSTREAM, false, 'thumbnail') - thumbnail?: Observable>; + thumbnail?: Observable>; /** * The BitstreamFormat of this Bitstream * Will be undefined unless the format {@link HALLink} has been resolved. */ @link(BITSTREAM_FORMAT, false, 'format') - format?: Observable>; + format?: Observable>; /** * The owning bundle for this Bitstream * Will be undefined unless the bundle{@link HALLink} has been resolved. */ @link(BUNDLE) - bundle?: Observable>; + bundle?: Observable>; getParentLinkKey(): keyof this['_links'] { return 'format'; diff --git a/src/app/core/shared/browse-definition.model.ts b/src/app/core/shared/browse-definition.model.ts index 8e50a57bb52..6de81727fae 100644 --- a/src/app/core/shared/browse-definition.model.ts +++ b/src/app/core/shared/browse-definition.model.ts @@ -9,7 +9,7 @@ import { CacheableObject } from '../cache/cacheable-object.model'; export abstract class BrowseDefinition extends CacheableObject { @autoserialize - id: string; + id: string; /** * Get the render type of the BrowseDefinition model diff --git a/src/app/core/shared/browse-entry.model.ts b/src/app/core/shared/browse-entry.model.ts index 03e3ab3f5b6..746333872e6 100644 --- a/src/app/core/shared/browse-entry.model.ts +++ b/src/app/core/shared/browse-entry.model.ts @@ -25,41 +25,41 @@ export class BrowseEntry extends ListableObject implements TypedObject { */ @excludeFromEquals @autoserialize - type: ResourceType; + type: ResourceType; /** * The authority string of this browse entry */ @autoserialize - authority: string; + authority: string; /** * The value of this browse entry */ @autoserialize - value: string; + value: string; /** * The language of the value of this browse entry */ @autoserializeAs('valueLang') - language: string; + language: string; /** * Thumbnail link used when browsing items with showThumbs config enabled. */ @autoserializeAs('thumbnail') - thumbnail: string; + thumbnail: string; /** * The count of this browse entry */ @excludeFromEquals @autoserialize - count: number; + count: number; @deserialize - _links: { + _links: { self: HALLink; entries: HALLink; thumbnail: HALLink; diff --git a/src/app/core/shared/bundle.model.ts b/src/app/core/shared/bundle.model.ts index e5b92799d4b..d9ea6d83dc9 100644 --- a/src/app/core/shared/bundle.model.ts +++ b/src/app/core/shared/bundle.model.ts @@ -27,7 +27,7 @@ export class Bundle extends DSpaceObject { * The {@link HALLink}s for this Bundle */ @deserialize - _links: { + _links: { self: HALLink; primaryBitstream: HALLink; bitstreams: HALLink; @@ -39,19 +39,19 @@ export class Bundle extends DSpaceObject { * Will be undefined unless the primaryBitstream {@link HALLink} has been resolved. */ @link(BITSTREAM) - primaryBitstream?: Observable>; + primaryBitstream?: Observable>; /** * The list of Bitstreams that are direct children of this Bundle * Will be undefined unless the bitstreams {@link HALLink} has been resolved. */ @link(BITSTREAM, true) - bitstreams?: Observable>>; + bitstreams?: Observable>>; /** * The owning item for this Bundle * Will be undefined unless the Item{@link HALLink} has been resolved. */ @link(ITEM) - item?: Observable>; + item?: Observable>; } diff --git a/src/app/core/shared/client-math.service.ts b/src/app/core/shared/client-math.service.ts index 2fde521ba9a..0630ec32461 100644 --- a/src/app/core/shared/client-math.service.ts +++ b/src/app/core/shared/client-math.service.ts @@ -31,7 +31,7 @@ export class ClientMathService extends MathService { protected mathJaxOptions = { tex: { - inlineMath: [['$', '$'], ['\\(', '\\)']], + inlineMath: [['$', '$'], ['$$', '$$'], ['\\(', '\\)']], }, svg: { fontCache: 'global', @@ -108,7 +108,7 @@ export class ClientMathService extends MathService { */ render(element: HTMLElement) { if (environment.markdown.mathjax) { - this._window.nativeWindow.MathJax.typesetPromise([element]); + return (window as any).MathJax.typesetPromise([element]) as Promise; } } } diff --git a/src/app/core/shared/collection.model.ts b/src/app/core/shared/collection.model.ts index 468b0ccc644..b929e54ccb4 100644 --- a/src/app/core/shared/collection.model.ts +++ b/src/app/core/shared/collection.model.ts @@ -33,13 +33,13 @@ export class Collection extends DSpaceObject implements ChildHALResource, Handle @excludeFromEquals @autoserialize - archivedItemsCount: number; + archivedItemsCount: number; /** * The {@link HALLink}s for this Collection */ @deserialize - _links: { + _links: { license: HALLink; harvester: HALLink; mappedItems: HALLink; @@ -60,28 +60,28 @@ export class Collection extends DSpaceObject implements ChildHALResource, Handle * Will be undefined unless the license {@link HALLink} has been resolved. */ @link(LICENSE) - license?: Observable>; + license?: Observable>; /** * The logo for this Collection * Will be undefined unless the logo {@link HALLink} has been resolved. */ @link(BITSTREAM) - logo?: Observable>; + logo?: Observable>; /** * The default access conditions for this Collection * Will be undefined unless the defaultAccessConditions {@link HALLink} has been resolved. */ @link(RESOURCE_POLICY, true) - defaultAccessConditions?: Observable>>; + defaultAccessConditions?: Observable>>; /** * The Community that is a direct parent of this Collection * Will be undefined unless the parent community HALLink has been resolved. */ @link(COMMUNITY, false) - parentCommunity?: Observable>; + parentCommunity?: Observable>; /** * A string representing the unique handle of this Collection diff --git a/src/app/core/shared/community.model.ts b/src/app/core/shared/community.model.ts index 764b15e2ed0..31b00398ffb 100644 --- a/src/app/core/shared/community.model.ts +++ b/src/app/core/shared/community.model.ts @@ -29,13 +29,13 @@ export class Community extends DSpaceObject implements ChildHALResource, HandleO @excludeFromEquals @autoserialize - archivedItemsCount: number; + archivedItemsCount: number; /** * The {@link HALLink}s for this Community */ @deserialize - _links: { + _links: { collections: HALLink; logo: HALLink; subcommunities: HALLink; @@ -49,28 +49,28 @@ export class Community extends DSpaceObject implements ChildHALResource, HandleO * Will be undefined unless the logo {@link HALLink} has been resolved. */ @link(BITSTREAM) - logo?: Observable>; + logo?: Observable>; /** * The list of Collections that are direct children of this Community * Will be undefined unless the collections {@link HALLink} has been resolved. */ @link(COLLECTION, true) - collections?: Observable>>; + collections?: Observable>>; /** * The list of Communities that are direct children of this Community * Will be undefined unless the subcommunities {@link HALLink} has been resolved. */ @link(COMMUNITY, true) - subcommunities?: Observable>>; + subcommunities?: Observable>>; /** * The Community that is a direct parent of this Community * Will be undefined unless the parent community HALLink has been resolved. */ @link(COMMUNITY, false) - parentCommunity?: Observable>; + parentCommunity?: Observable>; /** * A string representing the unique handle of this Community diff --git a/src/app/core/shared/configuration-property.model.ts b/src/app/core/shared/configuration-property.model.ts index 2b6ab606c91..e15f8a5b859 100644 --- a/src/app/core/shared/configuration-property.model.ts +++ b/src/app/core/shared/configuration-property.model.ts @@ -23,31 +23,31 @@ export class ConfigurationProperty implements CacheableObject { */ @excludeFromEquals @autoserialize - type: ResourceType; + type: ResourceType; /** * The uuid of the configuration property * The name is used as id for configuration properties */ @autoserializeAs(String, 'name') - uuid: string; + uuid: string; /** * The name of the configuration property */ @autoserialize - name: string; + name: string; /** * The values of the configuration property */ @autoserialize - values: string[]; + values: string[]; /** * The links of the configuration property */ @deserialize - _links: { self: HALLink }; + _links: { self: HALLink }; } diff --git a/src/app/core/shared/content-source.model.ts b/src/app/core/shared/content-source.model.ts index ee535191d0c..cfc78f223fc 100644 --- a/src/app/core/shared/content-source.model.ts +++ b/src/app/core/shared/content-source.model.ts @@ -38,20 +38,20 @@ export class ContentSource extends CacheableObject { * and we need a custom responseparser for ContentSource responses */ @excludeFromEquals - type: ResourceType = CONTENT_SOURCE; + type: ResourceType = CONTENT_SOURCE; /** * Unique identifier, this is necessary to store the ContentSource in FieldUpdates * Because the ContentSource coming from the REST API doesn't have a UUID, we're using the selflink */ @deserializeAs('self') - uuid: string; + uuid: string; /** * OAI Provider / Source */ @autoserializeAs('oai_source') - oaiSource: string; + oaiSource: string; /** * OAI Specific set ID @@ -64,14 +64,14 @@ export class ContentSource extends CacheableObject { * The ID of the metadata format used */ @autoserializeAs('metadata_config_id') - metadataConfigId: string; + metadataConfigId: string; /** * Type of content being harvested * Defaults to 'NONE', meaning the collection doesn't harvest its content from an external source */ @autoserializeAs('harvest_type') - harvestType = ContentSourceHarvestType.None; + harvestType = ContentSourceHarvestType.None; /** * The available metadata configurations @@ -82,31 +82,31 @@ export class ContentSource extends CacheableObject { * The current harvest status */ @autoserializeAs('harvest_status') - harvestStatus: string; + harvestStatus: string; /** * The last's harvest start time */ @autoserializeAs('harvest_start_time') - harvestStartTime: string; + harvestStartTime: string; /** * When the collection was last harvested */ @autoserializeAs('last_harvested') - lastHarvested: string; + lastHarvested: string; /** * The current harvest message */ @autoserializeAs('harvest_message') - message: string; + message: string; /** * The {@link HALLink}s for this ContentSource */ @deserialize - _links: { + _links: { self: HALLink }; } diff --git a/src/app/core/shared/dspace-object.model.ts b/src/app/core/shared/dspace-object.model.ts index dc9b7f5fca6..7bc05b1d3aa 100644 --- a/src/app/core/shared/dspace-object.model.ts +++ b/src/app/core/shared/dspace-object.model.ts @@ -46,20 +46,20 @@ export class DSpaceObject extends ListableObject implements CacheableObject { */ @excludeFromEquals @autoserializeAs(String, 'uuid') - id: string; + id: string; /** * The universally unique ide ntifier of this DSpaceObject */ @autoserializeAs(String) - uuid: string; + uuid: string; /** * A string representing the kind of DSpaceObject, e.g. community, item, … */ @excludeFromEquals @autoserialize - type: ResourceType; + type: ResourceType; /** * A shorthand to get this DSpaceObject's self link @@ -100,10 +100,10 @@ export class DSpaceObject extends ListableObject implements CacheableObject { */ @excludeFromEquals @autoserializeAs(MetadataMapSerializer) - metadata: MetadataMap; + metadata: MetadataMap; @deserialize - _links: { + _links: { self: HALLink; }; diff --git a/src/app/core/shared/external-source-entry.model.ts b/src/app/core/shared/external-source-entry.model.ts index d743a923109..01e52d30d52 100644 --- a/src/app/core/shared/external-source-entry.model.ts +++ b/src/app/core/shared/external-source-entry.model.ts @@ -27,44 +27,44 @@ export class ExternalSourceEntry extends ListableObject { * Unique identifier */ @autoserialize - id: string; + id: string; /** * The object type */ @excludeFromEquals @autoserialize - type: ResourceType; + type: ResourceType; /** * The value to display */ @autoserialize - display: string; + display: string; /** * The value to store the entry with */ @autoserialize - value: string; + value: string; /** * The ID of the external source this entry originates from */ @autoserialize - externalSource: string; + externalSource: string; /** * Metadata of the entry */ @autoserializeAs(MetadataMapSerializer) - metadata: MetadataMap; + metadata: MetadataMap; /** * The {@link HALLink}s for this ExternalSourceEntry */ @deserialize - _links: { + _links: { self: HALLink; }; diff --git a/src/app/core/shared/external-source.model.ts b/src/app/core/shared/external-source.model.ts index 70766e04940..40661917e22 100644 --- a/src/app/core/shared/external-source.model.ts +++ b/src/app/core/shared/external-source.model.ts @@ -30,38 +30,38 @@ export class ExternalSource extends CacheableObject { */ @excludeFromEquals @autoserialize - type: ResourceType; + type: ResourceType; /** * Unique identifier */ @autoserialize - id: string; + id: string; /** * The name of this external source */ @autoserialize - name: string; + name: string; /** * Is the source hierarchical? */ @autoserialize - hierarchical: boolean; + hierarchical: boolean; /** * The list of entity types that are compatible with this external source * Will be undefined unless the entityTypes {@link HALLink} has been resolved. */ @link(ITEM_TYPE, true) - entityTypes?: Observable>>; + entityTypes?: Observable>>; /** * The {@link HALLink}s for this ExternalSource */ @deserialize - _links: { + _links: { self: HALLink; entries: HALLink; entityTypes: HALLink; diff --git a/src/app/core/shared/feature.model.ts b/src/app/core/shared/feature.model.ts index 270eb2de9d9..1c18d1478f6 100644 --- a/src/app/core/shared/feature.model.ts +++ b/src/app/core/shared/feature.model.ts @@ -21,22 +21,22 @@ export class Feature extends DSpaceObject { * Unique identifier for this feature */ @autoserialize - id: string; + id: string; /** * A human readable description of the feature's purpose */ @autoserialize - description: string; + description: string; /** * A list of resource types this feature applies to */ @autoserialize - resourcetypes: string[]; + resourcetypes: string[]; @deserialize - _links: { + _links: { self: HALLink; }; } diff --git a/src/app/core/shared/flat-browse-definition.model.ts b/src/app/core/shared/flat-browse-definition.model.ts index d9d59d11142..74166f937c9 100644 --- a/src/app/core/shared/flat-browse-definition.model.ts +++ b/src/app/core/shared/flat-browse-definition.model.ts @@ -23,14 +23,14 @@ export class FlatBrowseDefinition extends NonHierarchicalBrowseDefinition { * The object type */ @excludeFromEquals - type: ResourceType = FLAT_BROWSE_DEFINITION; + type: ResourceType = FLAT_BROWSE_DEFINITION; get self(): string { return this._links.self.href; } @deserialize - _links: { + _links: { self: HALLink; items: HALLink; }; diff --git a/src/app/core/shared/hal-resource.model.ts b/src/app/core/shared/hal-resource.model.ts index 5f9d107a540..a27d448bd6f 100644 --- a/src/app/core/shared/hal-resource.model.ts +++ b/src/app/core/shared/hal-resource.model.ts @@ -13,7 +13,7 @@ export class HALResource { * The {@link HALLink}s for this {@link HALResource} */ @deserialize - _links: { + _links: { /** * The {@link HALLink} that refers to this {@link HALResource} diff --git a/src/app/core/shared/hierarchical-browse-definition.model.ts b/src/app/core/shared/hierarchical-browse-definition.model.ts index 97d50c3ca67..e7c06a5372f 100644 --- a/src/app/core/shared/hierarchical-browse-definition.model.ts +++ b/src/app/core/shared/hierarchical-browse-definition.model.ts @@ -25,23 +25,23 @@ export class HierarchicalBrowseDefinition extends BrowseDefinition { * The object type */ @excludeFromEquals - type: ResourceType = HIERARCHICAL_BROWSE_DEFINITION; + type: ResourceType = HIERARCHICAL_BROWSE_DEFINITION; @autoserialize - facetType: string; + facetType: string; @autoserialize - vocabulary: string; + vocabulary: string; @autoserializeAs('metadata') - metadataKeys: string[]; + metadataKeys: string[]; get self(): string { return this._links.self.href; } @deserialize - _links: { + _links: { self: HALLink; vocabulary: HALLink; }; diff --git a/src/app/core/shared/item-relationships/item-type.model.ts b/src/app/core/shared/item-relationships/item-type.model.ts index 1d8ca6df988..bbf1cb3be03 100644 --- a/src/app/core/shared/item-relationships/item-type.model.ts +++ b/src/app/core/shared/item-relationships/item-type.model.ts @@ -24,16 +24,16 @@ export class ItemType implements CacheableObject { */ @excludeFromEquals @autoserialize - type: ResourceType; + type: ResourceType; /** * The identifier of this ItemType */ @autoserialize - id: string; + id: string; @autoserialize - label: string; + label: string; /** * The universally unique identifier of this ItemType @@ -47,7 +47,7 @@ export class ItemType implements CacheableObject { * The {@link HALLink}s for this ItemType */ @deserialize - _links: { + _links: { self: HALLink, }; } diff --git a/src/app/core/shared/item-relationships/relationship-type.model.ts b/src/app/core/shared/item-relationships/relationship-type.model.ts index 75d5d70af6a..3a86ee34f84 100644 --- a/src/app/core/shared/item-relationships/relationship-type.model.ts +++ b/src/app/core/shared/item-relationships/relationship-type.model.ts @@ -31,19 +31,19 @@ export class RelationshipType implements CacheableObject { */ @excludeFromEquals @autoserialize - type: ResourceType; + type: ResourceType; /** * The label that describes this RelationshipType */ @autoserialize - label: string; + label: string; /** * The identifier of this RelationshipType */ @autoserialize - id: string; + id: string; /** * The universally unique identifier of this RelationshipType @@ -57,43 +57,43 @@ export class RelationshipType implements CacheableObject { * The label that describes the Relation to the left of this RelationshipType */ @autoserialize - leftwardType: string; + leftwardType: string; /** * The maximum amount of Relationships allowed to the left of this RelationshipType */ @autoserialize - leftMaxCardinality: number; + leftMaxCardinality: number; /** * The minimum amount of Relationships allowed to the left of this RelationshipType */ @autoserialize - leftMinCardinality: number; + leftMinCardinality: number; /** * The label that describes the Relation to the right of this RelationshipType */ @autoserialize - rightwardType: string; + rightwardType: string; /** * The maximum amount of Relationships allowed to the right of this RelationshipType */ @autoserialize - rightMaxCardinality: number; + rightMaxCardinality: number; /** * The minimum amount of Relationships allowed to the right of this RelationshipType */ @autoserialize - rightMinCardinality: number; + rightMinCardinality: number; /** * The {@link HALLink}s for this RelationshipType */ @deserialize - _links: { + _links: { self: HALLink; leftType: HALLink; rightType: HALLink; @@ -104,12 +104,12 @@ export class RelationshipType implements CacheableObject { * Will be undefined unless the leftType {@link HALLink} has been resolved. */ @link(ITEM_TYPE) - leftType?: Observable>; + leftType?: Observable>; /** * The type of Item found on the right side of this RelationshipType * Will be undefined unless the rightType {@link HALLink} has been resolved. */ @link(ITEM_TYPE) - rightType?: Observable>; + rightType?: Observable>; } diff --git a/src/app/core/shared/item-relationships/relationship.model.ts b/src/app/core/shared/item-relationships/relationship.model.ts index 4f6fe2796d0..77fdff8651f 100644 --- a/src/app/core/shared/item-relationships/relationship.model.ts +++ b/src/app/core/shared/item-relationships/relationship.model.ts @@ -33,7 +33,7 @@ export class Relationship implements CacheableObject { */ @excludeFromEquals @autoserialize - type: ResourceType; + type: ResourceType; /** * The universally unique identifier of this Relationship @@ -47,37 +47,37 @@ export class Relationship implements CacheableObject { * The identifier of this Relationship */ @autoserialize - id: string; + id: string; /** * The place of the Item to the left side of this Relationship */ @autoserialize - leftPlace: number; + leftPlace: number; /** * The place of the Item to the right side of this Relationship */ @autoserialize - rightPlace: number; + rightPlace: number; /** * The name variant of the Item to the left side of this Relationship */ @autoserialize - leftwardValue: string; + leftwardValue: string; /** * The name variant of the Item to the right side of this Relationship */ @autoserialize - rightwardValue: string; + rightwardValue: string; /** * The {@link HALLink}s for this Relationship */ @deserialize - _links: { + _links: { self: HALLink; leftItem: HALLink; rightItem: HALLink; @@ -89,20 +89,20 @@ export class Relationship implements CacheableObject { * Will be undefined unless the leftItem {@link HALLink} has been resolved. */ @link(ITEM) - leftItem?: Observable>; + leftItem?: Observable>; /** * The item on the right side of this relationship * Will be undefined unless the rightItem {@link HALLink} has been resolved. */ @link(ITEM) - rightItem?: Observable>; + rightItem?: Observable>; /** * The RelationshipType for this Relationship * Will be undefined unless the relationshipType {@link HALLink} has been resolved. */ @link(RELATIONSHIP_TYPE) - relationshipType?: Observable>; + relationshipType?: Observable>; } diff --git a/src/app/core/shared/item-request.model.ts b/src/app/core/shared/item-request.model.ts index 8b1ef94512e..5a4f912363b 100644 --- a/src/app/core/shared/item-request.model.ts +++ b/src/app/core/shared/item-request.model.ts @@ -22,70 +22,70 @@ export class ItemRequest implements CacheableObject { */ @excludeFromEquals @autoserialize - type: ResourceType; + type: ResourceType; /** * opaque string which uniquely identifies this request */ @autoserialize - token: string; + token: string; /** * true if the request is for all bitstreams of the item. */ @autoserialize - allfiles: boolean; + allfiles: boolean; /** * email address of the person requesting the files. */ @autoserialize - requestEmail: string; + requestEmail: string; /** * Human-readable name of the person requesting the files. */ @autoserialize - requestName: string; + requestName: string; /** * arbitrary message provided by the person requesting the files. */ @autoserialize - requestMessage: string; + requestMessage: string; /** * date that the request was recorded. */ @autoserialize - requestDate: string; + requestDate: string; /** * true if the request has been granted. */ @autoserialize - acceptRequest: boolean; + acceptRequest: boolean; /** * date that the request was granted or denied. */ @autoserialize - decisionDate: string; + decisionDate: string; /** * date on which the request is considered expired. */ @autoserialize - expires: string; + expires: string; /** * UUID of the requested Item. */ @autoserialize - itemId: string; + itemId: string; /** * UUID of the requested bitstream. */ @autoserialize - bitstreamId: string; + bitstreamId: string; /** * The {@link HALLink}s for this ItemRequest */ @deserialize - _links: { + _links: { self: HALLink; item: HALLink; bitstream: HALLink; diff --git a/src/app/core/shared/item.model.ts b/src/app/core/shared/item.model.ts index 33a9f3bb387..b13cf25f033 100644 --- a/src/app/core/shared/item.model.ts +++ b/src/app/core/shared/item.model.ts @@ -48,37 +48,37 @@ export class Item extends DSpaceObject implements ChildHALResource, HandleObject * A string representing the unique handle of this Item */ @autoserialize - handle: string; + handle: string; /** * The Date of the last modification of this Item */ @deserializeAs(Date) - lastModified: Date; + lastModified: Date; /** * A boolean representing if this Item is currently archived or not */ @autoserializeAs(Boolean, 'inArchive') - isArchived: boolean; + isArchived: boolean; /** * A boolean representing if this Item is currently discoverable or not */ @autoserializeAs(Boolean, 'discoverable') - isDiscoverable: boolean; + isDiscoverable: boolean; /** * A boolean representing if this Item is currently withdrawn or not */ @autoserializeAs(Boolean, 'withdrawn') - isWithdrawn: boolean; + isWithdrawn: boolean; /** * The {@link HALLink}s for this Item */ @deserialize - _links: { + _links: { mappedCollections: HALLink; relationships: HALLink; bundles: HALLink; @@ -96,35 +96,35 @@ export class Item extends DSpaceObject implements ChildHALResource, HandleObject * Will be undefined unless the owningCollection {@link HALLink} has been resolved. */ @link(COLLECTION) - owningCollection?: Observable>; + owningCollection?: Observable>; /** * The version this item represents in its history * Will be undefined unless the version {@link HALLink} has been resolved. */ @link(VERSION) - version?: Observable>; + version?: Observable>; /** * The list of Bundles inside this Item * Will be undefined unless the bundles {@link HALLink} has been resolved. */ @link(BUNDLE, true) - bundles?: Observable>>; + bundles?: Observable>>; /** * The list of Relationships this Item has with others * Will be undefined unless the relationships {@link HALLink} has been resolved. */ @link(RELATIONSHIP, true) - relationships?: Observable>>; + relationships?: Observable>>; /** * The thumbnail for this Item * Will be undefined unless the thumbnail {@link HALLink} has been resolved. */ @link(BITSTREAM, false, 'thumbnail') - thumbnail?: Observable>; + thumbnail?: Observable>; /** * The access status for this Item @@ -138,7 +138,7 @@ export class Item extends DSpaceObject implements ChildHALResource, HandleObject * Will be undefined unless the identifiers {@link HALLink} has been resolved. */ @link(IDENTIFIERS, false, 'identifiers') - identifiers?: Observable>; + identifiers?: Observable>; /** * Method that returns as which type of object this object should be rendered diff --git a/src/app/core/shared/license.model.ts b/src/app/core/shared/license.model.ts index af85c31eeae..0cb1e70b6b8 100644 --- a/src/app/core/shared/license.model.ts +++ b/src/app/core/shared/license.model.ts @@ -16,11 +16,11 @@ export class License extends DSpaceObject { * Is the license custom? */ @autoserialize - custom: boolean; + custom: boolean; /** * The text of the license */ @autoserialize - text: string; + text: string; } diff --git a/src/app/core/shared/math.service.spec.ts b/src/app/core/shared/math.service.spec.ts index cc0b2814f20..75d8bf1024c 100644 --- a/src/app/core/shared/math.service.spec.ts +++ b/src/app/core/shared/math.service.spec.ts @@ -22,8 +22,8 @@ export class MockMathService extends MathService { return of(true); } - render(element: HTMLElement): void { - return; + render(element: HTMLElement): Promise { + return Promise.resolve(); } } diff --git a/src/app/core/shared/math.service.ts b/src/app/core/shared/math.service.ts index c06ce06220b..909e0395643 100644 --- a/src/app/core/shared/math.service.ts +++ b/src/app/core/shared/math.service.ts @@ -15,5 +15,5 @@ export abstract class MathService { protected abstract registerMathJaxAsync(config: MathJaxConfig): Promise; abstract ready(): Observable; - abstract render(element: HTMLElement): void; + abstract render(element: HTMLElement): Promise; } diff --git a/src/app/core/shared/metadata-config.model.ts b/src/app/core/shared/metadata-config.model.ts index 861d04586ef..645d9c1dc85 100644 --- a/src/app/core/shared/metadata-config.model.ts +++ b/src/app/core/shared/metadata-config.model.ts @@ -3,7 +3,7 @@ */ export class MetadataConfig { /** - * A unique indentifier + * A unique identifier */ id: string; diff --git a/src/app/core/shared/metadata.models.ts b/src/app/core/shared/metadata.models.ts index 4ba78e39f75..15cfeb285e6 100644 --- a/src/app/core/shared/metadata.models.ts +++ b/src/app/core/shared/metadata.models.ts @@ -6,8 +6,6 @@ import { } from 'cerialize'; import { v4 as uuidv4 } from 'uuid'; -import { hasValue } from '../../shared/empty.util'; - export const VIRTUAL_METADATA_PREFIX = 'virtual::'; /** A single metadata value and its properties. */ @@ -37,45 +35,27 @@ export class MetadataValue implements MetadataValueInterface { /** The language. */ @autoserialize - language: string; + language: string; /** The string value. */ @autoserialize - value: string; + value: string; /** * The place of this MetadataValue within its list of metadata * This is used to render metadata in a specific custom order */ @autoserialize - place: number; + place: number; /** The authority key used for authority-controlled metadata */ @autoserialize - authority: string; + authority: string; /** The authority confidence value */ @autoserialize - confidence: number; - - /** - * Returns true if this Metadatum's authority key starts with 'virtual::' - */ - get isVirtual(): boolean { - return hasValue(this.authority) && this.authority.startsWith(VIRTUAL_METADATA_PREFIX); - } + confidence: number; - /** - * If this is a virtual Metadatum, it returns everything in the authority key after 'virtual::'. - * Returns undefined otherwise. - */ - get virtualValue(): string { - if (this.isVirtual) { - return this.authority.substring(this.authority.indexOf(VIRTUAL_METADATA_PREFIX) + VIRTUAL_METADATA_PREFIX.length); - } else { - return undefined; - } - } } /** Constraints for matching metadata values. */ diff --git a/src/app/core/shared/metadata.utils.ts b/src/app/core/shared/metadata.utils.ts index 1ad10356fb5..a82f218ed94 100644 --- a/src/app/core/shared/metadata.utils.ts +++ b/src/app/core/shared/metadata.utils.ts @@ -77,7 +77,7 @@ export class Metadata { /** * Gets the first matching MetadataValue object in the map(s), or `undefined`. * - * @param {MetadataMapInterface|MetadataMapInterface[]} mapOrMaps The source map(s). + * @param {MetadataMapInterface|MetadataMapInterface[]} mdMapOrMaps The source map(s). * @param {string|string[]} keyOrKeys The metadata key(s) in scope. Wildcards are supported; see above. * @param {MetadataValueFilter} filter The value filter to use. If unspecified, no filtering will be done. * @returns {MetadataValue} the first matching value, or `undefined`. @@ -98,7 +98,7 @@ export class Metadata { /** * Like [[Metadata.first]], but only returns a string value, or `undefined`. * - * @param {MetadataMapInterface|MetadataMapInterface[]} mapOrMaps The source map(s). + * @param {MetadataMapInterface|MetadataMapInterface[]} mdMapOrMaps The source map(s). * @param {string|string[]} keyOrKeys The metadata key(s) in scope. Wildcards are supported; see above. * @param {MetadataValueFilter} filter The value filter to use. If unspecified, no filtering will be done. * @returns {string} the first matching string value, or `undefined`. @@ -112,7 +112,7 @@ export class Metadata { /** * Checks for a matching metadata value in the given map(s). * - * @param {MetadataMapInterface|MetadataMapInterface[]} mapOrMaps The source map(s). + * @param {MetadataMapInterface|MetadataMapInterface[]} mdMapOrMaps The source map(s). * @param {string|string[]} keyOrKeys The metadata key(s) in scope. Wildcards are supported; see above. * @param {MetadataValueFilter} filter The value filter to use. If unspecified, no filtering will be done. * @returns {boolean} whether a match is found. @@ -163,7 +163,7 @@ export class Metadata { const outputKeys: string[] = []; for (const inputKey of inputKeys) { if (inputKey.includes('*')) { - const inputKeyRegex = new RegExp('^' + inputKey.replace(/\./g, '\\.').replace(/\*/g, '.*') + '$'); + const inputKeyRegex = new RegExp('^' + inputKey.replace(/\\/g, '\\\\').replace(/\./g, '\\.').replace(/\*/g, '.*') + '$'); for (const mapKey of Object.keys(mdMap)) { if (!outputKeys.includes(mapKey) && inputKeyRegex.test(mapKey)) { outputKeys.push(mapKey); diff --git a/src/app/core/shared/non-hierarchical-browse-definition.ts b/src/app/core/shared/non-hierarchical-browse-definition.ts index 769d70629de..7f8c6d0e5f0 100644 --- a/src/app/core/shared/non-hierarchical-browse-definition.ts +++ b/src/app/core/shared/non-hierarchical-browse-definition.ts @@ -16,14 +16,14 @@ import { SortOption } from './sort-option.model'; export abstract class NonHierarchicalBrowseDefinition extends BrowseDefinition { @autoserialize - sortOptions: SortOption[]; + sortOptions: SortOption[]; @autoserializeAs('order') - defaultSortOrder: string; + defaultSortOrder: string; @autoserializeAs('metadata') - metadataKeys: string[]; + metadataKeys: string[]; @autoserialize - dataType: BrowseByDataType; + dataType: BrowseByDataType; } diff --git a/src/app/core/shared/page-info.model.ts b/src/app/core/shared/page-info.model.ts index 5594e58309e..c9557abcb62 100644 --- a/src/app/core/shared/page-info.model.ts +++ b/src/app/core/shared/page-info.model.ts @@ -17,31 +17,31 @@ export class PageInfo implements HALResource { * The number of elements on a page */ @autoserializeAs(Number, 'size') - elementsPerPage: number; + elementsPerPage: number; /** * The total number of elements in the entire set */ @autoserialize - totalElements: number; + totalElements: number; /** * The total number of pages */ @autoserialize - totalPages: number; + totalPages: number; /** * The number of the current page, zero-based */ @autoserializeAs(Number, 'number') - currentPage: number; + currentPage: number; /** * The {@link HALLink}s for this PageInfo */ @deserialize - _links: { + _links: { first: HALLink; prev: HALLink; next: HALLink; diff --git a/src/app/core/shared/search/search-configuration.service.spec.ts b/src/app/core/shared/search/search-configuration.service.spec.ts index 7a47abbfd4e..d56ee8f756c 100644 --- a/src/app/core/shared/search/search-configuration.service.spec.ts +++ b/src/app/core/shared/search/search-configuration.service.spec.ts @@ -1,4 +1,5 @@ /* eslint-disable no-empty, @typescript-eslint/no-empty-function */ +import { Params } from '@angular/router'; import { combineLatest as observableCombineLatest, Observable, @@ -6,9 +7,11 @@ import { } from 'rxjs'; import { map } from 'rxjs/operators'; +import { environment } from '../../../../environments/environment.test'; import { getMockRequestService } from '../../../shared/mocks/request.service.mock'; import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model'; import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; +import { AppliedFilter } from '../../../shared/search/models/applied-filter.model'; import { PaginatedSearchOptions } from '../../../shared/search/models/paginated-search-options.model'; import { SearchFilter } from '../../../shared/search/models/search-filter.model'; import { SearchObjects } from '../../../shared/search/models/search-objects.model'; @@ -47,12 +50,14 @@ describe('SearchConfigurationService', () => { getQueryParameterValue: observableOf(value1), getQueryParamsWithPrefix: observableOf(prefixFilter), getRouteParameterValue: observableOf(''), + getParamsExceptValue: observableOf({}), + getParamsWithAdditionalValue: observableOf({}), }); - const paginationService = new PaginationServiceStub(); + let paginationService: PaginationServiceStub; - const activatedRoute: any = new ActivatedRouteStub(); + const activatedRoute: ActivatedRouteStub = new ActivatedRouteStub(); const linkService: any = {}; const requestService: any = getMockRequestService(); const halService: any = { @@ -78,7 +83,13 @@ describe('SearchConfigurationService', () => { }, }; beforeEach(() => { - service = new SearchConfigurationService(routeService, paginationService as any, activatedRoute, linkService, halService, requestService, rdb); + paginationService = new PaginationServiceStub(Object.assign(new PaginationComponentOptions(), { + id: defaults.pagination.id, + currentPage: 1, + pageSize: 20, + })); + + service = new SearchConfigurationService(routeService, paginationService as any, activatedRoute as any, linkService, halService, requestService, rdb, environment); }); describe('when the scope is called', () => { @@ -287,4 +298,68 @@ describe('SearchConfigurationService', () => { expect((service as any).requestService.send).toHaveBeenCalledWith(jasmine.objectContaining({ href: requestUrl }), true); }); }); + + describe('unselectAppliedFilterParams', () => { + let appliedFilter: AppliedFilter; + + beforeEach(() => { + appliedFilter = Object.assign(new AppliedFilter(), { + filter: 'author', + operator: 'authority', + value: '1282121b-5394-4689-ab93-78d537764052', + label: 'Odinson, Thor', + }); + }); + + it('should return all params except the applied filter', () => { + service.unselectAppliedFilterParams(appliedFilter.filter, appliedFilter.value, appliedFilter.operator); + + expect(routeService.getParamsExceptValue).toHaveBeenCalledWith('f.author', '1282121b-5394-4689-ab93-78d537764052,authority'); + }); + + it('should be able to remove AppliedFilter without operator', () => { + service.unselectAppliedFilterParams('dateIssued.max', '2000'); + + expect(routeService.getParamsExceptValue).toHaveBeenCalledWith('f.dateIssued.max', '2000'); + }); + + it('should reset the page to 1', (done: DoneFn) => { + service.unselectAppliedFilterParams('dateIssued.max', '2000').subscribe((params: Params) => { + expect(params[`${defaults.pagination.id}.page`]).toBe(1); + done(); + }); + }); + }); + + describe('selectNewAppliedFilterParams', () => { + let appliedFilter: AppliedFilter; + + beforeEach(() => { + appliedFilter = Object.assign(new AppliedFilter(), { + filter: 'author', + operator: 'authority', + value: '1282121b-5394-4689-ab93-78d537764052', + label: 'Odinson, Thor', + }); + }); + + it('should return all params with the applied filter', () => { + service.selectNewAppliedFilterParams(appliedFilter.filter, appliedFilter.value, appliedFilter.operator); + + expect(routeService.getParamsWithAdditionalValue).toHaveBeenCalledWith('f.author', '1282121b-5394-4689-ab93-78d537764052,authority'); + }); + + it('should be able to add AppliedFilter without operator', () => { + service.selectNewAppliedFilterParams('dateIssued.max', '2000'); + + expect(routeService.getParamsWithAdditionalValue).toHaveBeenCalledWith('f.dateIssued.max', '2000'); + }); + + it('should reset the page to 1', (done: DoneFn) => { + service.selectNewAppliedFilterParams('dateIssued.max', '2000').subscribe((params: Params) => { + expect(params[`${defaults.pagination.id}.page`]).toBe(1); + done(); + }); + }); + }); }); diff --git a/src/app/core/shared/search/search-configuration.service.ts b/src/app/core/shared/search/search-configuration.service.ts index e23551a9a16..b9e0207cc30 100644 --- a/src/app/core/shared/search/search-configuration.service.ts +++ b/src/app/core/shared/search/search-configuration.service.ts @@ -1,4 +1,5 @@ import { + Inject, Injectable, OnDestroy, } from '@angular/core'; @@ -20,6 +21,10 @@ import { take, } from 'rxjs/operators'; +import { + APP_CONFIG, + AppConfig, +} from '../../../../config/app-config.interface'; import { hasNoValue, hasValue, @@ -29,10 +34,12 @@ import { import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model'; import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; import { FacetConfigResponse } from '../../../shared/search/models/facet-config-response.model'; +import { FilterType } from '../../../shared/search/models/filter-type.model'; import { PaginatedSearchOptions } from '../../../shared/search/models/paginated-search-options.model'; import { SearchFilter } from '../../../shared/search/models/search-filter.model'; import { SearchFilterConfig } from '../../../shared/search/models/search-filter-config.model'; import { SearchOptions } from '../../../shared/search/models/search-options.model'; +import { addOperatorToFilterValue } from '../../../shared/search/search.utils'; import { LinkService } from '../../cache/builders/link.service'; import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service'; import { @@ -56,6 +63,7 @@ import { } from '../operators'; import { ViewMode } from '../view-mode.model'; import { + FilterConfig, SearchConfig, SortConfig, } from './search-filters/search-config.model'; @@ -120,24 +128,15 @@ export class SearchConfigurationService implements OnDestroy { */ protected subs: Map = new Map(null); - /** - * Initialize the search options - * @param {RouteService} routeService - * @param {PaginationService} paginationService - * @param {ActivatedRoute} route - * @param linkService - * @param halService - * @param requestService - * @param rdb - */ constructor(protected routeService: RouteService, protected paginationService: PaginationService, protected route: ActivatedRoute, protected linkService: LinkService, protected halService: HALEndpointService, protected requestService: RequestService, - protected rdb: RemoteDataBuildService) { - + protected rdb: RemoteDataBuildService, + @Inject(APP_CONFIG) protected appConfig: AppConfig, + ) { this.initDefaults(); } @@ -281,6 +280,22 @@ export class SearchConfigurationService implements OnDestroy { })); } + /** + * Return the {@link FilterConfig}s of the filters that should be displayed for the current configuration/scope + * + * @param configuration The search configuration + * @param scope The scope if exists + */ + public getConfigurationAdvancedSearchFilters(configuration: string, scope?: string): Observable { + return this.getConfigurationSearchConfig(configuration, scope).pipe( + map((searchConfiguration: SearchConfig) => { + return searchConfiguration.filters + .filter((filterConfig: FilterConfig) => this.appConfig.search.advancedFilters.filter.includes(filterConfig.filter)) + .filter((filterConfig: FilterConfig) => filterConfig.type !== FilterType.range); + }), + ); + } + setPaginationId(paginationId): void { if (isNotEmpty(paginationId)) { const currentValue: PaginatedSearchOptions = this.paginatedSearchOptions.getValue(); @@ -556,6 +571,35 @@ export class SearchConfigurationService implements OnDestroy { ); } + /** + * Calculates the {@link Params} of the search after removing a filter with a certain value and resets the page number. + * + * @param filterName The {@link AppliedFilter}'s name + * @param value The {@link AppliedFilter}'s value + * @param operator The {@link AppliedFilter}'s optional operator + */ + unselectAppliedFilterParams(filterName: string, value: string, operator?: string): Observable { + return this.routeService.getParamsExceptValue(`f.${filterName}`, hasValue(operator) ? addOperatorToFilterValue(value, operator) : value).pipe( + map((params: Params) => Object.assign(params, { + [this.paginationService.getPageParam(this.paginationID)]: 1, + })), + ); + } + + /** + * Calculates the {@link Params} of the search after adding a filter with a certain value and resets the page number. + * + * @param filterName The {@link AppliedFilter}'s name + * @param value The {@link AppliedFilter}'s value + * @param operator The {@link AppliedFilter}'s optional operator + */ + selectNewAppliedFilterParams(filterName: string, value: string, operator?: string): Observable { + return this.routeService.getParamsWithAdditionalValue(`f.${filterName}`, hasValue(operator) ? addOperatorToFilterValue(value, operator) : value).pipe( + map((params: Params) => Object.assign(params, { + [this.paginationService.getPageParam(this.paginationID)]: 1, + })), + ); + } /** * @returns {Observable} Emits the current view mode as a partial SearchOptions object diff --git a/src/app/core/shared/search/search-filter.service.spec.ts b/src/app/core/shared/search/search-filter.service.spec.ts index 47e76754af9..4a7243de953 100644 --- a/src/app/core/shared/search/search-filter.service.spec.ts +++ b/src/app/core/shared/search/search-filter.service.spec.ts @@ -1,8 +1,13 @@ +import { + TestBed, + waitForAsync, +} from '@angular/core/testing'; import { Store } from '@ngrx/store'; import { of as observableOf } from 'rxjs'; import { FilterType } from '../../../shared/search/models/filter-type.model'; import { SearchFilterConfig } from '../../../shared/search/models/search-filter-config.model'; +import { SearchOptions } from '../../../shared/search/models/search-options.model'; import { SearchFilterCollapseAction, SearchFilterDecrementPageAction, @@ -13,11 +18,14 @@ import { SearchFilterToggleAction, } from '../../../shared/search/search-filters/search-filter/search-filter.actions'; import { SearchFiltersState } from '../../../shared/search/search-filters/search-filter/search-filter.reducer'; -import { ActivatedRouteStub } from '../../../shared/testing/active-router.stub'; +import { routeServiceStub } from '../../../shared/testing/route-service.stub'; +import { SearchServiceStub } from '../../../shared/testing/search-service.stub'; import { SortDirection, SortOptions, } from '../../cache/models/sort-options.model'; +import { RouteService } from '../../services/route.service'; +import { SearchService } from './search.service'; import { SearchFilterService } from './search-filter.service'; describe('SearchFilterService', () => { @@ -32,7 +40,8 @@ describe('SearchFilterService', () => { }); const value1 = 'random value'; - // const value2 = 'another value'; + + let searchService: SearchServiceStub; const store: Store = jasmine.createSpyObj('store', { /* eslint-disable no-empty,@typescript-eslint/no-empty-function */ dispatch: {}, @@ -40,36 +49,20 @@ describe('SearchFilterService', () => { select: observableOf(true), }); - const routeServiceStub: any = { - /* eslint-disable no-empty,@typescript-eslint/no-empty-function */ - hasQueryParamWithValue: (param: string, value: string) => { - }, - hasQueryParam: (param: string) => { - }, - removeQueryParameterValue: (param: string, value: string) => { - }, - addQueryParameterValue: (param: string, value: string) => { - }, - getQueryParameterValue: (param: string) => { - }, - getQueryParameterValues: (param: string) => { - return observableOf({}); - }, - getQueryParamsWithPrefix: (param: string) => { - return observableOf({}); - }, - getRouteParameterValue: (param: string) => { - }, - /* eslint-enable no-empty, @typescript-eslint/no-empty-function */ - }; - const activatedRoute: any = new ActivatedRouteStub(); - const searchServiceStub: any = { - uiSearchRoute: '/search', - }; - - beforeEach(() => { - service = new SearchFilterService(store, routeServiceStub); - }); + beforeEach(waitForAsync(() => { + searchService = new SearchServiceStub(); + + TestBed.configureTestingModule({ + providers: [ + SearchFilterService, + { provide: SearchService, useValue: searchService }, + { provide: Store, useValue: store }, + { provide: RouteService, useValue: routeServiceStub }, + ], + }); + + service = TestBed.inject(SearchFilterService); + })); describe('when the initializeFilter method is triggered', () => { beforeEach(() => { @@ -167,17 +160,6 @@ describe('SearchFilterService', () => { }); }); - describe('when the getSelectedValuesForFilter method is called', () => { - beforeEach(() => { - spyOn(routeServiceStub, 'getQueryParameterValues'); - service.getSelectedValuesForFilter(mockFilterConfig); - }); - - it('should call getQueryParameterValues on the route service with the same parameters', () => { - expect(routeServiceStub.getQueryParameterValues).toHaveBeenCalledWith(mockFilterConfig.paramName); - }); - }); - describe('when the getCurrentScope method is called', () => { beforeEach(() => { spyOn(routeServiceStub, 'getQueryParameterValue'); @@ -273,4 +255,18 @@ describe('SearchFilterService', () => { }); }); + + describe('when findSuggestions is called with query \'test\'', () => { + const query = 'test'; + const searchOptions = new SearchOptions({}); + + beforeEach(() => { + spyOn(searchService, 'getFacetValuesFor').and.returnValue(observableOf()); + service.findSuggestions(mockFilterConfig, searchOptions, query); + }); + + it('should call getFacetValuesFor on the component\'s SearchService with the right query', () => { + expect(searchService.getFacetValuesFor).toHaveBeenCalledWith(mockFilterConfig, 1, searchOptions, query); + }); + }); }); diff --git a/src/app/core/shared/search/search-filter.service.ts b/src/app/core/shared/search/search-filter.service.ts index 20e0d3b644d..2803e71eebf 100644 --- a/src/app/core/shared/search/search-filter.service.ts +++ b/src/app/core/shared/search/search-filter.service.ts @@ -1,7 +1,4 @@ -import { - Injectable, - InjectionToken, -} from '@angular/core'; +import { Injectable } from '@angular/core'; import { Params } from '@angular/router'; import { createSelector, @@ -10,9 +7,9 @@ import { Store, } from '@ngrx/store'; import { - BehaviorSubject, combineLatest as observableCombineLatest, Observable, + of as observableOf, } from 'rxjs'; import { distinctUntilChanged, @@ -23,14 +20,22 @@ import { hasValue, isNotEmpty, } from '../../../shared/empty.util'; +import { InputSuggestion } from '../../../shared/input-suggestions/input-suggestions.model'; import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model'; +import { FacetValue } from '../../../shared/search/models/facet-value.model'; import { SearchFilterConfig } from '../../../shared/search/models/search-filter-config.model'; +import { SearchOptions } from '../../../shared/search/models/search-options.model'; +import { + getFacetValueForType, + stripOperatorFromFilterValue, +} from '../../../shared/search/search.utils'; import { SearchFilterCollapseAction, SearchFilterDecrementPageAction, SearchFilterExpandAction, SearchFilterIncrementPageAction, SearchFilterInitializeAction, + SearchFilterMinimizeAllPageAction, SearchFilterResetPageAction, SearchFilterToggleAction, } from '../../../shared/search/search-filters/search-filter/search-filter.actions'; @@ -38,27 +43,30 @@ import { SearchFiltersState, SearchFilterState, } from '../../../shared/search/search-filters/search-filter/search-filter.reducer'; +import { EmphasizePipe } from '../../../shared/utils/emphasize.pipe'; import { SortDirection, SortOptions, } from '../../cache/models/sort-options.model'; +import { PaginatedList } from '../../data/paginated-list.model'; +import { RemoteData } from '../../data/remote-data'; import { RouteService } from '../../services/route.service'; +import { getFirstSucceededRemoteData } from '../operators'; +import { SearchService } from './search.service'; const filterStateSelector = (state: SearchFiltersState) => state.searchFilter; -export const FILTER_CONFIG: InjectionToken = new InjectionToken('filterConfig'); -export const IN_PLACE_SEARCH: InjectionToken = new InjectionToken('inPlaceSearch'); -export const REFRESH_FILTER: InjectionToken> = new InjectionToken('refreshFilters'); -export const SCOPE: InjectionToken = new InjectionToken('scope'); - /** * Service that performs all actions that have to do with search filters and facets */ @Injectable({ providedIn: 'root' }) export class SearchFilterService { - constructor(private store: Store, - private routeService: RouteService) { + constructor( + protected searchService: SearchService, + protected store: Store, + protected routeService: RouteService, + ) { } /** @@ -84,7 +92,7 @@ export class SearchFilterService { * Fetch the current active scope from the query parameters * @returns {Observable} */ - getCurrentScope() { + getCurrentScope(): Observable { return this.routeService.getQueryParameterValue('scope'); } @@ -92,7 +100,7 @@ export class SearchFilterService { * Fetch the current query from the query parameters * @returns {Observable} */ - getCurrentQuery() { + getCurrentQuery(): Observable { return this.routeService.getQueryParameterValue('query'); } @@ -134,7 +142,7 @@ export class SearchFilterService { * Fetch the current active filters from the query parameters * @returns {Observable} */ - getCurrentFilters() { + getCurrentFilters(): Observable { return this.routeService.getQueryParamsWithPrefix('f.'); } @@ -142,29 +150,44 @@ export class SearchFilterService { * Fetch the current view from the query parameters * @returns {Observable} */ - getCurrentView() { + getCurrentView(): Observable { return this.routeService.getQueryParameterValue('view'); } /** - * Requests the active filter values set for a given filter - * @param {SearchFilterConfig} filterConfig The configuration for which the filters are active - * @returns {Observable} Emits the active filters for the given filter configuration + * Updates the found facet value suggestions for a given query + * Transforms the found values into display values + * + * @param searchFilterConfig The search filter config + * @param searchOptions The search options + * @param query The query for which is being searched */ - getSelectedValuesForFilter(filterConfig: SearchFilterConfig): Observable { - const values$ = this.routeService.getQueryParameterValues(filterConfig.paramName); - const prefixValues$ = this.routeService.getQueryParamsWithPrefix(filterConfig.paramName + '.').pipe( - map((params: Params) => [].concat(...Object.values(params))), - ); - return observableCombineLatest(values$, prefixValues$).pipe( - map(([values, prefixValues]) => { - if (isNotEmpty(values)) { - return values; - } - return prefixValues; - }, - ), - ); + findSuggestions(searchFilterConfig: SearchFilterConfig, searchOptions: SearchOptions, query: string): Observable { + if (isNotEmpty(query)) { + return this.searchService.getFacetValuesFor(searchFilterConfig, 1, searchOptions, query.toLowerCase()).pipe( + getFirstSucceededRemoteData(), + map((rd: RemoteData>) => rd.payload.page.map((facet) => { + return { + displayValue: this.getDisplayValue(facet, query), + query: getFacetValueForType(facet, searchFilterConfig), + value: stripOperatorFromFilterValue(getFacetValueForType(facet, searchFilterConfig)), + }; + })), + ); + } else { + return observableOf([]); + } + } + + /** + * Transforms the facet value string, so if the query matches part of the value, it's emphasized in the value + * + * @param facet The value of the facet as returned by the server + * @param query The query that was used to search facet values + * @returns {string} The facet value with the query part emphasized + */ + getDisplayValue(facet: FacetValue, query: string): string { + return `${new EmphasizePipe().transform(facet.value, query)} (${facet.count})`; } /** @@ -177,7 +200,7 @@ export class SearchFilterService { select(filterByNameSelector(filterName)), map((object: SearchFilterState) => { if (object) { - return object.filterCollapsed; + return object.filterCollapsed || object.minimized; } else { return false; } @@ -259,6 +282,10 @@ export class SearchFilterService { public resetPage(filterName: string): void { this.store.dispatch(new SearchFilterResetPageAction(filterName)); } + + public minimizeAll(): void { + this.store.dispatch(new SearchFilterMinimizeAllPageAction()); + } } function filterByNameSelector(name: string): MemoizedSelector { diff --git a/src/app/core/shared/search/search-filters/search-config.model.ts b/src/app/core/shared/search/search-filters/search-config.model.ts index ccc9827e599..2ebe5e84555 100644 --- a/src/app/core/shared/search/search-filters/search-config.model.ts +++ b/src/app/core/shared/search/search-filters/search-config.model.ts @@ -3,6 +3,7 @@ import { deserialize, } from 'cerialize'; +import { FilterType } from '../../../../shared/search/models/filter-type.model'; import { typedObject } from '../../../cache/builders/build-decorators'; import { CacheableObject } from '../../../cache/cacheable-object.model'; import { HALLink } from '../../hal-link.model'; @@ -20,31 +21,31 @@ export class SearchConfig implements CacheableObject { * The id of this search configuration. */ @autoserialize - id: string; + id: string; /** * The configured filters. */ @autoserialize - filters: FilterConfig[]; + filters: FilterConfig[]; /** * The configured sort options. */ @autoserialize - sortOptions: SortConfig[]; + sortOptions: SortConfig[]; /** * The object type. */ @autoserialize - type: ResourceType; + type: ResourceType; /** * The {@link HALLink}s for this Item */ @deserialize - _links: { + _links: { facets: HALLink; objects: HALLink; self: HALLink; @@ -60,7 +61,7 @@ export interface FilterConfig { operators: OperatorConfig[]; openByDefault: boolean; pageSize: number; - type: string; + type: FilterType; } /** diff --git a/src/app/core/shared/search/search.service.ts b/src/app/core/shared/search/search.service.ts index 528c3bbe230..60a2da6af1e 100644 --- a/src/app/core/shared/search/search.service.ts +++ b/src/app/core/shared/search/search.service.ts @@ -1,17 +1,17 @@ /* eslint-disable max-classes-per-file */ -import { - Injectable, - OnDestroy, -} from '@angular/core'; +import { Injectable } from '@angular/core'; import { Angulartics2 } from 'angulartics2'; import { + BehaviorSubject, combineLatest as observableCombineLatest, Observable, } from 'rxjs'; import { + distinctUntilChanged, map, switchMap, take, + tap, } from 'rxjs/operators'; import { @@ -21,6 +21,7 @@ import { } from '../../../shared/empty.util'; import { ListableObject } from '../../../shared/object-collection/shared/listable-object.model'; import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model'; +import { AppliedFilter } from '../../../shared/search/models/applied-filter.model'; import { FacetValues } from '../../../shared/search/models/facet-values.model'; import { PaginatedSearchOptions } from '../../../shared/search/models/paginated-search-options.model'; import { SearchFilterConfig } from '../../../shared/search/models/search-filter-config.model'; @@ -56,7 +57,7 @@ import { SearchConfigurationService } from './search-configuration.service'; * - Overrides {@link BaseDataService.addEmbedParams} in order to make it public * * Doesn't use any of the service's dependencies, they are initialized as undefined - * Therefore, equest/response handling methods won't work even though they're defined + * Therefore, request/response handling methods won't work even though they're defined */ class SearchDataService extends BaseDataService { constructor() { @@ -78,7 +79,7 @@ class SearchDataService extends BaseDataService { * Service that performs all general actions that have to do with the search page */ @Injectable({ providedIn: 'root' }) -export class SearchService implements OnDestroy { +export class SearchService { /** * Endpoint link path for retrieving general search results @@ -95,16 +96,13 @@ export class SearchService implements OnDestroy { */ private request: GenericConstructor = GetRequest; - /** - * Subscription to unsubscribe from - */ - private sub; - /** * Instance of SearchDataService to forward data service methods to */ private searchDataService: SearchDataService; + public appliedFilters$: BehaviorSubject = new BehaviorSubject([]); + constructor( private routeService: RouteService, protected requestService: RequestService, @@ -118,6 +116,18 @@ export class SearchService implements OnDestroy { this.searchDataService = new SearchDataService(); } + /** + * Get the currently {@link AppliedFilter}s for the given filter. + * + * @param filterName The name of the filter + */ + getSelectedValuesForFilter(filterName: string): Observable { + return this.appliedFilters$.pipe( + map((appliedFilters: AppliedFilter[]) => appliedFilters.filter((appliedFilter: AppliedFilter) => appliedFilter.filter === filterName)), + distinctUntilChanged((previous: AppliedFilter[], next: AppliedFilter[]) => JSON.stringify(previous) === JSON.stringify(next)), + ); + } + /** * Method to set service options * @param {GenericConstructor} parser The ResponseParsingService constructor name @@ -191,26 +201,11 @@ export class SearchService implements OnDestroy { return this.directlyAttachIndexableObjects(sqr$, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow); } - /** - * Method to retrieve request entries for search results from the server - * @param {PaginatedSearchOptions} searchOptions The configuration necessary to perform this search - * @returns {Observable>>} Emits a paginated list with all search results found - */ - searchEntries(searchOptions?: PaginatedSearchOptions): Observable>> { - const href$ = this.getEndpoint(searchOptions); - - const sqr$ = href$.pipe( - switchMap((href: string) => this.rdb.buildFromHref>(href)), - ); - - return this.directlyAttachIndexableObjects(sqr$); - } - /** * Method to directly attach the indexableObjects to search results, instead of using RemoteData. * For compatibility with the way the search was written originally * - * @param sqr$: a SearchObjects RemotaData Observable without its + * @param sqr$ A {@link SearchObjects} {@link RemoteData} Observable without its * indexableObjects attached * @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's * no valid cached version. Defaults to true @@ -311,7 +306,19 @@ export class SearchService implements OnDestroy { }); this.requestService.send(request, useCachedVersionIfAvailable); - return this.rdb.buildFromHref(href); + return this.rdb.buildFromHref(href).pipe( + tap((facetValuesRD: RemoteData) => { + if (facetValuesRD.hasSucceeded) { + const appliedFilters: AppliedFilter[] = (facetValuesRD.payload.appliedFilters ?? []) + .filter((appliedFilter: AppliedFilter) => hasValue(appliedFilter)) + // TODO this should ideally be fixed in the backend + .map((appliedFilter: AppliedFilter) => Object.assign({}, appliedFilter, { + operator: hasValue(appliedFilter.value.match(/\[\s*(\*|\d+)\s*TO\s*(\*|\d+)\s*]/)) ? 'range' : appliedFilter.operator, + })); + this.appliedFilters$.next(appliedFilters); + } + }), + ); } /** @@ -387,12 +394,4 @@ export class SearchService implements OnDestroy { return '/search'; } - /** - * Unsubscribe from the subscription - */ - ngOnDestroy(): void { - if (this.sub !== undefined) { - this.sub.unsubscribe(); - } - } } diff --git a/src/app/core/shared/server-math.service.ts b/src/app/core/shared/server-math.service.ts index 75fa775feeb..62070dbafc0 100644 --- a/src/app/core/shared/server-math.service.ts +++ b/src/app/core/shared/server-math.service.ts @@ -47,6 +47,6 @@ export class ServerMathService extends MathService { } render(element: HTMLElement) { - return; + return Promise.resolve(); } } diff --git a/src/app/core/shared/sort-option.model.ts b/src/app/core/shared/sort-option.model.ts index b179d159b68..c735e87b9af 100644 --- a/src/app/core/shared/sort-option.model.ts +++ b/src/app/core/shared/sort-option.model.ts @@ -2,8 +2,8 @@ import { autoserialize } from 'cerialize'; export class SortOption { @autoserialize - name: string; + name: string; @autoserialize - metadata: string; + metadata: string; } diff --git a/src/app/core/shared/template-item.model.ts b/src/app/core/shared/template-item.model.ts index 4c33a2bd92f..a536dea49e9 100644 --- a/src/app/core/shared/template-item.model.ts +++ b/src/app/core/shared/template-item.model.ts @@ -23,6 +23,6 @@ export class TemplateItem extends Item { * The Collection that this item is a template for */ @link(COLLECTION) - templateItemOf: Observable>; + templateItemOf: Observable>; } diff --git a/src/app/core/shared/value-list-browse-definition.model.ts b/src/app/core/shared/value-list-browse-definition.model.ts index 80ad73e7612..ad9fc60d1cb 100644 --- a/src/app/core/shared/value-list-browse-definition.model.ts +++ b/src/app/core/shared/value-list-browse-definition.model.ts @@ -23,14 +23,14 @@ export class ValueListBrowseDefinition extends NonHierarchicalBrowseDefinition { * The object type */ @excludeFromEquals - type: ResourceType = VALUE_LIST_BROWSE_DEFINITION; + type: ResourceType = VALUE_LIST_BROWSE_DEFINITION; get self(): string { return this._links.self.href; } @deserialize - _links: { + _links: { self: HALLink; entries: HALLink; }; diff --git a/src/app/core/shared/version-history.model.ts b/src/app/core/shared/version-history.model.ts index 9792a20ff31..58d9259a047 100644 --- a/src/app/core/shared/version-history.model.ts +++ b/src/app/core/shared/version-history.model.ts @@ -27,7 +27,7 @@ export class VersionHistory extends DSpaceObject { static type = VERSION_HISTORY; @deserialize - _links: { + _links: { self: HALLink; versions: HALLink; draftVersion: HALLink; @@ -37,30 +37,30 @@ export class VersionHistory extends DSpaceObject { * The identifier of this Version History */ @autoserialize - id: string; + id: string; /** * The summary of this Version History */ @autoserialize - summary: string; + summary: string; /** * The name of the submitter of this Version History */ @autoserialize - submitterName: string; + submitterName: string; /** * Whether exist a workspace item */ @autoserialize - draftVersion: boolean; + draftVersion: boolean; /** * The list of versions within this history */ @excludeFromEquals @link(VERSION, true) - versions: Observable>>; + versions: Observable>>; } diff --git a/src/app/core/shared/version.model.ts b/src/app/core/shared/version.model.ts index f66bc8938bb..105ba744167 100644 --- a/src/app/core/shared/version.model.ts +++ b/src/app/core/shared/version.model.ts @@ -30,7 +30,7 @@ export class Version extends DSpaceObject { static type = VERSION; @deserialize - _links: { + _links: { self: HALLink; item: HALLink; versionhistory: HALLink; @@ -41,50 +41,50 @@ export class Version extends DSpaceObject { * The identifier of this Version */ @autoserialize - id: string; + id: string; /** * The version number of the version's history this version represents */ @autoserialize - version: number; + version: number; /** * The summary for the changes made in this version */ @autoserialize - summary: string; + summary: string; /** * The name of the submitter of this version */ @autoserialize - submitterName: string; + submitterName: string; /** * The Date this version was created */ @deserialize - created: Date; + created: Date; /** * The full version history this version is apart of */ @excludeFromEquals @link(VERSION_HISTORY) - versionhistory: Observable>; + versionhistory: Observable>; /** * The item this version represents */ @excludeFromEquals @link(ITEM) - item: Observable>; + item: Observable>; /** * The e-person who created this version */ @excludeFromEquals @link(EPERSON) - eperson: Observable>; + eperson: Observable>; } diff --git a/src/app/core/statistics/models/usage-report.model.ts b/src/app/core/statistics/models/usage-report.model.ts index b8a415667dd..a4924f3a3db 100644 --- a/src/app/core/statistics/models/usage-report.model.ts +++ b/src/app/core/statistics/models/usage-report.model.ts @@ -26,19 +26,19 @@ export class UsageReport extends HALResource { */ @excludeFromEquals @autoserialize - type: ResourceType; + type: ResourceType; @autoserialize - id: string; + id: string; @autoserializeAs('report-type') reportType: string; @autoserialize - points: Point[]; + points: Point[]; @deserialize - _links: { + _links: { self: HALLink; }; } diff --git a/src/app/core/statistics/usage-report-data.service.ts b/src/app/core/statistics/usage-report-data.service.ts index 457b33f4ecb..2737e875de9 100644 --- a/src/app/core/statistics/usage-report-data.service.ts +++ b/src/app/core/statistics/usage-report-data.service.ts @@ -4,6 +4,7 @@ import { map } from 'rxjs/operators'; 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 { IdentifiableDataService } from '../data/base/identifiable-data.service'; import { @@ -49,10 +50,7 @@ export class UsageReportDataService extends IdentifiableDataService searchStatistics(uri: string, page: number, size: number): Observable { return this.searchBy('object', { searchParams: [ - { - fieldName: `uri`, - fieldValue: uri, - }, + new RequestParam('uri', uri), ], currentPage: page, elementsPerPage: size, diff --git a/src/app/core/submission/correctiontype-data.service.ts b/src/app/core/submission/correctiontype-data.service.ts index 9fb771288f5..550fdcacbac 100644 --- a/src/app/core/submission/correctiontype-data.service.ts +++ b/src/app/core/submission/correctiontype-data.service.ts @@ -76,10 +76,7 @@ export class CorrectionTypeDataService extends IdentifiableDataService { const options = new FindListOptions(); options.searchParams = [ - { - fieldName: 'topic', - fieldValue: topic, - }, + new RequestParam('topic', topic), ]; return this.searchData.searchBy(this.searchByTopic, options, useCachedVersionIfAvailable, reRequestOnStale).pipe( diff --git a/src/app/core/submission/models/correctiontype.model.ts b/src/app/core/submission/models/correctiontype.model.ts index 0b9dd755457..4b5bd1ddd68 100644 --- a/src/app/core/submission/models/correctiontype.model.ts +++ b/src/app/core/submission/models/correctiontype.model.ts @@ -22,7 +22,7 @@ export class CorrectionType extends CacheableObject { */ @excludeFromEquals @autoserialize - type: ResourceType; + type: ResourceType; @autoserialize /** * The unique identifier for the correction type mode. diff --git a/src/app/core/submission/models/submission-cc-license-url.model.ts b/src/app/core/submission/models/submission-cc-license-url.model.ts index 85fa1d0896e..b1e53aa206e 100644 --- a/src/app/core/submission/models/submission-cc-license-url.model.ts +++ b/src/app/core/submission/models/submission-cc-license-url.model.ts @@ -20,8 +20,8 @@ export class SubmissionCcLicenceUrl extends HALResource { */ @excludeFromEquals @autoserialize - type: ResourceType; + type: ResourceType; @autoserialize - url: string; + url: string; } diff --git a/src/app/core/submission/models/submission-cc-license.model.ts b/src/app/core/submission/models/submission-cc-license.model.ts index c730aa44fc0..8ab235941fc 100644 --- a/src/app/core/submission/models/submission-cc-license.model.ts +++ b/src/app/core/submission/models/submission-cc-license.model.ts @@ -20,16 +20,16 @@ export class SubmissionCcLicence extends HALResource { */ @excludeFromEquals @autoserialize - type: ResourceType; + type: ResourceType; @autoserialize - id: string; + id: string; @autoserialize - name: string; + name: string; @autoserialize - fields: Field[]; + fields: Field[]; } export interface Field { diff --git a/src/app/core/submission/models/submission-object.model.ts b/src/app/core/submission/models/submission-object.model.ts index 882fafb4674..d4836f6dcf1 100644 --- a/src/app/core/submission/models/submission-object.model.ts +++ b/src/app/core/submission/models/submission-object.model.ts @@ -35,38 +35,38 @@ export abstract class SubmissionObject extends DSpaceObject implements Cacheable @excludeFromEquals @autoserialize - id: string; + id: string; /** * The SubmissionObject last modified date */ @autoserialize - lastModified: Date; + lastModified: Date; /** * The collection this submission applies to * Will be undefined unless the collection {@link HALLink} has been resolved. */ @link(COLLECTION) - collection?: Observable> | Collection; + collection?: Observable> | Collection; /** * The SubmissionObject's last section's data */ @autoserialize - sections: WorkspaceitemSectionsObject; + sections: WorkspaceitemSectionsObject; /** * The SubmissionObject's last section's errors */ @autoserialize - errors: SubmissionObjectError[]; + errors: SubmissionObjectError[]; /** * The {@link HALLink}s for this SubmissionObject */ @deserialize - _links: { + _links: { self: HALLink; collection: HALLink; item: HALLink; @@ -92,14 +92,14 @@ export abstract class SubmissionObject extends DSpaceObject implements Cacheable * Will be undefined unless the submissionDefinition {@link HALLink} has been resolved. */ @link(SubmissionDefinitionsModel.type) - submissionDefinition?: Observable> | SubmissionDefinitionsModel; + submissionDefinition?: Observable> | SubmissionDefinitionsModel; /** * The submitter for this SubmissionObject * Will be undefined unless the submitter {@link HALLink} has been resolved. */ @link(EPERSON) - submitter?: Observable> | EPerson; + submitter?: Observable> | EPerson; /** * The submission supervision order diff --git a/src/app/core/submission/submission-cc-license-url-data.service.ts b/src/app/core/submission/submission-cc-license-url-data.service.ts index 94c3b39365f..575f6113353 100644 --- a/src/app/core/submission/submission-cc-license-url-data.service.ts +++ b/src/app/core/submission/submission-cc-license-url-data.service.ts @@ -7,6 +7,7 @@ import { 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 { BaseDataService } from '../data/base/base-data.service'; import { @@ -54,17 +55,8 @@ export class SubmissionCcLicenseUrlDataService extends BaseDataService { - return { - fieldName: `answer_${field.id}`, - fieldValue: options.get(field).id, - }; - }), + new RequestParam('license', ccLicense.id), + ...ccLicense.fields.map((field: Field) => new RequestParam(`answer_${field.id}`, options.get(field).id)), ], }, ).pipe( diff --git a/src/app/core/submission/submission-duplicate-data.service.ts b/src/app/core/submission/submission-duplicate-data.service.ts index 755fec05c22..b3273424690 100644 --- a/src/app/core/submission/submission-duplicate-data.service.ts +++ b/src/app/core/submission/submission-duplicate-data.service.ts @@ -1,5 +1,8 @@ /* eslint-disable max-classes-per-file */ -import { Injectable } from '@angular/core'; +import { + Injectable, + OnDestroy, +} from '@angular/core'; import { Observable } from 'rxjs'; import { Duplicate } from '../../shared/object-list/duplicate-data/duplicate.model'; @@ -33,7 +36,8 @@ import { HALEndpointService } from '../shared/hal-endpoint.service'; * */ @Injectable({ providedIn: 'root' }) -export class SubmissionDuplicateDataService extends BaseDataService implements SearchData { +export class SubmissionDuplicateDataService extends BaseDataService + implements SearchData, OnDestroy { /** * The ResponseParsingService constructor name diff --git a/src/app/core/submission/submission-json-patch-operations.service.spec.ts b/src/app/core/submission/submission-json-patch-operations.service.spec.ts index 76e487400b5..dd98b5de570 100644 --- a/src/app/core/submission/submission-json-patch-operations.service.spec.ts +++ b/src/app/core/submission/submission-json-patch-operations.service.spec.ts @@ -17,6 +17,9 @@ describe('SubmissionJsonPatchOperationsService', () => { const rdbService = {} as RemoteDataBuildService; const halEndpointService = {} as HALEndpointService; + const uuid = '91ecbeda-99fe-42ac-9430-b9b75af56f78'; + const href = 'https://rest.api/some/self/link?with=maybe&a=few&other=parameters'; + function initTestService() { return new SubmissionJsonPatchOperationsService( requestService, @@ -36,4 +39,16 @@ describe('SubmissionJsonPatchOperationsService', () => { expect((service as any).patchRequestConstructor).toEqual(SubmissionPatchRequest); }); + describe(`getRequestInstance`, () => { + it(`should add a parameter to embed the item to the request URL`, () => { + const result = (service as any).getRequestInstance(uuid, href); + const resultURL = new URL(result.href); + expect(resultURL.searchParams.get('embed')).toEqual('item'); + + // if we delete the embed item param, it should be identical to the original url + resultURL.searchParams.delete('embed', 'item'); + expect(href).toEqual(resultURL.toString()); + }); + }); + }); diff --git a/src/app/core/submission/submission-json-patch-operations.service.ts b/src/app/core/submission/submission-json-patch-operations.service.ts index 16d74b2bf86..8d454ae2112 100644 --- a/src/app/core/submission/submission-json-patch-operations.service.ts +++ b/src/app/core/submission/submission-json-patch-operations.service.ts @@ -8,6 +8,7 @@ import { RequestService } from '../data/request.service'; import { JsonPatchOperationsService } from '../json-patch/json-patch-operations.service'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { SubmitDataResponseDefinitionObject } from '../shared/submit-data-response-definition.model'; +import { URLCombiner } from '../url-combiner/url-combiner'; /** * A service that provides methods to make JSON Patch requests. @@ -26,4 +27,20 @@ export class SubmissionJsonPatchOperationsService extends JsonPatchOperationsSer super(); } + /** + * Return an instance for RestRequest class + * + * @param uuid + * The request uuid + * @param href + * The request href + * @param body + * The request body + * @return Object + * instance of PatchRequestDefinition + */ + protected getRequestInstance(uuid: string, href: string, body?: any): SubmissionPatchRequest { + return new this.patchRequestConstructor(uuid, new URLCombiner(href, '?embed=item').toString(), body); + } + } diff --git a/src/app/core/submission/submission-object-data.service.ts b/src/app/core/submission/submission-object-data.service.ts index 4cf45fa84b9..c044cb8129d 100644 --- a/src/app/core/submission/submission-object-data.service.ts +++ b/src/app/core/submission/submission-object-data.service.ts @@ -68,7 +68,7 @@ export class SubmissionObjectDataService { environment.cache.msToLive.default, now, RequestEntryState.Error, - 'The request couldn\'t be sent. Unable to determine the type of submission object', + 'The request could not be sent. Unable to determine the type of submission object', undefined, 400, )); diff --git a/src/app/core/submission/submission-parent-breadcrumb.service.ts b/src/app/core/submission/submission-parent-breadcrumb.service.ts index 4241d001926..2f9051b8581 100644 --- a/src/app/core/submission/submission-parent-breadcrumb.service.ts +++ b/src/app/core/submission/submission-parent-breadcrumb.service.ts @@ -8,7 +8,10 @@ import { import { getDSORoute } from '../../app-routing-paths'; import { Breadcrumb } from '../../breadcrumbs/breadcrumb/breadcrumb.model'; -import { hasValue } from '../../shared/empty.util'; +import { + hasValue, + isEmpty, +} from '../../shared/empty.util'; import { SubmissionService } from '../../submission/submission.service'; import { BreadcrumbsProviderService } from '../breadcrumbs/breadcrumbsProviderService'; import { DSOBreadcrumbsService } from '../breadcrumbs/dso-breadcrumbs.service'; @@ -46,6 +49,10 @@ export class SubmissionParentBreadcrumbsService implements BreadcrumbsProviderSe * @param submissionObject The {@link SubmissionObject} for which the parent breadcrumb structure needs to be created */ getBreadcrumbs(submissionObject: SubmissionObject): Observable { + if (isEmpty(submissionObject)) { + return observableOf([]); + } + return combineLatest([ (submissionObject.collection as Observable>).pipe( getFirstCompletedRemoteData(), diff --git a/src/app/core/submission/submission-rest.service.spec.ts b/src/app/core/submission/submission-rest.service.spec.ts index 5512e683cde..ee6a157b82e 100644 --- a/src/app/core/submission/submission-rest.service.spec.ts +++ b/src/app/core/submission/submission-rest.service.spec.ts @@ -26,7 +26,7 @@ describe('SubmissionRestService test suite', () => { const resourceEndpoint = 'workspaceitems'; const resourceScope = '260'; const body = { test: new FormFieldMetadataValueObject('test') }; - const resourceHref = resourceEndpointURL + '/' + resourceEndpoint + '/' + resourceScope + '?projection=full'; + const resourceHref = resourceEndpointURL + '/' + resourceEndpoint + '/' + resourceScope + '?embed=item,sections,collection'; const timestampResponse = 1545994811992; function initTestService() { diff --git a/src/app/core/submission/submission-rest.service.ts b/src/app/core/submission/submission-rest.service.ts index 0dd59962a73..b1f4ad9e1b6 100644 --- a/src/app/core/submission/submission-rest.service.ts +++ b/src/app/core/submission/submission-rest.service.ts @@ -13,6 +13,7 @@ import { isNotEmpty, } from '../../shared/empty.util'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { ErrorResponse } from '../cache/response.models'; import { RemoteData } from '../data/remote-data'; import { DeleteRequest, @@ -23,6 +24,7 @@ import { SubmissionRequest, } from '../data/request.models'; import { RequestService } from '../data/request.service'; +import { RequestError } from '../data/request-error.model'; import { RestRequest } from '../data/rest-request.model'; import { HttpOptions } from '../dspace-rest/dspace-rest.service'; import { HALEndpointService } from '../shared/hal-endpoint.service'; @@ -57,7 +59,7 @@ export class SubmissionRestService { getFirstCompletedRemoteData(), map((response: RemoteData) => { if (response.hasFailed) { - throw new Error(response.errorMessage); + throw new ErrorResponse({ statusText: response.errorMessage, statusCode: response.statusCode } as RequestError); } else { return hasValue(response.payload) ? response.payload.dataDefinition : response.payload; } @@ -78,7 +80,7 @@ export class SubmissionRestService { */ protected getEndpointByIDHref(endpoint, resourceID, collectionId?: string): string { let url = isNotEmpty(resourceID) ? `${endpoint}/${resourceID}` : `${endpoint}`; - url = new URLCombiner(url, '?projection=full').toString(); + url = new URLCombiner(url, '?embed=item,sections,collection').toString(); if (collectionId) { url = new URLCombiner(url, `&owningCollection=${collectionId}`).toString(); } diff --git a/src/app/core/submission/vocabularies/models/vocabulary-entry-detail.model.ts b/src/app/core/submission/vocabularies/models/vocabulary-entry-detail.model.ts index 3dcb0d374a7..f7dff16a405 100644 --- a/src/app/core/submission/vocabularies/models/vocabulary-entry-detail.model.ts +++ b/src/app/core/submission/vocabularies/models/vocabulary-entry-detail.model.ts @@ -21,19 +21,19 @@ export class VocabularyEntryDetail extends VocabularyEntry { * The unique id of the entry */ @autoserialize - id: string; + id: string; /** * In an hierarchical vocabulary representing if entry is selectable as value */ @autoserialize - selectable: boolean; + selectable: boolean; /** * The {@link HALLink}s for this ExternalSourceEntry */ @deserialize - _links: { + _links: { self: HALLink; vocabulary: HALLink; parent: HALLink; diff --git a/src/app/core/submission/vocabularies/models/vocabulary-entry.model.ts b/src/app/core/submission/vocabularies/models/vocabulary-entry.model.ts index 323db57de14..85b310bad88 100644 --- a/src/app/core/submission/vocabularies/models/vocabulary-entry.model.ts +++ b/src/app/core/submission/vocabularies/models/vocabulary-entry.model.ts @@ -24,25 +24,25 @@ export class VocabularyEntry extends ListableObject { * The identifier of this vocabulary entry */ @autoserialize - authority: string; + authority: string; /** * The display value of this vocabulary entry */ @autoserialize - display: string; + display: string; /** * The value of this vocabulary entry */ @autoserialize - value: string; + value: string; /** * An object containing additional information related to this vocabulary entry */ @autoserialize - otherInformation: OtherInformation; + otherInformation: OtherInformation; /** * A string representing the kind of vocabulary entry @@ -55,7 +55,7 @@ export class VocabularyEntry extends ListableObject { * The {@link HALLink}s for this ExternalSourceEntry */ @deserialize - _links: { + _links: { self: HALLink; vocabularyEntryDetail?: HALLink; }; diff --git a/src/app/core/submission/vocabularies/models/vocabulary.model.ts b/src/app/core/submission/vocabularies/models/vocabulary.model.ts index 9026ce677af..9da5ade3267 100644 --- a/src/app/core/submission/vocabularies/models/vocabulary.model.ts +++ b/src/app/core/submission/vocabularies/models/vocabulary.model.ts @@ -29,32 +29,32 @@ export class Vocabulary implements CacheableObject { * The identifier of this Vocabulary */ @autoserialize - id: string; + id: string; /** * The name of this Vocabulary */ @autoserialize - name: string; + name: string; /** * True if it is possible to scroll all the entries in the vocabulary without providing a filter parameter */ @autoserialize - scrollable: boolean; + scrollable: boolean; /** * True if the vocabulary exposes a tree structure where some entries are parent of others */ @autoserialize - hierarchical: boolean; + hierarchical: boolean; /** * For hierarchical vocabularies express the preference to preload the tree at a specific * level of depth (0 only the top nodes are shown, 1 also their children are preloaded and so on) */ @autoserialize - preloadLevel: any; + preloadLevel: any; /** * A string representing the kind of Vocabulary model @@ -64,13 +64,13 @@ export class Vocabulary implements CacheableObject { public type: any; @link(VOCABULARY_ENTRY, true) - entries?: Observable>>; + entries?: Observable>>; /** * The {@link HALLink}s for this Vocabulary */ @deserialize - _links: { + _links: { self: HALLink, entries: HALLink }; diff --git a/src/app/core/submission/vocabularies/vocabulary.data.service.spec.ts b/src/app/core/submission/vocabularies/vocabulary.data.service.spec.ts index 68c7c3ff808..0596b21d577 100644 --- a/src/app/core/submission/vocabularies/vocabulary.data.service.spec.ts +++ b/src/app/core/submission/vocabularies/vocabulary.data.service.spec.ts @@ -33,8 +33,8 @@ describe('VocabularyDataService', () => { spyOn(service, 'findByHref').and.returnValue(createSuccessfulRemoteDataObject$(null)); service.getVocabularyByMetadataAndCollection('dc.contributor.author', '1234-1234'); const options = Object.assign(new FindListOptions(), { - searchParams: [Object.assign(new RequestParam('metadata', encodeURIComponent('dc.contributor.author'))), - Object.assign(new RequestParam('collection', encodeURIComponent('1234-1234')))], + searchParams: [Object.assign(new RequestParam('metadata', 'dc.contributor.author')), + Object.assign(new RequestParam('collection', '1234-1234'))], }); expect((service as any).searchData.getSearchByHref).toHaveBeenCalledWith('byMetadataAndCollection', options); expect(service.findByHref).toHaveBeenCalledWith(vocabularyByMetadataAndCollectionEndpoint, true, true); diff --git a/src/app/core/submission/vocabularies/vocabulary.data.service.ts b/src/app/core/submission/vocabularies/vocabulary.data.service.ts index 57669d93491..62691cef8f2 100644 --- a/src/app/core/submission/vocabularies/vocabulary.data.service.ts +++ b/src/app/core/submission/vocabularies/vocabulary.data.service.ts @@ -78,8 +78,8 @@ export class VocabularyDataService extends IdentifiableDataService i */ public getVocabularyByMetadataAndCollection(metadataField: string, collectionUUID: string, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig[]): Observable> { const findListOptions = new FindListOptions(); - findListOptions.searchParams = [new RequestParam('metadata', encodeURIComponent(metadataField)), - new RequestParam('collection', encodeURIComponent(collectionUUID))]; + findListOptions.searchParams = [new RequestParam('metadata', metadataField), + new RequestParam('collection', collectionUUID)]; const href$ = this.searchData.getSearchByHref(this.searchByMetadataAndCollectionPath, findListOptions, ...linksToFollow); return this.findByHref(href$, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow); } diff --git a/src/app/core/submission/workflowitem-data.service.ts b/src/app/core/submission/workflowitem-data.service.ts index 88846ee2906..bb0acb57610 100644 --- a/src/app/core/submission/workflowitem-data.service.ts +++ b/src/app/core/submission/workflowitem-data.service.ts @@ -29,14 +29,12 @@ import { HALEndpointService } from '../shared/hal-endpoint.service'; import { NoContent } from '../shared/NoContent.model'; import { getFirstCompletedRemoteData } from '../shared/operators'; import { WorkflowItem } from './models/workflowitem.model'; -import { WorkspaceItem } from './models/workspaceitem.model'; /** * A service that provides methods to make REST requests with workflow items endpoint. */ @Injectable({ providedIn: 'root' }) export class WorkflowItemDataService extends IdentifiableDataService implements SearchData, DeleteData { - protected linkPath = 'workflowitems'; protected searchByItemLinkPath = 'item'; protected responseMsToLive = 10 * 1000; @@ -50,7 +48,7 @@ export class WorkflowItemDataService extends IdentifiableDataService[]): Observable> { + public findByItem(uuid: string, useCachedVersionIfAvailable = false, reRequestOnStale = true, options: FindListOptions = {}, ...linksToFollow: FollowLinkConfig[]): Observable> { const findListOptions = new FindListOptions(); - findListOptions.searchParams = [new RequestParam('uuid', encodeURIComponent(uuid))]; + findListOptions.searchParams = [new RequestParam('uuid', uuid)]; const href$ = this.searchData.getSearchByHref(this.searchByItemLinkPath, findListOptions, ...linksToFollow); return this.findByHref(href$, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow); } @@ -134,7 +132,7 @@ export class WorkflowItemDataService extends IdentifiableDataService>} * Return an observable that emits response from the server */ - public searchBy(searchMethod: string, options?: FindListOptions, useCachedVersionIfAvailable?: boolean, reRequestOnStale?: boolean, ...linksToFollow: FollowLinkConfig[]): Observable>> { + public searchBy(searchMethod: string, options?: FindListOptions, useCachedVersionIfAvailable?: boolean, reRequestOnStale?: boolean, ...linksToFollow: FollowLinkConfig[]): Observable>> { return this.searchData.searchBy(searchMethod, options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow); } diff --git a/src/app/core/submission/workspaceitem-data.service.spec.ts b/src/app/core/submission/workspaceitem-data.service.spec.ts index 644cae85cb6..8837792a786 100644 --- a/src/app/core/submission/workspaceitem-data.service.spec.ts +++ b/src/app/core/submission/workspaceitem-data.service.spec.ts @@ -2,13 +2,18 @@ import { HttpClient, HttpHeaders, } from '@angular/common/http'; +import { waitForAsync } from '@angular/core/testing'; import { Store } from '@ngrx/store'; import { cold, getTestScheduler, hot, } from 'jasmine-marbles'; -import { of as observableOf } from 'rxjs'; +import { + of as observableOf, + of, +} from 'rxjs'; +import { map } from 'rxjs/operators'; import { TestScheduler } from 'rxjs/testing'; import { getMockHrefOnlyDataService } from '../../shared/mocks/href-only-data.service.mock'; @@ -151,12 +156,17 @@ describe('WorkspaceitemDataService test', () => { }); describe('findByItem', () => { - it('should proxy the call to UpdateDataServiceImpl.findByHref', () => { + it('should proxy the call to UpdateDataServiceImpl.findByHref', waitForAsync(() => { scheduler.schedule(() => service.findByItem('1234-1234', true, true, pageInfo)); scheduler.flush(); - const searchUrl = service.getIDHref('item', [new RequestParam('uuid', encodeURIComponent('1234-1234'))]); - expect((service as any).findByHref).toHaveBeenCalledWith(searchUrl, true, true); - }); + const searchUrl$ = + of('https://rest.api/rest/api/submission/workspaceitems/search/item') + .pipe(map(href => service.buildHrefFromFindOptions(href, { searchParams: [new RequestParam('uuid', '1234-1234')] }, []))); + searchUrl$.subscribe((url) => { + expect(url).toEqual('https://rest.api/rest/api/submission/workspaceitems/search/item?uuid=1234-1234'); + }); + expect((service as any).findByHref).toHaveBeenCalled(); + })); it('should return a RemoteData for the search', () => { const result = service.findByItem('1234-1234', true, true, pageInfo); diff --git a/src/app/core/submission/workspaceitem-data.service.ts b/src/app/core/submission/workspaceitem-data.service.ts index 6df06ccad53..17aafaf6514 100644 --- a/src/app/core/submission/workspaceitem-data.service.ts +++ b/src/app/core/submission/workspaceitem-data.service.ts @@ -45,7 +45,7 @@ export class WorkspaceitemDataService extends IdentifiableDataService; - private searchData: SearchData; + private searchData: SearchDataImpl; constructor( protected comparator: DSOChangeAnalyzer, @@ -90,8 +90,8 @@ export class WorkspaceitemDataService extends IdentifiableDataService[]): Observable> { const findListOptions = new FindListOptions(); - findListOptions.searchParams = [new RequestParam('uuid', encodeURIComponent(uuid))]; - const href$ = this.getIDHref(this.searchByItemLinkPath, findListOptions, ...linksToFollow); + findListOptions.searchParams = [new RequestParam('uuid', uuid)]; + const href$ = this.searchData.getSearchByHref(this.searchByItemLinkPath, findListOptions, ...linksToFollow); return this.findByHref(href$, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow); } diff --git a/src/app/core/supervision-order/models/supervision-order.model.ts b/src/app/core/supervision-order/models/supervision-order.model.ts index 64087c8a219..6426f62aeca 100644 --- a/src/app/core/supervision-order/models/supervision-order.model.ts +++ b/src/app/core/supervision-order/models/supervision-order.model.ts @@ -32,21 +32,21 @@ export class SupervisionOrder implements CacheableObject { * The identifier for this Supervision Order */ @autoserialize - id: string; + id: string; /** * The object type */ @excludeFromEquals @autoserialize - type: ResourceType; + type: ResourceType; /** * The object type */ @excludeFromEquals @autoserialize - ordertype: string; + ordertype: string; /** * The universally unique identifier for this Supervision Order @@ -60,7 +60,7 @@ export class SupervisionOrder implements CacheableObject { * The {@link HALLink}s for this SupervisionOrder */ @deserialize - _links: { + _links: { item: HALLink, group: HALLink, self: HALLink, @@ -71,12 +71,12 @@ export class SupervisionOrder implements CacheableObject { * Will be undefined unless the item {@link HALLink} has been resolved. */ @link(ITEM) - item?: Observable>; + item?: Observable>; /** * The group linked by this supervision order * Will be undefined unless the version {@link HALLink} has been resolved. */ @link(GROUP) - group?: Observable>; + group?: Observable>; } diff --git a/src/app/core/tasks/models/advanced-workflow-info.model.ts b/src/app/core/tasks/models/advanced-workflow-info.model.ts index 6e5feec357e..87991a375c8 100644 --- a/src/app/core/tasks/models/advanced-workflow-info.model.ts +++ b/src/app/core/tasks/models/advanced-workflow-info.model.ts @@ -6,6 +6,6 @@ import { autoserialize } from 'cerialize'; export abstract class AdvancedWorkflowInfo { @autoserialize - id: string; + id: string; } diff --git a/src/app/core/tasks/models/rating-advanced-workflow-info.model.ts b/src/app/core/tasks/models/rating-advanced-workflow-info.model.ts index 892806f7d16..6ecb0e6dbfe 100644 --- a/src/app/core/tasks/models/rating-advanced-workflow-info.model.ts +++ b/src/app/core/tasks/models/rating-advanced-workflow-info.model.ts @@ -21,12 +21,12 @@ export class RatingAdvancedWorkflowInfo extends AdvancedWorkflowInfo { * Whether the description is required. */ @autoserialize - descriptionRequired: boolean; + descriptionRequired: boolean; /** * The maximum value. */ @autoserialize - maxValue: number; + maxValue: number; } diff --git a/src/app/core/tasks/models/select-reviewer-advanced-workflow-info.model.ts b/src/app/core/tasks/models/select-reviewer-advanced-workflow-info.model.ts index d202593efb8..307caeaa1fe 100644 --- a/src/app/core/tasks/models/select-reviewer-advanced-workflow-info.model.ts +++ b/src/app/core/tasks/models/select-reviewer-advanced-workflow-info.model.ts @@ -18,6 +18,6 @@ export class SelectReviewerAdvancedWorkflowInfo extends AdvancedWorkflowInfo { static type: ResourceType = SELECT_REVIEWER_ADVANCED_WORKFLOW_INFO; @autoserialize - group: string; + group: string; } diff --git a/src/app/core/tasks/models/task-object.model.ts b/src/app/core/tasks/models/task-object.model.ts index cea886a1c6a..0e5022d5797 100644 --- a/src/app/core/tasks/models/task-object.model.ts +++ b/src/app/core/tasks/models/task-object.model.ts @@ -34,19 +34,19 @@ export class TaskObject extends DSpaceObject implements CacheableObject { * The task identifier */ @autoserialize - id: string; + id: string; /** * The workflow step */ @autoserialize - step: string; + step: string; /** * The {@link HALLink}s for this TaskObject */ @deserialize - _links: { + _links: { self: HALLink; owner: HALLink; group: HALLink; @@ -59,14 +59,14 @@ export class TaskObject extends DSpaceObject implements CacheableObject { * Will be undefined unless the eperson {@link HALLink} has been resolved. */ @link(EPERSON, false, 'owner') - eperson?: Observable>; + eperson?: Observable>; /** * The Group for this task * Will be undefined unless the group {@link HALLink} has been resolved. */ @link(GROUP) - group?: Observable>; + group?: Observable>; /** * The WorkflowItem for this task @@ -81,6 +81,6 @@ export class TaskObject extends DSpaceObject implements CacheableObject { * Will be undefined unless the group {@link HALLink} has been resolved. */ @link(WORKFLOW_ACTION, false, 'action') - action: Observable>; + action: Observable>; } diff --git a/src/app/core/tasks/models/workflow-action-object.model.ts b/src/app/core/tasks/models/workflow-action-object.model.ts index 53ae4249c87..a1010008851 100644 --- a/src/app/core/tasks/models/workflow-action-object.model.ts +++ b/src/app/core/tasks/models/workflow-action-object.model.ts @@ -20,30 +20,30 @@ export class WorkflowAction extends DSpaceObject { * The workflow action's identifier */ @autoserialize - id: string; + id: string; /** * The options available for this workflow action */ @autoserialize - options: string[]; + options: string[]; /** * Whether this action has advanced options */ @autoserialize - advanced: boolean; + advanced: boolean; /** * The advanced options that the user can select at this action */ @autoserialize - advancedOptions: string[]; + advancedOptions: string[]; /** * The advanced info required by the advanced options */ @autoserialize - advancedInfo: AdvancedWorkflowInfo[]; + advancedInfo: AdvancedWorkflowInfo[]; } diff --git a/src/app/core/utilities/equatable.spec.ts b/src/app/core/utilities/equatable.spec.ts index 808c21b39fc..cea3c35b3d0 100644 --- a/src/app/core/utilities/equatable.spec.ts +++ b/src/app/core/utilities/equatable.spec.ts @@ -21,7 +21,7 @@ class Dog extends EquatableObject { class Owner extends EquatableObject { @excludeFromEquals - favouriteFood: string; + favouriteFood: string; constructor( public name: string, diff --git a/src/app/curation-form/curation-form.component.ts b/src/app/curation-form/curation-form.component.ts index 0580193418f..8e1c1b26c8f 100644 --- a/src/app/curation-form/curation-form.component.ts +++ b/src/app/curation-form/curation-form.component.ts @@ -62,7 +62,7 @@ export class CurationFormComponent implements OnDestroy, OnInit { form: UntypedFormGroup; @Input() - dsoHandle: string; + dsoHandle: string; subs: Subscription[] = []; diff --git a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value/dso-edit-metadata-value.component.html b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value/dso-edit-metadata-value.component.html index b79b185b40d..25b82b9b437 100644 --- a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value/dso-edit-metadata-value.component.html +++ b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value/dso-edit-metadata-value.component.html @@ -1,4 +1,4 @@ -
@@ -9,12 +9,12 @@
@@ -55,7 +55,7 @@
diff --git a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value/dso-edit-metadata-value.component.spec.ts b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value/dso-edit-metadata-value.component.spec.ts index ccfcfca93c4..e96959c1d1f 100644 --- a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value/dso-edit-metadata-value.component.spec.ts +++ b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value/dso-edit-metadata-value.component.spec.ts @@ -10,10 +10,7 @@ import { import { By } from '@angular/platform-browser'; import { RouterTestingModule } from '@angular/router/testing'; import { TranslateModule } from '@ngx-translate/core'; -import { - Observable, - of, -} from 'rxjs'; +import { of } from 'rxjs'; import { MetadataField } from 'src/app/core/metadata/metadata-field.model'; import { MetadataSchema } from 'src/app/core/metadata/metadata-schema.model'; import { RegistryService } from 'src/app/core/registry/registry.service'; @@ -221,7 +218,7 @@ describe('DsoEditMetadataValueComponent', () => { it('should not show a badge', () => { expect( - fixture.debugElement.query(By.css('ds-themed-type-badge')), + fixture.debugElement.query(By.css('ds-type-badge')), ).toBeNull(); }); @@ -289,7 +286,7 @@ describe('DsoEditMetadataValueComponent', () => { it('should show a badge', () => { expect( - fixture.debugElement.query(By.css('ds-themed-type-badge')), + fixture.debugElement.query(By.css('ds-type-badge')), ).toBeTruthy(); }); @@ -346,14 +343,11 @@ describe('DsoEditMetadataValueComponent', () => { }); it('getModel should return a DynamicScrollableDropdownModel', () => { - const result = component.getModel(); + const model = component.getModel(); - expect(result instanceof Observable).toBe(true); + expect(model instanceof DynamicScrollableDropdownModel).toBe(true); + expect(model.vocabularyOptions.name).toBe(mockVocabularyScrollable.name); - result.subscribe((model) => { - expect(model instanceof DynamicScrollableDropdownModel).toBe(true); - expect(model.vocabularyOptions.name).toBe(mockVocabularyScrollable.name); - }); }); }); @@ -380,14 +374,10 @@ describe('DsoEditMetadataValueComponent', () => { }); it('getModel should return a DynamicOneboxModel', () => { - const result = component.getModel(); - - expect(result instanceof Observable).toBe(true); + const model = component.getModel(); - result.subscribe((model) => { - expect(model instanceof DynamicOneboxModel).toBe(true); - expect(model.vocabularyOptions.name).toBe(mockVocabularyHierarchical.name); - }); + expect(model instanceof DynamicOneboxModel).toBe(true); + expect(model.vocabularyOptions.name).toBe(mockVocabularyHierarchical.name); }); }); @@ -416,14 +406,10 @@ describe('DsoEditMetadataValueComponent', () => { }); it('getModel should return a DynamicOneboxModel', () => { - const result = component.getModel(); - - expect(result instanceof Observable).toBe(true); + const model = component.getModel(); - result.subscribe((model) => { - expect(model instanceof DynamicOneboxModel).toBe(true); - expect(model.vocabularyOptions.name).toBe(mockVocabularySuggester.name); - }); + expect(model instanceof DynamicOneboxModel).toBe(true); + expect(model.vocabularyOptions.name).toBe(mockVocabularySuggester.name); }); describe('authority key edition', () => { diff --git a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value/dso-edit-metadata-value.component.ts b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value/dso-edit-metadata-value.component.ts index 1aefe5af236..11bbcfd9d5e 100644 --- a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value/dso-edit-metadata-value.component.ts +++ b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value/dso-edit-metadata-value.component.ts @@ -29,6 +29,7 @@ import { TranslateService, } from '@ngx-translate/core'; import { + BehaviorSubject, EMPTY, Observable, of as observableOf, @@ -37,6 +38,7 @@ import { map, switchMap, take, + tap, } from 'rxjs/operators'; import { RegistryService } from 'src/app/core/registry/registry.service'; import { VocabularyService } from 'src/app/core/submission/vocabularies/vocabulary.service'; @@ -45,6 +47,7 @@ import { NotificationsService } from 'src/app/shared/notifications/notifications import { DSONameService } from '../../../core/breadcrumbs/dso-name.service'; import { ItemDataService } from '../../../core/data/item-data.service'; import { RelationshipDataService } from '../../../core/data/relationship-data.service'; +import { MetadataService } from '../../../core/metadata/metadata.service'; import { Collection } from '../../../core/shared/collection.model'; import { ConfidenceType } from '../../../core/shared/confidence-type'; import { DSpaceObject } from '../../../core/shared/dspace-object.model'; @@ -195,9 +198,9 @@ export class DsoEditMetadataValueComponent implements OnInit, OnChanges { group = new UntypedFormGroup({ authorityField : new UntypedFormControl() }); /** - * Observable property of the model to use for editinf authorities values + * Model to use for editing authorities values */ - private model$: Observable; + private model$: BehaviorSubject = new BehaviorSubject(null); /** * Observable with information about the authority vocabulary used @@ -212,14 +215,17 @@ export class DsoEditMetadataValueComponent implements OnInit, OnChanges { private isScrollableVocabulary$: Observable; private isSuggesterVocabulary$: Observable; - constructor(protected relationshipService: RelationshipDataService, - protected dsoNameService: DSONameService, - protected vocabularyService: VocabularyService, - protected itemService: ItemDataService, - protected cdr: ChangeDetectorRef, - protected registryService: RegistryService, - protected notificationsService: NotificationsService, - protected translate: TranslateService) { + constructor( + protected relationshipService: RelationshipDataService, + protected dsoNameService: DSONameService, + protected vocabularyService: VocabularyService, + protected itemService: ItemDataService, + protected cdr: ChangeDetectorRef, + protected registryService: RegistryService, + protected notificationsService: NotificationsService, + protected translate: TranslateService, + protected metadataService: MetadataService, + ) { } ngOnInit(): void { @@ -231,7 +237,7 @@ export class DsoEditMetadataValueComponent implements OnInit, OnChanges { * Initialise potential properties of a virtual metadata value */ initVirtualProperties(): void { - this.mdRepresentation$ = this.mdValue.newValue.isVirtual ? + this.mdRepresentation$ = this.metadataService.isVirtual(this.mdValue.newValue) ? this.relationshipService.resolveMetadataRepresentation(this.mdValue.newValue, this.dso, 'Item') .pipe( map((mdRepresentation: MetadataRepresentation) => @@ -274,6 +280,8 @@ export class DsoEditMetadataValueComponent implements OnInit, OnChanges { } this.isAuthorityControlled$ = this.vocabulary$.pipe( + // Create the model used by the authority fields to ensure its existence when the field is initialized + tap((v: Vocabulary) => this.model$.next(this.createModel(v))), map((result: Vocabulary) => isNotEmpty(result)), ); @@ -289,54 +297,62 @@ export class DsoEditMetadataValueComponent implements OnInit, OnChanges { map((result: Vocabulary) => isNotEmpty(result) && !result.hierarchical && !result.scrollable), ); - this.model$ = this.vocabulary$.pipe( - map((vocabulary: Vocabulary) => { - let formFieldValue; - if (isNotEmpty(this.mdValue.newValue.value)) { - formFieldValue = new FormFieldMetadataValueObject(); - formFieldValue.value = this.mdValue.newValue.value; - formFieldValue.display = this.mdValue.newValue.value; - if (this.mdValue.newValue.authority) { - formFieldValue.authority = this.mdValue.newValue.authority; - formFieldValue.confidence = this.mdValue.newValue.confidence; - } - } else { - formFieldValue = this.mdValue.newValue.value; - } + } - const vocabularyOptions = vocabulary ? { - closed: false, - name: vocabulary.name, - } as VocabularyOptions : null; - - if (!vocabulary.scrollable) { - const model: DsDynamicOneboxModelConfig = { - id: 'authorityField', - label: `${this.dsoType}.edit.metadata.edit.value`, - vocabularyOptions: vocabularyOptions, - metadataFields: [this.mdField], - value: formFieldValue, - repeatable: false, - submissionId: 'edit-metadata', - hasSelectableMetadata: false, - }; - return new DynamicOneboxModel(model); - } else { - const model: DynamicScrollableDropdownModelConfig = { - id: 'authorityField', - label: `${this.dsoType}.edit.metadata.edit.value`, - placeholder: `${this.dsoType}.edit.metadata.edit.value`, - vocabularyOptions: vocabularyOptions, - metadataFields: [this.mdField], - value: formFieldValue, - repeatable: false, - submissionId: 'edit-metadata', - hasSelectableMetadata: false, - maxOptions: 10, - }; - return new DynamicScrollableDropdownModel(model); + /** + * Returns a {@link DynamicOneboxModel} or {@link DynamicScrollableDropdownModel} model based on the + * vocabulary used. + */ + private createModel(vocabulary: Vocabulary): DynamicOneboxModel | DynamicScrollableDropdownModel { + if (isNotEmpty(vocabulary)) { + let formFieldValue; + if (isNotEmpty(this.mdValue.newValue.value)) { + formFieldValue = new FormFieldMetadataValueObject(); + formFieldValue.value = this.mdValue.newValue.value; + formFieldValue.display = this.mdValue.newValue.value; + if (this.mdValue.newValue.authority) { + formFieldValue.authority = this.mdValue.newValue.authority; + formFieldValue.confidence = this.mdValue.newValue.confidence; } - })); + } else { + formFieldValue = this.mdValue.newValue.value; + } + + const vocabularyOptions = vocabulary ? { + closed: false, + name: vocabulary.name, + } as VocabularyOptions : null; + + if (!vocabulary.scrollable) { + const model: DsDynamicOneboxModelConfig = { + id: 'authorityField', + label: `${this.dsoType}.edit.metadata.edit.value`, + vocabularyOptions: vocabularyOptions, + metadataFields: [this.mdField], + value: formFieldValue, + repeatable: false, + submissionId: 'edit-metadata', + hasSelectableMetadata: false, + }; + return new DynamicOneboxModel(model); + } else { + const model: DynamicScrollableDropdownModelConfig = { + id: 'authorityField', + label: `${this.dsoType}.edit.metadata.edit.value`, + placeholder: `${this.dsoType}.edit.metadata.edit.value`, + vocabularyOptions: vocabularyOptions, + metadataFields: [this.mdField], + value: formFieldValue, + repeatable: false, + submissionId: 'edit-metadata', + hasSelectableMetadata: false, + maxOptions: 10, + }; + return new DynamicScrollableDropdownModel(model); + } + } else { + return null; + } } /** @@ -434,11 +450,11 @@ export class DsoEditMetadataValueComponent implements OnInit, OnChanges { } /** - * Returns an observable with the {@link DynamicOneboxModel} or {@link DynamicScrollableDropdownModel} model used + * Returns the {@link DynamicOneboxModel} or {@link DynamicScrollableDropdownModel} model used * for the authority field */ - getModel(): Observable { - return this.model$; + getModel(): DynamicOneboxModel | DynamicScrollableDropdownModel { + return this.model$.value; } /** diff --git a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata.component.html b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata.component.html index d6c72abdb94..f8b193f4a05 100644 --- a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata.component.html +++ b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata.component.html @@ -1,21 +1,25 @@ diff --git a/src/app/item-page/alerts/item-alerts.component.spec.ts b/src/app/item-page/alerts/item-alerts.component.spec.ts index d2541f9d0d3..ef788c47ecb 100644 --- a/src/app/item-page/alerts/item-alerts.component.spec.ts +++ b/src/app/item-page/alerts/item-alerts.component.spec.ts @@ -161,7 +161,7 @@ describe('ItemAlertsComponent', () => { (authorizationService.isAuthorized).and.returnValue(isAdmin$); (correctionTypeDataService.findByItem).and.returnValue(correction$); - expectObservable(component.showReinstateButton$()).toBe(expectedMarble, expectedValues); + expectObservable(component.shouldShowReinstateButton()).toBe(expectedMarble, expectedValues); }); }); diff --git a/src/app/item-page/alerts/item-alerts.component.ts b/src/app/item-page/alerts/item-alerts.component.ts index 6196eaf9ece..bcf940e69d2 100644 --- a/src/app/item-page/alerts/item-alerts.component.ts +++ b/src/app/item-page/alerts/item-alerts.component.ts @@ -5,6 +5,8 @@ import { import { Component, Input, + OnChanges, + SimpleChanges, } from '@angular/core'; import { RouterLink } from '@angular/router'; import { TranslateModule } from '@ngx-translate/core'; @@ -30,7 +32,7 @@ import { } from '../../shared/dso-page/dso-withdrawn-reinstate-service/dso-withdrawn-reinstate-modal.service'; @Component({ - selector: 'ds-item-alerts', + selector: 'ds-base-item-alerts', templateUrl: './item-alerts.component.html', styleUrls: ['./item-alerts.component.scss'], imports: [ @@ -45,12 +47,17 @@ import { /** * Component displaying alerts for an item */ -export class ItemAlertsComponent { +export class ItemAlertsComponent implements OnChanges { /** * The Item to display alerts for */ @Input() item: Item; + /** + * Whether the reinstate button should be shown + */ + showReinstateButton$: Observable; + /** * The AlertType enumeration * @type {AlertType} @@ -58,18 +65,24 @@ export class ItemAlertsComponent { public AlertTypeEnum = AlertType; constructor( - private authService: AuthorizationDataService, - private dsoWithdrawnReinstateModalService: DsoWithdrawnReinstateModalService, - private correctionTypeDataService: CorrectionTypeDataService, + protected authService: AuthorizationDataService, + protected dsoWithdrawnReinstateModalService: DsoWithdrawnReinstateModalService, + protected correctionTypeDataService: CorrectionTypeDataService, ) { } + ngOnChanges(changes: SimpleChanges): void { + if (changes.item?.currentValue.withdrawn && this.showReinstateButton$) { + this.showReinstateButton$ = this.shouldShowReinstateButton(); + } + } + /** * Determines whether to show the reinstate button. * The button is shown if the user is not an admin and the item has a reinstate request. * @returns An Observable that emits a boolean value indicating whether to show the reinstate button. */ - showReinstateButton$(): Observable { + shouldShowReinstateButton(): Observable { const correction$ = this.correctionTypeDataService.findByItem(this.item.uuid, true).pipe( getFirstCompletedRemoteData(), map((correctionTypeRD: RemoteData>) => correctionTypeRD.hasSucceeded ? correctionTypeRD.payload.page : []), @@ -78,8 +91,8 @@ export class ItemAlertsComponent { return combineLatest([isAdmin$, correction$]).pipe( map(([isAdmin, correction]) => { return !isAdmin && correction.some((correctionType) => correctionType.topic === REQUEST_REINSTATE); - }, - )); + }), + ); } /** diff --git a/src/app/item-page/alerts/themed-item-alerts.component.ts b/src/app/item-page/alerts/themed-item-alerts.component.ts index 9a686d9a36a..94197f3fb41 100644 --- a/src/app/item-page/alerts/themed-item-alerts.component.ts +++ b/src/app/item-page/alerts/themed-item-alerts.component.ts @@ -11,10 +11,11 @@ import { ItemAlertsComponent } from './item-alerts.component'; * Themed wrapper for {@link ItemAlertsComponent} */ @Component({ - selector: 'ds-themed-item-alerts', + selector: 'ds-item-alerts', styleUrls: [], templateUrl: '../../shared/theme-support/themed.component.html', standalone: true, + imports: [ItemAlertsComponent], }) export class ThemedItemAlertsComponent extends ThemedComponent { protected inAndOutputNames: (keyof ItemAlertsComponent & keyof this)[] = ['item']; diff --git a/src/app/item-page/bitstreams/request-a-copy/bitstream-request-a-copy-page.component.ts b/src/app/item-page/bitstreams/request-a-copy/bitstream-request-a-copy-page.component.ts index e32239a5779..bd12d3675ec 100644 --- a/src/app/item-page/bitstreams/request-a-copy/bitstream-request-a-copy-page.component.ts +++ b/src/app/item-page/bitstreams/request-a-copy/bitstream-request-a-copy-page.component.ts @@ -253,7 +253,7 @@ export class BitstreamRequestACopyPageComponent implements OnInit, OnDestroy { } /** - * Retrieves the link to the bistream download page + * Retrieves the link to the bitstream download page */ getBitstreamLink() { return [getBitstreamDownloadRoute(this.bitstream)]; diff --git a/src/app/item-page/edit-item-page/abstract-item-update/abstract-item-update.component.ts b/src/app/item-page/edit-item-page/abstract-item-update/abstract-item-update.component.ts index 77c3fdaafb6..254c44ad467 100644 --- a/src/app/item-page/edit-item-page/abstract-item-update/abstract-item-update.component.ts +++ b/src/app/item-page/edit-item-page/abstract-item-update/abstract-item-update.component.ts @@ -16,9 +16,9 @@ import { Subscription, } from 'rxjs'; import { - first, map, switchMap, + take, tap, } from 'rxjs/operators'; @@ -33,7 +33,7 @@ import { getAllSucceededRemoteData } from '../../../core/shared/operators'; import { hasValue } from '../../../shared/empty.util'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { AbstractTrackableComponent } from '../../../shared/trackable/abstract-trackable.component'; -import { ITEM_PAGE_LINKS_TO_FOLLOW } from '../../item.resolver'; +import { getItemPageLinksToFollow } from '../../item.resolver'; import { getItemPageRoute } from '../../item-page-routing-paths'; @Component({ @@ -74,7 +74,7 @@ export class AbstractItemUpdateComponent extends AbstractTrackableComponent impl public translateService: TranslateService, public route: ActivatedRoute, ) { - super(objectUpdatesService, notificationsService, translateService); + super(objectUpdatesService, notificationsService, translateService, router); } /** @@ -92,20 +92,17 @@ export class AbstractItemUpdateComponent extends AbstractTrackableComponent impl this.item = rd.payload; }), switchMap((rd: RemoteData) => { - return this.itemService.findByHref(rd.payload._links.self.href, true, true, ...ITEM_PAGE_LINKS_TO_FOLLOW); + return this.itemService.findByHref(rd.payload._links.self.href, true, true, ...getItemPageLinksToFollow()); }), getAllSucceededRemoteData(), ).subscribe((rd: RemoteData) => { this.setItem(rd.payload); }); } + super.ngOnInit(); this.discardTimeOut = environment.item.edit.undoTimeout; - this.url = this.router.url; - if (this.url.indexOf('?') > 0) { - this.url = this.url.substr(0, this.url.indexOf('?')); - } - this.hasChanges().pipe(first()).subscribe((hasChanges) => { + this.hasChanges().pipe(take(1)).subscribe((hasChanges) => { if (!hasChanges) { this.initializeOriginalFields(); } else { @@ -190,7 +187,7 @@ export class AbstractItemUpdateComponent extends AbstractTrackableComponent impl */ private checkLastModified() { const currentVersion = this.item.lastModified; - this.objectUpdatesService.getLastModified(this.url).pipe(first()).subscribe( + this.objectUpdatesService.getLastModified(this.url).pipe(take(1)).subscribe( (updateVersion: Date) => { if (updateVersion.getDate() !== currentVersion.getDate()) { this.notificationsService.warning(this.getNotificationTitle('outdated'), this.getNotificationContent('outdated')); diff --git a/src/app/item-page/edit-item-page/edit-item-page-routes.ts b/src/app/item-page/edit-item-page/edit-item-page-routes.ts index 1b1e43a883c..a7189f98881 100644 --- a/src/app/item-page/edit-item-page/edit-item-page-routes.ts +++ b/src/app/item-page/edit-item-page/edit-item-page-routes.ts @@ -1,7 +1,4 @@ -import { - mapToCanActivate, - Route, -} from '@angular/router'; +import { Route } from '@angular/router'; import { i18nBreadcrumbResolver } from '../../core/breadcrumbs/i18n-breadcrumb.resolver'; import { ThemedDsoEditMetadataComponent } from '../../dso-shared/dso-edit-metadata/themed-dso-edit-metadata.component'; @@ -27,17 +24,21 @@ import { ItemCollectionMapperComponent } from './item-collection-mapper/item-col import { ItemCurateComponent } from './item-curate/item-curate.component'; import { ItemDeleteComponent } from './item-delete/item-delete.component'; import { ItemMoveComponent } from './item-move/item-move.component'; -import { ItemPageAccessControlGuard } from './item-page-access-control.guard'; -import { ItemPageBitstreamsGuard } from './item-page-bitstreams.guard'; -import { ItemPageCollectionMapperGuard } from './item-page-collection-mapper.guard'; -import { ItemPageCurateGuard } from './item-page-curate.guard'; -import { ItemPageMetadataGuard } from './item-page-metadata.guard'; -import { ItemPageRegisterDoiGuard } from './item-page-register-doi.guard'; -import { ItemPageReinstateGuard } from './item-page-reinstate.guard'; -import { ItemPageRelationshipsGuard } from './item-page-relationships.guard'; -import { ItemPageStatusGuard } from './item-page-status.guard'; -import { ItemPageVersionHistoryGuard } from './item-page-version-history.guard'; -import { ItemPageWithdrawGuard } from './item-page-withdraw.guard'; +import { itemPageAccessControlGuard } from './item-page-access-control.guard'; +import { itemPageBitstreamsGuard } from './item-page-bitstreams.guard'; +import { itemPageCollectionMapperGuard } from './item-page-collection-mapper.guard'; +import { itemPageCurateGuard } from './item-page-curate.guard'; +import { itemPageDeleteGuard } from './item-page-delete.guard'; +import { itemPageEditAuthorizationsGuard } from './item-page-edit-authorizations.guard'; +import { itemPageMetadataGuard } from './item-page-metadata.guard'; +import { itemPageMoveGuard } from './item-page-move.guard'; +import { itemPagePrivateGuard } from './item-page-private.guard'; +import { itemPageRegisterDoiGuard } from './item-page-register-doi.guard'; +import { itemPageReinstateGuard } from './item-page-reinstate.guard'; +import { itemPageRelationshipsGuard } from './item-page-relationships.guard'; +import { itemPageStatusGuard } from './item-page-status.guard'; +import { itemPageVersionHistoryGuard } from './item-page-version-history.guard'; +import { itemPageWithdrawGuard } from './item-page-withdraw.guard'; import { ItemPrivateComponent } from './item-private/item-private.component'; import { ItemPublicComponent } from './item-public/item-public.component'; import { ItemRegisterDoiComponent } from './item-register-doi/item-register-doi.component'; @@ -72,31 +73,31 @@ export const ROUTES: Route[] = [ path: 'status', component: ThemedItemStatusComponent, data: { title: 'item.edit.tabs.status.title', showBreadcrumbs: true }, - canActivate: mapToCanActivate([ItemPageStatusGuard]), + canActivate: [itemPageStatusGuard], }, { path: 'bitstreams', component: ItemBitstreamsComponent, data: { title: 'item.edit.tabs.bitstreams.title', showBreadcrumbs: true }, - canActivate: mapToCanActivate([ItemPageBitstreamsGuard]), + canActivate: [itemPageBitstreamsGuard], }, { path: 'metadata', component: ThemedDsoEditMetadataComponent, data: { title: 'item.edit.tabs.metadata.title', showBreadcrumbs: true }, - canActivate: mapToCanActivate([ItemPageMetadataGuard]), + canActivate: [itemPageMetadataGuard], }, { path: 'curate', component: ItemCurateComponent, data: { title: 'item.edit.tabs.curate.title', showBreadcrumbs: true }, - canActivate: mapToCanActivate([ItemPageCurateGuard]), + canActivate: [itemPageCurateGuard], }, { path: 'relationships', component: ItemRelationshipsComponent, data: { title: 'item.edit.tabs.relationships.title', showBreadcrumbs: true }, - canActivate: mapToCanActivate([ItemPageRelationshipsGuard]), + canActivate: [itemPageRelationshipsGuard], }, /* TODO - uncomment & fix when view page exists { @@ -114,19 +115,19 @@ export const ROUTES: Route[] = [ path: 'versionhistory', component: ItemVersionHistoryComponent, data: { title: 'item.edit.tabs.versionhistory.title', showBreadcrumbs: true }, - canActivate: mapToCanActivate([ItemPageVersionHistoryGuard]), + canActivate: [itemPageVersionHistoryGuard], }, { path: 'access-control', component: ItemAccessControlComponent, data: { title: 'item.edit.tabs.access-control.title', showBreadcrumbs: true }, - canActivate: mapToCanActivate([ItemPageAccessControlGuard]), + canActivate: [itemPageAccessControlGuard], }, { path: 'mapper', component: ItemCollectionMapperComponent, data: { title: 'item.edit.tabs.item-mapper.title', showBreadcrumbs: true }, - canActivate: mapToCanActivate([ItemPageCollectionMapperGuard]), + canActivate: [itemPageCollectionMapperGuard], }, ], }, @@ -137,16 +138,17 @@ export const ROUTES: Route[] = [ { path: ITEM_EDIT_WITHDRAW_PATH, component: ItemWithdrawComponent, - canActivate: mapToCanActivate([ItemPageWithdrawGuard]), + canActivate: [itemPageWithdrawGuard], }, { path: ITEM_EDIT_REINSTATE_PATH, component: ItemReinstateComponent, - canActivate: mapToCanActivate([ItemPageReinstateGuard]), + canActivate: [itemPageReinstateGuard], }, { path: ITEM_EDIT_PRIVATE_PATH, component: ItemPrivateComponent, + canActivate: [itemPagePrivateGuard], }, { path: ITEM_EDIT_PUBLIC_PATH, @@ -155,16 +157,18 @@ export const ROUTES: Route[] = [ { path: ITEM_EDIT_DELETE_PATH, component: ItemDeleteComponent, + canActivate: [itemPageDeleteGuard], }, { path: ITEM_EDIT_MOVE_PATH, component: ItemMoveComponent, data: { title: 'item.edit.move.title' }, + canActivate: [itemPageMoveGuard], }, { path: ITEM_EDIT_REGISTER_DOI_PATH, component: ItemRegisterDoiComponent, - canActivate: mapToCanActivate([ItemPageRegisterDoiGuard]), + canActivate: [itemPageRegisterDoiGuard], data: { title: 'item.edit.register-doi.title' }, }, { @@ -192,6 +196,7 @@ export const ROUTES: Route[] = [ data: { title: 'item.edit.authorizations.title' }, }, ], + canActivate: [itemPageEditAuthorizationsGuard], }, ], }, diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.html b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.html index 48635a50f62..9d8f384e166 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.html +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.html @@ -1,4 +1,8 @@
+
+ +
+
- - -
-
-
-
- - {{'item.edit.bitstreams.headers.name' | translate}} -
-
{{'item.edit.bitstreams.headers.description' | translate}}
-
{{'item.edit.bitstreams.headers.format' | translate}}
-
{{'item.edit.bitstreams.headers.actions' | translate}}
-
- + + [isFirstTable]="isFirst" + aria-describedby="reorder-description">
- +
- - -
+ + diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.scss b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.scss index 7de575b7855..7fd1f4b31e7 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.scss +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.scss @@ -1,23 +1,4 @@ -.header-row { - color: var(--bs-table-dark-color); - background-color: var(--bs-table-dark-bg); - border-color: var(--bs-table-dark-border-color); -} - -.bundle-row { - color: var(--bs-table-head-color); - background-color: var(--bs-table-head-bg); - border-color: var(--bs-table-border-color); -} - -.row-element { - padding: 12px; - padding: 0.75em; - border-bottom: var(--bs-table-border-width) solid var(--bs-table-border-color); -} - .drag-handle { - visibility: hidden; &:hover { cursor: move; } @@ -27,10 +8,6 @@ cursor: move; } -:host ::ng-deep .bitstream-row:hover .drag-handle, :host ::ng-deep .bitstream-row-drag-handle:focus .drag-handle { - visibility: visible !important; -} - .cdk-drag-preview { margin-left: 0; box-sizing: border-box; @@ -54,3 +31,25 @@ :host ::ng-deep .larger-tooltip .tooltip-inner { max-width: 500px; } + +.table-border { + border: 1px solid #dee2e6; +} + +:host ::ng-deep .pagination { + padding-top: 0.5rem; +} + +.scrollable-table { + overflow-x: auto; +} + +.disabled-overlay { + opacity: 0.6; +} + +.loading-overlay { + position: fixed; + top: 50%; + left: 50%; +} diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.spec.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.spec.ts index 6fcb2fab843..874107e6de1 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.spec.ts +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.spec.ts @@ -7,6 +7,7 @@ import { TestBed, waitForAsync, } from '@angular/core/testing'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { ActivatedRoute, Router, @@ -15,7 +16,6 @@ import { TranslateModule } from '@ngx-translate/core'; import { of as observableOf } from 'rxjs'; import { ObjectCacheService } from '../../../core/cache/object-cache.service'; -import { RestResponse } from '../../../core/cache/response.models'; import { BitstreamDataService } from '../../../core/data/bitstream-data.service'; import { BundleDataService } from '../../../core/data/bundle-data.service'; import { ItemDataService } from '../../../core/data/item-data.service'; @@ -44,8 +44,12 @@ import { createPaginatedList } from '../../../shared/testing/utils.test'; import { ObjectValuesPipe } from '../../../shared/utils/object-values-pipe'; import { VarDirective } from '../../../shared/utils/var.directive'; import { ItemBitstreamsComponent } from './item-bitstreams.component'; +import { ItemBitstreamsService } from './item-bitstreams.service'; +import { + getItemBitstreamsServiceStub, + ItemBitstreamsServiceStub, +} from './item-bitstreams.service.stub'; import { ItemEditBitstreamBundleComponent } from './item-edit-bitstream-bundle/item-edit-bitstream-bundle.component'; -import { ItemEditBitstreamDragHandleComponent } from './item-edit-bitstream-drag-handle/item-edit-bitstream-drag-handle.component'; let comp: ItemBitstreamsComponent; let fixture: ComponentFixture; @@ -97,6 +101,7 @@ let objectCache: ObjectCacheService; let requestService: RequestService; let searchConfig: SearchConfigurationService; let bundleService: BundleDataService; +let itemBitstreamsService: ItemBitstreamsServiceStub; describe('ItemBitstreamsComponent', () => { beforeEach(waitForAsync(() => { @@ -165,11 +170,19 @@ describe('ItemBitstreamsComponent', () => { url: url, }); bundleService = jasmine.createSpyObj('bundleService', { - patch: observableOf(new RestResponse(true, 200, 'OK')), + patch: createSuccessfulRemoteDataObject$({}), }); + itemBitstreamsService = getItemBitstreamsServiceStub(); + TestBed.configureTestingModule({ - imports: [TranslateModule.forRoot(), ItemBitstreamsComponent, ObjectValuesPipe, VarDirective], + imports: [ + TranslateModule.forRoot(), + ItemBitstreamsComponent, + ObjectValuesPipe, + VarDirective, + BrowserAnimationsModule, + ], providers: [ { provide: ItemDataService, useValue: itemService }, { provide: ObjectUpdatesService, useValue: objectUpdatesService }, @@ -181,6 +194,7 @@ describe('ItemBitstreamsComponent', () => { { provide: RequestService, useValue: requestService }, { provide: SearchConfigurationService, useValue: searchConfig }, { provide: BundleDataService, useValue: bundleService }, + { provide: ItemBitstreamsService, useValue: itemBitstreamsService }, ChangeDetectorRef, ], schemas: [ NO_ERRORS_SCHEMA, @@ -189,7 +203,6 @@ describe('ItemBitstreamsComponent', () => { .overrideComponent(ItemBitstreamsComponent, { remove: { imports: [ItemEditBitstreamBundleComponent, - ItemEditBitstreamDragHandleComponent, ThemedLoadingComponent], }, }) @@ -209,28 +222,8 @@ describe('ItemBitstreamsComponent', () => { comp.submit(); }); - it('should call removeMultiple on the bitstreamService for the marked field', () => { - expect(bitstreamService.removeMultiple).toHaveBeenCalledWith([bitstream2]); - }); - - it('should not call removeMultiple on the bitstreamService for the unmarked field', () => { - expect(bitstreamService.removeMultiple).not.toHaveBeenCalledWith([bitstream1]); - }); - }); - - describe('when dropBitstream is called', () => { - beforeEach((done) => { - comp.dropBitstream(bundle, { - fromIndex: 0, - toIndex: 50, - finish: () => { - done(); - }, - }); - }); - - it('should send out a patch for the move operation', () => { - expect(bundleService.patch).toHaveBeenCalled(); + it('should call removeMarkedBitstreams on the itemBitstreamsService', () => { + expect(itemBitstreamsService.removeMarkedBitstreams).toHaveBeenCalled(); }); }); @@ -247,4 +240,114 @@ describe('ItemBitstreamsComponent', () => { expect(objectUpdatesService.reinstateFieldUpdates).toHaveBeenCalledWith(bundle.self); }); }); + + describe('moveUp', () => { + it('should move the selected bitstream up', () => { + itemBitstreamsService.hasSelectedBitstream.and.returnValue(true); + + const event = { + preventDefault: () => {/* Intentionally empty */}, + } as KeyboardEvent; + comp.moveUp(event); + + expect(itemBitstreamsService.moveSelectedBitstreamUp).toHaveBeenCalled(); + }); + + it('should not do anything if no bitstream is selected', () => { + itemBitstreamsService.hasSelectedBitstream.and.returnValue(false); + + const event = { + preventDefault: () => {/* Intentionally empty */}, + } as KeyboardEvent; + comp.moveUp(event); + + expect(itemBitstreamsService.moveSelectedBitstreamUp).not.toHaveBeenCalled(); + }); + }); + + describe('moveDown', () => { + it('should move the selected bitstream down', () => { + itemBitstreamsService.hasSelectedBitstream.and.returnValue(true); + + const event = { + preventDefault: () => {/* Intentionally empty */}, + } as KeyboardEvent; + comp.moveDown(event); + + expect(itemBitstreamsService.moveSelectedBitstreamDown).toHaveBeenCalled(); + }); + + it('should not do anything if no bitstream is selected', () => { + itemBitstreamsService.hasSelectedBitstream.and.returnValue(false); + + const event = { + preventDefault: () => {/* Intentionally empty */}, + } as KeyboardEvent; + comp.moveDown(event); + + expect(itemBitstreamsService.moveSelectedBitstreamDown).not.toHaveBeenCalled(); + }); + }); + + describe('cancelSelection', () => { + it('should cancel the selection', () => { + itemBitstreamsService.hasSelectedBitstream.and.returnValue(true); + + const event = { + preventDefault: () => {/* Intentionally empty */}, + } as KeyboardEvent; + comp.cancelSelection(event); + + expect(itemBitstreamsService.cancelSelection).toHaveBeenCalled(); + }); + + it('should not do anything if no bitstream is selected', () => { + itemBitstreamsService.hasSelectedBitstream.and.returnValue(false); + + const event = { + preventDefault: () => {/* Intentionally empty */}, + } as KeyboardEvent; + comp.cancelSelection(event); + + expect(itemBitstreamsService.cancelSelection).not.toHaveBeenCalled(); + }); + }); + + describe('clearSelection', () => { + it('should clear the selection', () => { + itemBitstreamsService.hasSelectedBitstream.and.returnValue(true); + + const event = { + target: document.createElement('BODY'), + preventDefault: () => {/* Intentionally empty */}, + } as unknown as KeyboardEvent; + comp.clearSelection(event); + + expect(itemBitstreamsService.clearSelection).toHaveBeenCalled(); + }); + + it('should not do anything if no bitstream is selected', () => { + itemBitstreamsService.hasSelectedBitstream.and.returnValue(false); + + const event = { + target: document.createElement('BODY'), + preventDefault: () => {/* Intentionally empty */}, + } as unknown as KeyboardEvent; + comp.clearSelection(event); + + expect(itemBitstreamsService.clearSelection).not.toHaveBeenCalled(); + }); + + it('should not do anything if the event target is not \'BODY\'', () => { + itemBitstreamsService.hasSelectedBitstream.and.returnValue(true); + + const event = { + target: document.createElement('NOT-BODY'), + preventDefault: () => {/* Intentionally empty */}, + } as unknown as KeyboardEvent; + comp.clearSelection(event); + + expect(itemBitstreamsService.clearSelection).not.toHaveBeenCalled(); + }); + }); }); diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.ts index d96699d3c21..143723d447d 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.ts +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.ts @@ -1,11 +1,8 @@ -import { - AsyncPipe, - NgForOf, - NgIf, -} from '@angular/common'; +import { CommonModule } from '@angular/common'; import { ChangeDetectorRef, Component, + HostListener, NgZone, OnDestroy, } from '@angular/core'; @@ -18,14 +15,12 @@ import { TranslateModule, TranslateService, } from '@ngx-translate/core'; -import { Operation } from 'fast-json-patch'; import { + combineLatest, Observable, Subscription, - zip as observableZip, } from 'rxjs'; import { - filter, map, switchMap, take, @@ -35,49 +30,40 @@ import { ObjectCacheService } from '../../../core/cache/object-cache.service'; import { BitstreamDataService } from '../../../core/data/bitstream-data.service'; import { BundleDataService } from '../../../core/data/bundle-data.service'; import { ItemDataService } from '../../../core/data/item-data.service'; -import { FieldChangeType } from '../../../core/data/object-updates/field-change-type.model'; -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 { PaginatedList } from '../../../core/data/paginated-list.model'; import { RemoteData } from '../../../core/data/remote-data'; import { RequestService } from '../../../core/data/request.service'; -import { Bitstream } from '../../../core/shared/bitstream.model'; import { Bundle } from '../../../core/shared/bundle.model'; import { NoContent } from '../../../core/shared/NoContent.model'; import { getFirstSucceededRemoteData, getRemoteDataPayload, } from '../../../core/shared/operators'; -import { - hasValue, - isNotEmpty, -} from '../../../shared/empty.util'; +import { AlertComponent } from '../../../shared/alert/alert.component'; +import { AlertType } from '../../../shared/alert/alert-type'; import { ThemedLoadingComponent } from '../../../shared/loading/themed-loading.component'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; -import { ResponsiveColumnSizes } from '../../../shared/responsive-table-sizes/responsive-column-sizes'; import { ResponsiveTableSizes } from '../../../shared/responsive-table-sizes/responsive-table-sizes'; import { PaginatedSearchOptions } from '../../../shared/search/models/paginated-search-options.model'; import { ObjectValuesPipe } from '../../../shared/utils/object-values-pipe'; import { VarDirective } from '../../../shared/utils/var.directive'; import { AbstractItemUpdateComponent } from '../abstract-item-update/abstract-item-update.component'; +import { ItemBitstreamsService } from './item-bitstreams.service'; import { ItemEditBitstreamBundleComponent } from './item-edit-bitstream-bundle/item-edit-bitstream-bundle.component'; -import { ItemEditBitstreamDragHandleComponent } from './item-edit-bitstream-drag-handle/item-edit-bitstream-drag-handle.component'; @Component({ selector: 'ds-item-bitstreams', styleUrls: ['./item-bitstreams.component.scss'], templateUrl: './item-bitstreams.component.html', imports: [ - AsyncPipe, + CommonModule, TranslateModule, ItemEditBitstreamBundleComponent, RouterLink, - NgIf, VarDirective, - ItemEditBitstreamDragHandleComponent, - NgForOf, ThemedLoadingComponent, + AlertComponent, ], providers: [ObjectValuesPipe], standalone: true, @@ -87,33 +73,18 @@ import { ItemEditBitstreamDragHandleComponent } from './item-edit-bitstream-drag */ export class ItemBitstreamsComponent extends AbstractItemUpdateComponent implements OnDestroy { + // Declared for use in template + protected readonly AlertType = AlertType; + /** * The currently listed bundles */ bundles$: Observable; - /** - * The page options to use for fetching the bundles - */ - bundlesOptions = { - id: 'bundles-pagination-options', - currentPage: 1, - pageSize: 9999, - } as any; - /** * The bootstrap sizes used for the columns within this table */ - columnSizes = new ResponsiveTableSizes([ - // Name column - new ResponsiveColumnSizes(2, 2, 3, 4, 4), - // Description column - new ResponsiveColumnSizes(2, 3, 3, 3, 3), - // Format column - new ResponsiveColumnSizes(2, 2, 2, 2, 2), - // Actions column - new ResponsiveColumnSizes(6, 5, 4, 3, 3), - ]); + columnSizes: ResponsiveTableSizes; /** * Are we currently submitting the changes? @@ -127,6 +98,11 @@ export class ItemBitstreamsComponent extends AbstractItemUpdateComponent impleme */ itemUpdateSubscription: Subscription; + /** + * An observable which emits a boolean which represents whether the service is currently handling a 'move' request + */ + isProcessingMoveRequest: Observable; + constructor( public itemService: ItemDataService, public objectUpdatesService: ObjectUpdatesService, @@ -140,21 +116,82 @@ export class ItemBitstreamsComponent extends AbstractItemUpdateComponent impleme public cdRef: ChangeDetectorRef, public bundleService: BundleDataService, public zone: NgZone, + public itemBitstreamsService: ItemBitstreamsService, ) { super(itemService, objectUpdatesService, router, notificationsService, translateService, route); + + this.columnSizes = this.itemBitstreamsService.getColumnSizes(); } /** * Actions to perform after the item has been initialized */ postItemInit(): void { - this.bundles$ = this.itemService.getBundles(this.item.id, new PaginatedSearchOptions({ pagination: this.bundlesOptions })).pipe( + const bundlesOptions = this.itemBitstreamsService.getInitialBundlesPaginationOptions(); + this.isProcessingMoveRequest = this.itemBitstreamsService.getPerformingMoveRequest$(); + + this.bundles$ = this.itemService.getBundles(this.item.id, new PaginatedSearchOptions({ pagination: bundlesOptions })).pipe( getFirstSucceededRemoteData(), getRemoteDataPayload(), map((bundlePage: PaginatedList) => bundlePage.page), ); } + /** + * Handles keyboard events that should move the currently selected bitstream up + */ + @HostListener('document:keydown.arrowUp', ['$event']) + moveUp(event: KeyboardEvent) { + if (this.itemBitstreamsService.hasSelectedBitstream()) { + event.preventDefault(); + this.itemBitstreamsService.moveSelectedBitstreamUp(); + } + } + + /** + * Handles keyboard events that should move the currently selected bitstream down + */ + @HostListener('document:keydown.arrowDown', ['$event']) + moveDown(event: KeyboardEvent) { + if (this.itemBitstreamsService.hasSelectedBitstream()) { + event.preventDefault(); + this.itemBitstreamsService.moveSelectedBitstreamDown(); + } + } + + /** + * Handles keyboard events that should cancel the currently selected bitstream. + * A cancel means that the selected bitstream is returned to its original position and is no longer selected. + * @param event + */ + @HostListener('document:keyup.escape', ['$event']) + cancelSelection(event: KeyboardEvent) { + if (this.itemBitstreamsService.hasSelectedBitstream()) { + event.preventDefault(); + this.itemBitstreamsService.cancelSelection(); + } + } + + /** + * Handles keyboard events that should clear the currently selected bitstream. + * A clear means that the selected bitstream remains in its current position but is no longer selected. + */ + @HostListener('document:keydown.enter', ['$event']) + @HostListener('document:keydown.space', ['$event']) + clearSelection(event: KeyboardEvent) { + // Only when no specific element is in focus do we want to clear the currently selected bitstream + // Otherwise we might clear the selection when a different action was intended, e.g. clicking a button or selecting + // a different bitstream. + if ( + this.itemBitstreamsService.hasSelectedBitstream() && + event.target instanceof Element && + event.target.tagName === 'BODY' + ) { + event.preventDefault(); + this.itemBitstreamsService.clearSelection(); + } + } + /** * Initialize the notification messages prefix */ @@ -170,84 +207,16 @@ export class ItemBitstreamsComponent extends AbstractItemUpdateComponent impleme */ submit() { this.submitting = true; - const bundlesOnce$ = this.bundles$.pipe(take(1)); - - // Fetch all removed bitstreams from the object update service - const removedBitstreams$ = bundlesOnce$.pipe( - switchMap((bundles: Bundle[]) => observableZip( - ...bundles.map((bundle: Bundle) => this.objectUpdatesService.getFieldUpdates(bundle.self, [], true)), - )), - map((fieldUpdates: FieldUpdates[]) => ([] as FieldUpdate[]).concat( - ...fieldUpdates.map((updates: FieldUpdates) => Object.values(updates).filter((fieldUpdate: FieldUpdate) => fieldUpdate.changeType === FieldChangeType.REMOVE)), - )), - map((fieldUpdates: FieldUpdate[]) => fieldUpdates.map((fieldUpdate: FieldUpdate) => fieldUpdate.field)), - ); - // Send out delete requests for all deleted bitstreams - const removedResponses$: Observable> = removedBitstreams$.pipe( - take(1), - switchMap((removedBitstreams: Bitstream[]) => { - return this.bitstreamService.removeMultiple(removedBitstreams); - }), - ); + const removedResponses$ = this.itemBitstreamsService.removeMarkedBitstreams(this.bundles$); // Perform the setup actions from above in order and display notifications removedResponses$.subscribe((responses: RemoteData) => { - this.displayNotifications('item.edit.bitstreams.notifications.remove', [responses]); + this.itemBitstreamsService.displayNotifications('item.edit.bitstreams.notifications.remove', [responses]); this.submitting = false; }); } - /** - * A bitstream was dropped in a new location. Send out a Move Patch request to the REST API, display notifications, - * refresh the bundle's cache (so the lists can properly reload) and call the event's callback function (which will - * navigate the user to the correct page) - * @param bundle The bundle to send patch requests to - * @param event The event containing the index the bitstream came from and was dropped to - */ - dropBitstream(bundle: Bundle, event: any) { - this.zone.runOutsideAngular(() => { - if (hasValue(event) && hasValue(event.fromIndex) && hasValue(event.toIndex) && hasValue(event.finish)) { - const moveOperation = { - op: 'move', - from: `/_links/bitstreams/${event.fromIndex}/href`, - path: `/_links/bitstreams/${event.toIndex}/href`, - } as Operation; - this.bundleService.patch(bundle, [moveOperation]).pipe(take(1)).subscribe((response: RemoteData) => { - this.zone.run(() => { - this.displayNotifications('item.edit.bitstreams.notifications.move', [response]); - // Remove all cached requests from this bundle and call the event's callback when the requests are cleared - this.requestService.removeByHrefSubstring(bundle.self).pipe( - filter((isCached) => isCached), - take(1), - ).subscribe(() => event.finish()); - }); - }); - } - }); - } - - /** - * Display notifications - * - Error notification for each failed response with their message - * - Success notification in case there's at least one successful response - * @param key The i18n key for the notification messages - * @param responses The returned responses to display notifications for - */ - displayNotifications(key: string, responses: RemoteData[]) { - if (isNotEmpty(responses)) { - const failedResponses = responses.filter((response: RemoteData) => hasValue(response) && response.hasFailed); - const successfulResponses = responses.filter((response: RemoteData) => hasValue(response) && response.hasSucceeded); - - failedResponses.forEach((response: RemoteData) => { - this.notificationsService.error(this.translateService.instant(`${key}.failed.title`), response.errorMessage); - }); - if (successfulResponses.length > 0) { - this.notificationsService.success(this.translateService.instant(`${key}.saved.title`), this.translateService.instant(`${key}.saved.content`)); - } - } - } - /** * Request the object updates service to discard all current changes to this item * Shows a notification to remind the user that they can undo this @@ -273,7 +242,7 @@ export class ItemBitstreamsComponent extends AbstractItemUpdateComponent impleme */ isReinstatable(): Observable { return this.bundles$.pipe( - switchMap((bundles: Bundle[]) => observableZip(...bundles.map((bundle: Bundle) => this.objectUpdatesService.isReinstatable(bundle.self)))), + switchMap((bundles: Bundle[]) => combineLatest(bundles.map((bundle: Bundle) => this.objectUpdatesService.isReinstatable(bundle.self)))), map((reinstatable: boolean[]) => reinstatable.includes(true)), ); } @@ -283,7 +252,7 @@ export class ItemBitstreamsComponent extends AbstractItemUpdateComponent impleme */ hasChanges(): Observable { return this.bundles$.pipe( - switchMap((bundles: Bundle[]) => observableZip(...bundles.map((bundle: Bundle) => this.objectUpdatesService.hasUpdates(bundle.self)))), + switchMap((bundles: Bundle[]) => combineLatest(bundles.map((bundle: Bundle) => this.objectUpdatesService.hasUpdates(bundle.self)))), map((hasChanges: boolean[]) => hasChanges.includes(true)), ); } diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.spec.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.spec.ts new file mode 100644 index 00000000000..490897b22a5 --- /dev/null +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.spec.ts @@ -0,0 +1,712 @@ +import { + fakeAsync, + tick, +} from '@angular/core/testing'; +import { TranslateService } from '@ngx-translate/core'; +import { of } from 'rxjs'; + +import { DSONameService } from '../../../core/breadcrumbs/dso-name.service'; +import { BitstreamDataService } from '../../../core/data/bitstream-data.service'; +import { BundleDataService } from '../../../core/data/bundle-data.service'; +import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service'; +import { ObjectUpdatesServiceStub } from '../../../core/data/object-updates/object-updates.service.stub'; +import { RequestService } from '../../../core/data/request.service'; +import { Bitstream } from '../../../core/shared/bitstream.model'; +import { BitstreamFormat } from '../../../core/shared/bitstream-format.model'; +import { Bundle } from '../../../core/shared/bundle.model'; +import { LiveRegionService } from '../../../shared/live-region/live-region.service'; +import { getLiveRegionServiceStub } from '../../../shared/live-region/live-region.service.stub'; +import { DSONameServiceMock } from '../../../shared/mocks/dso-name.service.mock'; +import { getMockTranslateService } from '../../../shared/mocks/translate.service.mock'; +import { NotificationsService } from '../../../shared/notifications/notifications.service'; +import { + createFailedRemoteDataObject, + createSuccessfulRemoteDataObject, + createSuccessfulRemoteDataObject$, +} from '../../../shared/remote-data.utils'; +import { BitstreamDataServiceStub } from '../../../shared/testing/bitstream-data-service.stub'; +import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub'; +import { + ItemBitstreamsService, + SelectedBitstreamTableEntry, +} from './item-bitstreams.service'; +import createSpy = jasmine.createSpy; +import { MoveOperation } from 'fast-json-patch'; + +describe('ItemBitstreamsService', () => { + let service: ItemBitstreamsService; + let notificationsService: NotificationsService; + let translateService: TranslateService; + let objectUpdatesService: ObjectUpdatesService; + let bitstreamDataService: BitstreamDataService; + let bundleDataService: BundleDataService; + let dsoNameService: DSONameService; + let requestService: RequestService; + let liveRegionService: LiveRegionService; + + beforeEach(() => { + notificationsService = new NotificationsServiceStub() as any; + translateService = getMockTranslateService(); + objectUpdatesService = new ObjectUpdatesServiceStub() as any; + bitstreamDataService = new BitstreamDataServiceStub() as any; + bundleDataService = jasmine.createSpyObj('bundleDataService', { + patch: createSuccessfulRemoteDataObject$(new Bundle()), + }); + dsoNameService = new DSONameServiceMock() as any; + requestService = jasmine.createSpyObj('requestService', { + setStaleByHrefSubstring: of(true), + }); + liveRegionService = getLiveRegionServiceStub(); + + service = new ItemBitstreamsService( + notificationsService, + translateService, + objectUpdatesService, + bitstreamDataService, + bundleDataService, + dsoNameService, + requestService, + liveRegionService, + ); + }); + + const defaultEntry: SelectedBitstreamTableEntry = { + bitstream: { + name: 'bitstream name', + } as any, + bundle: Object.assign(new Bundle(), { + _links: { self: { href: 'self_link' } }, + }), + bundleSize: 10, + currentPosition: 0, + originalPosition: 0, + }; + + describe('selectBitstreamEntry', () => { + it('should correctly make getSelectedBitstream$ emit', fakeAsync(() => { + const emittedActions = []; + + service.getSelectionAction$().subscribe(selected => emittedActions.push(selected)); + + expect(emittedActions.length).toBe(1); + expect(emittedActions[0]).toBeNull(); + + const entry = Object.assign({}, defaultEntry); + + service.selectBitstreamEntry(entry); + tick(); + + expect(emittedActions.length).toBe(2); + expect(emittedActions[1]).toEqual({ action: 'Selected', selectedEntry: entry }); + })); + + it('should correctly make getSelectedBitstream return the bitstream', () => { + expect(service.getSelectedBitstream()).toBeNull(); + + const entry = Object.assign({}, defaultEntry); + + service.selectBitstreamEntry(entry); + expect(service.getSelectedBitstream()).toEqual(entry); + }); + + it('should correctly make hasSelectedBitstream return', () => { + expect(service.hasSelectedBitstream()).toBeFalse(); + + const entry = Object.assign({}, defaultEntry); + + service.selectBitstreamEntry(entry); + expect(service.hasSelectedBitstream()).toBeTrue(); + }); + + it('should do nothing if no entry was provided', fakeAsync(() => { + const emittedActions = []; + + service.getSelectionAction$().subscribe(selected => emittedActions.push(selected)); + + expect(emittedActions.length).toBe(1); + expect(emittedActions[0]).toBeNull(); + + const entry = Object.assign({}, defaultEntry); + + service.selectBitstreamEntry(entry); + tick(); + + expect(emittedActions.length).toBe(2); + expect(emittedActions[1]).toEqual({ action: 'Selected', selectedEntry: entry }); + + service.selectBitstreamEntry(null); + tick(); + + expect(emittedActions.length).toBe(2); + expect(emittedActions[1]).toEqual({ action: 'Selected', selectedEntry: entry }); + })); + + it('should announce the selected bitstream', () => { + const entry = Object.assign({}, defaultEntry); + + spyOn(service, 'announceSelect'); + + service.selectBitstreamEntry(entry); + expect(service.announceSelect).toHaveBeenCalledWith(entry.bitstream.name); + }); + }); + + describe('clearSelection', () => { + it('should clear the selected bitstream', fakeAsync(() => { + const emittedActions = []; + + service.getSelectionAction$().subscribe(selected => emittedActions.push(selected)); + + expect(emittedActions.length).toBe(1); + expect(emittedActions[0]).toBeNull(); + + const entry = Object.assign({}, defaultEntry); + + service.selectBitstreamEntry(entry); + tick(); + + expect(emittedActions.length).toBe(2); + expect(emittedActions[1]).toEqual({ action: 'Selected', selectedEntry: entry }); + + service.clearSelection(); + tick(); + + expect(emittedActions.length).toBe(3); + expect(emittedActions[2]).toEqual({ action: 'Cleared', selectedEntry: entry }); + })); + + it('should not do anything if there is no selected bitstream', fakeAsync(() => { + const emittedActions = []; + + service.getSelectionAction$().subscribe(selected => emittedActions.push(selected)); + + expect(emittedActions.length).toBe(1); + expect(emittedActions[0]).toBeNull(); + + service.clearSelection(); + tick(); + + expect(emittedActions.length).toBe(1); + expect(emittedActions[0]).toBeNull(); + })); + + it('should announce the cleared bitstream', () => { + const entry = Object.assign({}, defaultEntry); + + spyOn(service, 'announceClear'); + service.selectBitstreamEntry(entry); + service.clearSelection(); + + expect(service.announceClear).toHaveBeenCalledWith(entry.bitstream.name); + }); + + it('should display a notification if the selected bitstream was moved', () => { + const entry = Object.assign({}, defaultEntry, + { + originalPosition: 5, + currentPosition: 7, + }, + ); + + spyOn(service, 'displaySuccessNotification'); + service.selectBitstreamEntry(entry); + service.clearSelection(); + + expect(service.displaySuccessNotification).toHaveBeenCalled(); + }); + + it('should not display a notification if the selected bitstream is in its original position', () => { + const entry = Object.assign({}, defaultEntry, + { + originalPosition: 7, + currentPosition: 7, + }, + ); + + spyOn(service, 'displaySuccessNotification'); + service.selectBitstreamEntry(entry); + service.clearSelection(); + + expect(service.displaySuccessNotification).not.toHaveBeenCalled(); + }); + }); + + describe('cancelSelection', () => { + it('should clear the selected bitstream if it has not moved', fakeAsync(() => { + const emittedActions = []; + + service.getSelectionAction$().subscribe(selected => emittedActions.push(selected)); + + expect(emittedActions.length).toBe(1); + expect(emittedActions[0]).toBeNull(); + + const entry = Object.assign({}, defaultEntry); + + service.selectBitstreamEntry(entry); + tick(); + + expect(emittedActions.length).toBe(2); + expect(emittedActions[1]).toEqual({ action: 'Selected', selectedEntry: entry }); + + service.cancelSelection(); + tick(); + + expect(emittedActions.length).toBe(3); + expect(emittedActions[2]).toEqual({ action: 'Cleared', selectedEntry: entry }); + })); + + it('should cancel the selected bitstream if it has moved', fakeAsync(() => { + const emittedActions = []; + + service.getSelectionAction$().subscribe(selected => emittedActions.push(selected)); + + expect(emittedActions.length).toBe(1); + expect(emittedActions[0]).toBeNull(); + + const entry = Object.assign({}, defaultEntry, { + originalPosition: 0, + currentPosition: 3, + }); + + service.selectBitstreamEntry(entry); + tick(); + + expect(emittedActions.length).toBe(2); + expect(emittedActions[1]).toEqual({ action: 'Selected', selectedEntry: entry }); + + service.cancelSelection(); + tick(); + + expect(emittedActions.length).toBe(3); + expect(emittedActions[2]).toEqual({ action: 'Cancelled', selectedEntry: entry }); + })); + + it('should announce a clear if the bitstream has not moved', () => { + const entry = Object.assign({}, defaultEntry, + { + originalPosition: 7, + currentPosition: 7, + }, + ); + + spyOn(service, 'announceClear'); + spyOn(service, 'announceCancel'); + + service.selectBitstreamEntry(entry); + service.cancelSelection(); + + expect(service.announceClear).toHaveBeenCalledWith(entry.bitstream.name); + expect(service.announceCancel).not.toHaveBeenCalled(); + }); + + it('should announce a cancel if the bitstream has moved', () => { + const entry = Object.assign({}, defaultEntry, + { + originalPosition: 5, + currentPosition: 7, + }, + ); + + spyOn(service, 'announceClear'); + spyOn(service, 'announceCancel'); + + service.selectBitstreamEntry(entry); + service.cancelSelection(); + + expect(service.announceClear).not.toHaveBeenCalled(); + expect(service.announceCancel).toHaveBeenCalledWith(entry.bitstream.name, entry.originalPosition); + }); + + it('should return the bitstream to its original position if it has moved', () => { + const entry = Object.assign({}, defaultEntry, + { + originalPosition: 5, + currentPosition: 7, + }, + ); + + spyOn(service, 'performBitstreamMoveRequest'); + + service.selectBitstreamEntry(entry); + service.cancelSelection(); + + expect(service.performBitstreamMoveRequest).toHaveBeenCalledWith(entry.bundle, entry.currentPosition, entry.originalPosition); + }); + + it('should not move the bitstream if it has not moved', () => { + const entry = Object.assign({}, defaultEntry, + { + originalPosition: 7, + currentPosition: 7, + }, + ); + + spyOn(service, 'performBitstreamMoveRequest'); + + service.selectBitstreamEntry(entry); + service.cancelSelection(); + + expect(service.performBitstreamMoveRequest).not.toHaveBeenCalled(); + }); + + it('should not do anything if there is no selected bitstream', () => { + spyOn(service, 'announceClear'); + spyOn(service, 'announceCancel'); + spyOn(service, 'performBitstreamMoveRequest'); + + service.cancelSelection(); + + expect(service.announceClear).not.toHaveBeenCalled(); + expect(service.announceCancel).not.toHaveBeenCalled(); + expect(service.performBitstreamMoveRequest).not.toHaveBeenCalled(); + }); + }); + + describe('moveSelectedBitstream', () => { + beforeEach(() => { + spyOn(service, 'performBitstreamMoveRequest').and.callThrough(); + }); + + describe('up', () => { + it('should move the selected bitstream one position up', () => { + const startPosition = 7; + const endPosition = startPosition - 1; + + const entry = Object.assign({}, defaultEntry, + { + originalPosition: 5, + currentPosition: startPosition, + }, + ); + + const movedEntry = Object.assign({}, defaultEntry, + { + originalPosition: 5, + currentPosition: endPosition, + }, + ); + + service.selectBitstreamEntry(entry); + service.moveSelectedBitstreamUp(); + expect(service.performBitstreamMoveRequest).toHaveBeenCalledWith(entry.bundle, startPosition, endPosition, jasmine.any(Function)); + expect(service.getSelectedBitstream()).toEqual(movedEntry); + }); + + it('should emit the move', fakeAsync(() => { + const emittedActions = []; + + service.getSelectionAction$().subscribe(selected => emittedActions.push(selected)); + + expect(emittedActions.length).toBe(1); + expect(emittedActions[0]).toBeNull(); + + const startPosition = 7; + const endPosition = startPosition - 1; + + const entry = Object.assign({}, defaultEntry, + { + originalPosition: 5, + currentPosition: startPosition, + }, + ); + + const movedEntry = Object.assign({}, defaultEntry, + { + originalPosition: 5, + currentPosition: endPosition, + }, + ); + + service.selectBitstreamEntry(entry); + tick(); + + expect(emittedActions.length).toBe(2); + expect(emittedActions[1]).toEqual({ action: 'Selected', selectedEntry: entry }); + + service.moveSelectedBitstreamUp(); + tick(); + + expect(emittedActions.length).toBe(3); + expect(emittedActions[2]).toEqual({ action: 'Moved', selectedEntry: movedEntry }); + })); + + it('should announce the move', () => { + const startPosition = 7; + const endPosition = startPosition - 1; + + spyOn(service, 'announceMove'); + + const entry = Object.assign({}, defaultEntry, + { + originalPosition: 5, + currentPosition: startPosition, + }, + ); + + service.selectBitstreamEntry(entry); + service.moveSelectedBitstreamUp(); + + expect(service.announceMove).toHaveBeenCalledWith(entry.bitstream.name, endPosition); + }); + + it('should not do anything if the bitstream is already at the top', () => { + const entry = Object.assign({}, defaultEntry, + { + originalPosition: 5, + currentPosition: 0, + }, + ); + + service.selectBitstreamEntry(entry); + service.moveSelectedBitstreamUp(); + + expect(service.performBitstreamMoveRequest).not.toHaveBeenCalled(); + }); + + it('should not do anything if there is no selected bitstream', () => { + service.moveSelectedBitstreamUp(); + + expect(service.performBitstreamMoveRequest).not.toHaveBeenCalled(); + }); + }); + + describe('down', () => { + it('should move the selected bitstream one position down', () => { + const startPosition = 7; + const endPosition = startPosition + 1; + + const entry = Object.assign({}, defaultEntry, + { + originalPosition: 5, + currentPosition: startPosition, + }, + ); + + const movedEntry = Object.assign({}, defaultEntry, + { + originalPosition: 5, + currentPosition: endPosition, + }, + ); + + service.selectBitstreamEntry(entry); + service.moveSelectedBitstreamDown(); + expect(service.performBitstreamMoveRequest).toHaveBeenCalledWith(entry.bundle, startPosition, endPosition, jasmine.any(Function)); + expect(service.getSelectedBitstream()).toEqual(movedEntry); + }); + + it('should emit the move', fakeAsync(() => { + const emittedActions = []; + + service.getSelectionAction$().subscribe(selected => emittedActions.push(selected)); + + expect(emittedActions.length).toBe(1); + expect(emittedActions[0]).toBeNull(); + + const startPosition = 7; + const endPosition = startPosition + 1; + + const entry = Object.assign({}, defaultEntry, + { + originalPosition: 5, + currentPosition: startPosition, + }, + ); + + const movedEntry = Object.assign({}, defaultEntry, + { + originalPosition: 5, + currentPosition: endPosition, + }, + ); + + service.selectBitstreamEntry(entry); + tick(); + + expect(emittedActions.length).toBe(2); + expect(emittedActions[1]).toEqual({ action: 'Selected', selectedEntry: entry }); + + service.moveSelectedBitstreamDown(); + tick(); + + expect(emittedActions.length).toBe(3); + expect(emittedActions[2]).toEqual({ action: 'Moved', selectedEntry: movedEntry }); + })); + + it('should announce the move', () => { + const startPosition = 7; + const endPosition = startPosition + 1; + + spyOn(service, 'announceMove'); + + const entry = Object.assign({}, defaultEntry, + { + originalPosition: 5, + currentPosition: startPosition, + }, + ); + + service.selectBitstreamEntry(entry); + service.moveSelectedBitstreamDown(); + + expect(service.announceMove).toHaveBeenCalledWith(entry.bitstream.name, endPosition); + }); + + it('should not do anything if the bitstream is already at the bottom of the bundle', () => { + const entry = Object.assign({}, defaultEntry, + { + originalPosition: 5, + currentPosition: 9, + }, + ); + + service.selectBitstreamEntry(entry); + service.moveSelectedBitstreamDown(); + + expect(service.performBitstreamMoveRequest).not.toHaveBeenCalled(); + }); + + it('should not do anything if there is no selected bitstream', () => { + service.moveSelectedBitstreamDown(); + + expect(service.performBitstreamMoveRequest).not.toHaveBeenCalled(); + }); + }); + }); + + describe('performBitstreamMoveRequest', () => { + const bundle: Bundle = defaultEntry.bundle; + const from = 5; + const to = 7; + const callback = createSpy('callbackFunction'); + + it('should correctly create the Move request', () => { + const expectedOperation: MoveOperation = { + op: 'move', + from: `/_links/bitstreams/${from}/href`, + path: `/_links/bitstreams/${to}/href`, + }; + + service.performBitstreamMoveRequest(bundle, from, to, callback); + expect(bundleDataService.patch).toHaveBeenCalledWith(bundle, [expectedOperation]); + }); + + it('should correctly make the bundle\'s self link stale', () => { + service.performBitstreamMoveRequest(bundle, from, to, callback); + expect(requestService.setStaleByHrefSubstring).toHaveBeenCalledWith(bundle._links.self.href); + }); + + it('should attempt to show a message should the request have failed', () => { + spyOn(service, 'displayFailedResponseNotifications'); + service.performBitstreamMoveRequest(bundle, from, to, callback); + expect(service.displayFailedResponseNotifications).toHaveBeenCalled(); + }); + + it('should correctly call the provided function once the request has finished', () => { + service.performBitstreamMoveRequest(bundle, from, to, callback); + expect(callback).toHaveBeenCalled(); + }); + + it('should emit at the start and end of the request', fakeAsync(() => { + const emittedActions = []; + + service.getPerformingMoveRequest$().subscribe(selected => emittedActions.push(selected)); + + expect(emittedActions.length).toBe(1); + expect(emittedActions[0]).toBeFalse(); + + service.performBitstreamMoveRequest(bundle, from, to, callback); + tick(); + + expect(emittedActions.length).toBe(3); + expect(emittedActions[1]).toBeTrue(); + expect(emittedActions[2]).toBeFalse(); + })); + }); + + describe('displayNotifications', () => { + it('should display an error notification if a response failed', () => { + const responses = [ + createFailedRemoteDataObject(), + ]; + + const key = 'some.key'; + + service.displayNotifications(key, responses); + + expect(notificationsService.success).not.toHaveBeenCalled(); + expect(notificationsService.error).toHaveBeenCalled(); + expect(translateService.instant).toHaveBeenCalledWith('some.key.failed.title'); + }); + + it('should display a success notification if a response succeeded', () => { + const responses = [ + createSuccessfulRemoteDataObject(undefined), + ]; + + const key = 'some.key'; + + service.displayNotifications(key, responses); + + expect(notificationsService.success).toHaveBeenCalled(); + expect(notificationsService.error).not.toHaveBeenCalled(); + expect(translateService.instant).toHaveBeenCalledWith('some.key.saved.title'); + }); + + it('should display both notifications if some failed and some succeeded', () => { + const responses = [ + createFailedRemoteDataObject(), + createSuccessfulRemoteDataObject(undefined), + ]; + + const key = 'some.key'; + + service.displayNotifications(key, responses); + + expect(notificationsService.success).toHaveBeenCalled(); + expect(notificationsService.error).toHaveBeenCalled(); + expect(translateService.instant).toHaveBeenCalledWith('some.key.saved.title'); + expect(translateService.instant).toHaveBeenCalledWith('some.key.saved.title'); + }); + }); + + describe('mapBitstreamsToTableEntries', () => { + it('should correctly map a Bitstream to a BitstreamTableEntry', () => { + const format: BitstreamFormat = new BitstreamFormat(); + + const bitstream: Bitstream = Object.assign(new Bitstream(), { + uuid: 'testUUID', + format: createSuccessfulRemoteDataObject$(format), + }); + + spyOn(dsoNameService, 'getName').and.returnValue('Test Name'); + spyOn(bitstream, 'firstMetadataValue').and.returnValue('description'); + + const tableEntry = service.mapBitstreamsToTableEntries([bitstream])[0]; + + expect(tableEntry.name).toEqual('Test Name'); + expect(tableEntry.nameStripped).toEqual('TestName'); + expect(tableEntry.bitstream).toBe(bitstream); + expect(tableEntry.id).toEqual('testUUID'); + expect(tableEntry.description).toEqual('description'); + expect(tableEntry.downloadUrl).toEqual('/bitstreams/testUUID/download'); + }); + }); + + describe('nameToHeader', () => { + it('should correctly transform a string to an appropriate header ID', () => { + const stringA = 'Test String'; + const stringAResult = 'TestString'; + expect(service.nameToHeader(stringA)).toEqual(stringAResult); + + const stringB = 'Test String Two'; + const stringBResult = 'TestStringTwo'; + expect(service.nameToHeader(stringB)).toEqual(stringBResult); + + const stringC = 'Test String Three'; + const stringCResult = 'TestStringThree'; + expect(service.nameToHeader(stringC)).toEqual(stringCResult); + }); + }); + +}); diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.stub.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.stub.ts new file mode 100644 index 00000000000..eb99c214ca4 --- /dev/null +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.stub.ts @@ -0,0 +1,79 @@ +import { of } from 'rxjs'; + +import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model'; +import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; +import { ResponsiveColumnSizes } from '../../../shared/responsive-table-sizes/responsive-column-sizes'; +import { ResponsiveTableSizes } from '../../../shared/responsive-table-sizes/responsive-table-sizes'; + +export function getItemBitstreamsServiceStub(): ItemBitstreamsServiceStub { + return new ItemBitstreamsServiceStub(); +} + +export class ItemBitstreamsServiceStub { + getSelectionAction$ = jasmine.createSpy('getSelectedBitstream$').and + .returnValue(of(null)); + + getSelectedBitstream = jasmine.createSpy('getSelectedBitstream').and + .returnValue(null); + + hasSelectedBitstream = jasmine.createSpy('hasSelectedBitstream').and + .returnValue(false); + + selectBitstreamEntry = jasmine.createSpy('selectBitstreamEntry'); + + clearSelection = jasmine.createSpy('clearSelection'); + + cancelSelection = jasmine.createSpy('cancelSelection'); + + moveSelectedBitstreamUp = jasmine.createSpy('moveSelectedBitstreamUp'); + + moveSelectedBitstreamDown = jasmine.createSpy('moveSelectedBitstreamDown'); + + performBitstreamMoveRequest = jasmine.createSpy('performBitstreamMoveRequest'); + + getPerformingMoveRequest = jasmine.createSpy('getPerformingMoveRequest').and.returnValue(false); + + getPerformingMoveRequest$ = jasmine.createSpy('getPerformingMoveRequest$').and.returnValue(of(false)); + + getInitialBundlesPaginationOptions = jasmine.createSpy('getInitialBundlesPaginationOptions').and + .returnValue(new PaginationComponentOptions()); + + getInitialBitstreamsPaginationOptions = jasmine.createSpy('getInitialBitstreamsPaginationOptions').and + .returnValue(new PaginationComponentOptions()); + + getColumnSizes = jasmine.createSpy('getColumnSizes').and + .returnValue( + new ResponsiveTableSizes([ + new ResponsiveColumnSizes(2, 2, 3, 4, 4), + new ResponsiveColumnSizes(2, 3, 3, 3, 3), + new ResponsiveColumnSizes(2, 2, 2, 2, 2), + new ResponsiveColumnSizes(6, 5, 4, 3, 3), + ]), + ); + + displayNotifications = jasmine.createSpy('displayNotifications'); + + displayFailedResponseNotifications = jasmine.createSpy('displayFailedResponseNotifications'); + + displayErrorNotification = jasmine.createSpy('displayErrorNotification'); + + displaySuccessFulResponseNotifications = jasmine.createSpy('displaySuccessFulResponseNotifications'); + + displaySuccessNotification = jasmine.createSpy('displaySuccessNotification'); + + removeMarkedBitstreams = jasmine.createSpy('removeMarkedBitstreams').and + .returnValue(createSuccessfulRemoteDataObject$({})); + + mapBitstreamsToTableEntries = jasmine.createSpy('mapBitstreamsToTableEntries').and + .returnValue([]); + + nameToHeader = jasmine.createSpy('nameToHeader').and.returnValue('header'); + + stripWhiteSpace = jasmine.createSpy('stripWhiteSpace').and.returnValue('string'); + + announceSelect = jasmine.createSpy('announceSelect'); + + announceMove = jasmine.createSpy('announceMove'); + + announceCancel = jasmine.createSpy('announceCancel'); +} diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.ts new file mode 100644 index 00000000000..2221e037fb8 --- /dev/null +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.service.ts @@ -0,0 +1,568 @@ +import { Injectable } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; +import { MoveOperation } from 'fast-json-patch'; +import { + BehaviorSubject, + Observable, + zip as observableZip, +} from 'rxjs'; +import { + map, + switchMap, + take, + tap, +} from 'rxjs/operators'; + +import { getBitstreamDownloadRoute } from '../../../app-routing-paths'; +import { DSONameService } from '../../../core/breadcrumbs/dso-name.service'; +import { BitstreamDataService } from '../../../core/data/bitstream-data.service'; +import { BundleDataService } from '../../../core/data/bundle-data.service'; +import { FieldChangeType } from '../../../core/data/object-updates/field-change-type.model'; +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 { RemoteData } from '../../../core/data/remote-data'; +import { RequestService } from '../../../core/data/request.service'; +import { Bitstream } from '../../../core/shared/bitstream.model'; +import { BitstreamFormat } from '../../../core/shared/bitstream-format.model'; +import { Bundle } from '../../../core/shared/bundle.model'; +import { NoContent } from '../../../core/shared/NoContent.model'; +import { + getFirstCompletedRemoteData, + getFirstSucceededRemoteDataPayload, +} from '../../../core/shared/operators'; +import { + hasNoValue, + hasValue, +} from '../../../shared/empty.util'; +import { LiveRegionService } from '../../../shared/live-region/live-region.service'; +import { NotificationsService } from '../../../shared/notifications/notifications.service'; +import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model'; +import { ResponsiveColumnSizes } from '../../../shared/responsive-table-sizes/responsive-column-sizes'; +import { ResponsiveTableSizes } from '../../../shared/responsive-table-sizes/responsive-table-sizes'; + +export const MOVE_KEY = 'item.edit.bitstreams.notifications.move'; + +/** + * Interface storing all the information necessary to create a row in the bitstream edit table + */ +export interface BitstreamTableEntry { + /** + * The bitstream + */ + bitstream: Bitstream, + /** + * The uuid of the Bitstream + */ + id: string, + /** + * The name of the Bitstream + */ + name: string, + /** + * The name of the Bitstream with all whitespace removed + */ + nameStripped: string, + /** + * The description of the Bitstream + */ + description: string, + /** + * Observable emitting the Format of the Bitstream + */ + format: Observable, + /** + * The download url of the Bitstream + */ + downloadUrl: string, +} + +/** + * Interface storing information necessary to highlight and reorder the selected bitstream entry + */ +export interface SelectedBitstreamTableEntry { + /** + * The selected entry + */ + bitstream: BitstreamTableEntry, + /** + * The bundle the bitstream belongs to + */ + bundle: Bundle, + /** + * The total number of bitstreams in the bundle + */ + bundleSize: number, + /** + * The original position of the bitstream within the bundle. + */ + originalPosition: number, + /** + * The current position of the bitstream within the bundle. + */ + currentPosition: number, +} + +/** + * Interface storing data regarding a change in selected bitstream + */ +export interface SelectionAction { + /** + * The different types of actions: + * - Selected: Bitstream was selected + * - Moved: Bitstream was moved + * - Cleared: Selection was cleared, bitstream remains at its current position + * - Cancelled: Selection was cancelled, bitstream returns to its original position + */ + action: 'Selected' | 'Moved' | 'Cleared' | 'Cancelled' + /** + * The table entry to which the selection action applies + */ + selectedEntry: SelectedBitstreamTableEntry, +} + +/** + * This service handles the selection and updating of the bitstreams and their order on the + * 'Edit Item' -> 'Bitstreams' page. + */ +@Injectable( + { providedIn: 'root' }, +) +export class ItemBitstreamsService { + + /** + * BehaviorSubject which emits every time the selected bitstream changes. + */ + protected selectionAction$: BehaviorSubject = new BehaviorSubject(null); + + protected isPerformingMoveRequest: BehaviorSubject = new BehaviorSubject(false); + + constructor( + protected notificationsService: NotificationsService, + protected translateService: TranslateService, + protected objectUpdatesService: ObjectUpdatesService, + protected bitstreamService: BitstreamDataService, + protected bundleService: BundleDataService, + protected dsoNameService: DSONameService, + protected requestService: RequestService, + protected liveRegionService: LiveRegionService, + ) { + } + + /** + * Returns the observable emitting the selection actions + */ + getSelectionAction$(): Observable { + return this.selectionAction$; + } + + /** + * Returns the latest selection action + */ + getSelectionAction(): SelectionAction { + const action = this.selectionAction$.value; + + if (hasNoValue(action)) { + return null; + } + + return Object.assign({}, action); + } + + /** + * Returns true if there currently is a selected bitstream + */ + hasSelectedBitstream(): boolean { + const selectionAction = this.getSelectionAction(); + + if (hasNoValue(selectionAction)) { + return false; + } + + const action = selectionAction.action; + + return action === 'Selected' || action === 'Moved'; + } + + /** + * Returns a copy of the currently selected bitstream + */ + getSelectedBitstream(): SelectedBitstreamTableEntry { + if (!this.hasSelectedBitstream()) { + return null; + } + + const selectionAction = this.getSelectionAction(); + return Object.assign({}, selectionAction.selectedEntry); + } + + /** + * Select the provided entry + */ + selectBitstreamEntry(entry: SelectedBitstreamTableEntry) { + if (hasValue(entry) && entry.bitstream !== this.getSelectedBitstream()?.bitstream) { + this.announceSelect(entry.bitstream.name); + this.updateSelectionAction({ action: 'Selected', selectedEntry: entry }); + } + } + + /** + * Makes the {@link selectionAction$} observable emit the provided {@link SelectedBitstreamTableEntry}. + * @protected + */ + protected updateSelectionAction(action: SelectionAction) { + this.selectionAction$.next(action); + } + + /** + * Unselects the selected bitstream. Does nothing if no bitstream is selected. + */ + clearSelection() { + const selected = this.getSelectedBitstream(); + + if (hasValue(selected)) { + this.updateSelectionAction({ action: 'Cleared', selectedEntry: selected }); + this.announceClear(selected.bitstream.name); + + if (selected.currentPosition !== selected.originalPosition) { + this.displaySuccessNotification(MOVE_KEY); + } + } + } + + /** + * Returns the currently selected bitstream to its original position and unselects the bitstream. + * Does nothing if no bitstream is selected. + */ + cancelSelection() { + const selected = this.getSelectedBitstream(); + + if (hasNoValue(selected) || this.getPerformingMoveRequest()) { + return; + } + + + const originalPosition = selected.originalPosition; + const currentPosition = selected.currentPosition; + + // If the selected bitstream has not moved, there is no need to return it to its original position + if (currentPosition === originalPosition) { + this.announceClear(selected.bitstream.name); + this.updateSelectionAction({ action: 'Cleared', selectedEntry: selected }); + } else { + this.announceCancel(selected.bitstream.name, originalPosition); + this.performBitstreamMoveRequest(selected.bundle, currentPosition, originalPosition); + this.updateSelectionAction({ action: 'Cancelled', selectedEntry: selected }); + } + } + + /** + * Moves the selected bitstream one position up in the bundle. Does nothing if no bitstream is selected or the + * selected bitstream already is at the beginning of the bundle. + */ + moveSelectedBitstreamUp() { + const selected = this.getSelectedBitstream(); + + if (hasNoValue(selected) || this.getPerformingMoveRequest()) { + return; + } + + const originalPosition = selected.currentPosition; + if (originalPosition > 0) { + const newPosition = originalPosition - 1; + selected.currentPosition = newPosition; + + const onRequestCompleted = () => { + this.announceMove(selected.bitstream.name, newPosition); + }; + + this.performBitstreamMoveRequest(selected.bundle, originalPosition, newPosition, onRequestCompleted); + this.updateSelectionAction({ action: 'Moved', selectedEntry: selected }); + } + } + + /** + * Moves the selected bitstream one position down in the bundle. Does nothing if no bitstream is selected or the + * selected bitstream already is at the end of the bundle. + */ + moveSelectedBitstreamDown() { + const selected = this.getSelectedBitstream(); + + if (hasNoValue(selected) || this.getPerformingMoveRequest()) { + return; + } + + const originalPosition = selected.currentPosition; + if (originalPosition < selected.bundleSize - 1) { + const newPosition = originalPosition + 1; + selected.currentPosition = newPosition; + + const onRequestCompleted = () => { + this.announceMove(selected.bitstream.name, newPosition); + }; + + this.performBitstreamMoveRequest(selected.bundle, originalPosition, newPosition, onRequestCompleted); + this.updateSelectionAction({ action: 'Moved', selectedEntry: selected }); + } + } + + /** + * Sends out a Move Patch request to the REST API, display notifications, + * refresh the bundle's cache (so the lists can properly reload) + * @param bundle The bundle to send patch requests to + * @param fromIndex The index to move from + * @param toIndex The index to move to + * @param finish Optional: Function to execute once the response has been received + */ + performBitstreamMoveRequest(bundle: Bundle, fromIndex: number, toIndex: number, finish?: () => void) { + if (this.getPerformingMoveRequest()) { + console.warn('Attempted to perform move request while previous request has not completed yet'); + return; + } + + const moveOperation: MoveOperation = { + op: 'move', + from: `/_links/bitstreams/${fromIndex}/href`, + path: `/_links/bitstreams/${toIndex}/href`, + }; + + this.announceLoading(); + this.isPerformingMoveRequest.next(true); + this.bundleService.patch(bundle, [moveOperation]).pipe( + getFirstCompletedRemoteData(), + tap((response: RemoteData) => this.displayFailedResponseNotifications(MOVE_KEY, [response])), + switchMap(() => this.requestService.setStaleByHrefSubstring(bundle.self)), + take(1), + ).subscribe(() => { + this.isPerformingMoveRequest.next(false); + finish?.(); + }); + } + + /** + * Whether the service currently is processing a 'move' request + */ + getPerformingMoveRequest(): boolean { + return this.isPerformingMoveRequest.value; + } + + /** + * Returns an observable which emits when the service starts, or ends, processing a 'move' request + */ + getPerformingMoveRequest$(): Observable { + return this.isPerformingMoveRequest; + } + + /** + * Returns the pagination options to use when fetching the bundles + */ + getInitialBundlesPaginationOptions(): PaginationComponentOptions { + return Object.assign(new PaginationComponentOptions(), { + id: 'bundles-pagination-options', + currentPage: 1, + pageSize: 9999, + }); + } + + /** + * Returns the initial pagination options to use when fetching the bitstreams + * @param bundleName The name of the bundle, will be as pagination id. + */ + getInitialBitstreamsPaginationOptions(bundleName: string): PaginationComponentOptions { + return Object.assign(new PaginationComponentOptions(),{ + id: bundleName, // This might behave unexpectedly if the item contains two bundles with the same name + currentPage: 1, + pageSize: 10, + }); + } + + /** + * Returns the {@link ResponsiveTableSizes} for use in the columns of the edit bitstreams table + */ + getColumnSizes(): ResponsiveTableSizes { + return new ResponsiveTableSizes([ + // Name column + new ResponsiveColumnSizes(2, 2, 3, 4, 4), + // Description column + new ResponsiveColumnSizes(2, 3, 3, 3, 3), + // Format column + new ResponsiveColumnSizes(2, 2, 2, 2, 2), + // Actions column + new ResponsiveColumnSizes(6, 5, 4, 3, 3), + ]); + } + + /** + * Display notifications + * - Error notification for each failed response with their message + * - Success notification in case there's at least one successful response + * @param key The i18n key for the notification messages + * @param responses The returned responses to display notifications for + */ + displayNotifications(key: string, responses: RemoteData[]) { + this.displayFailedResponseNotifications(key, responses); + this.displaySuccessFulResponseNotifications(key, responses); + } + + /** + * Display an error notification for each failed response with their message + * @param key The i18n key for the notification messages + * @param responses The returned responses to display notifications for + */ + displayFailedResponseNotifications(key: string, responses: RemoteData[]) { + const failedResponses = responses.filter((response: RemoteData) => hasValue(response) && response.hasFailed); + failedResponses.forEach((response: RemoteData) => { + this.displayErrorNotification(key, response.errorMessage); + }); + } + + /** + * Display an error notification with the provided key and message + * @param key The i18n key for the notification messages + * @param errorMessage The error message to display + */ + displayErrorNotification(key: string, errorMessage: string) { + this.notificationsService.error(this.translateService.instant(`${key}.failed.title`), errorMessage); + } + + /** + * Display a success notification in case there's at least one successful response + * @param key The i18n key for the notification messages + * @param responses The returned responses to display notifications for + */ + displaySuccessFulResponseNotifications(key: string, responses: RemoteData[]) { + const successfulResponses = responses.filter((response: RemoteData) => hasValue(response) && response.hasSucceeded); + if (successfulResponses.length > 0) { + this.displaySuccessNotification(key); + } + } + + /** + * Display a success notification with the provided key + * @param key The i18n key for the notification messages + */ + displaySuccessNotification(key: string) { + this.notificationsService.success(this.translateService.instant(`${key}.saved.title`), this.translateService.instant(`${key}.saved.content`)); + } + + /** + * Removes the bitstreams marked for deletion from the Bundles emitted by the provided observable. + * @param bundles$ + */ + removeMarkedBitstreams(bundles$: Observable): Observable> { + const bundlesOnce$ = bundles$.pipe(take(1)); + + // Fetch all removed bitstreams from the object update service + const removedBitstreams$ = bundlesOnce$.pipe( + switchMap((bundles: Bundle[]) => observableZip( + ...bundles.map((bundle: Bundle) => this.objectUpdatesService.getFieldUpdates(bundle.self, [], true)), + )), + map((fieldUpdates: FieldUpdates[]) => ([] as FieldUpdate[]).concat( + ...fieldUpdates.map((updates: FieldUpdates) => Object.values(updates).filter((fieldUpdate: FieldUpdate) => fieldUpdate.changeType === FieldChangeType.REMOVE)), + )), + map((fieldUpdates: FieldUpdate[]) => fieldUpdates.map((fieldUpdate: FieldUpdate) => fieldUpdate.field)), + ); + + // Send out delete requests for all deleted bitstreams + return removedBitstreams$.pipe( + take(1), + switchMap((removedBitstreams: Bitstream[]) => { + return this.bitstreamService.removeMultiple(removedBitstreams); + }), + ); + } + + /** + * Creates an array of {@link BitstreamTableEntry}s from an array of {@link Bitstream}s + * @param bitstreams The bitstreams array to map to table entries + */ + mapBitstreamsToTableEntries(bitstreams: Bitstream[]): BitstreamTableEntry[] { + return bitstreams.map((bitstream) => { + const name = this.dsoNameService.getName(bitstream); + + return { + bitstream: bitstream, + id: bitstream.uuid, + name: name, + nameStripped: this.nameToHeader(name), + description: bitstream.firstMetadataValue('dc.description'), + format: bitstream.format.pipe(getFirstSucceededRemoteDataPayload()), + downloadUrl: getBitstreamDownloadRoute(bitstream), + }; + }); + } + + /** + * Returns a string appropriate to be used as header ID + * @param name + */ + nameToHeader(name: string): string { + // Whitespace is stripped from the Bitstream names for accessibility reasons. + // To make it clear which headers are relevant for a specific field in the table, the 'headers' attribute is used to + // refer to specific headers. The Bitstream's name is used as header ID for the row containing information regarding + // that bitstream. As the 'headers' attribute contains a space-separated string of header IDs, the Bitstream's header + // ID can not contain spaces itself. + return this.stripWhiteSpace(name); + } + + /** + * Returns a string equal to the input string with all whitespace removed. + * @param str + */ + stripWhiteSpace(str: string): string { + // '/\s+/g' matches all occurrences of any amount of whitespace characters + return str.replace(/\s+/g, ''); + } + + /** + * Adds a message to the live region mentioning that the bitstream with the provided name was selected. + * @param bitstreamName The name of the bitstream that was selected. + */ + announceSelect(bitstreamName: string) { + const message = this.translateService.instant('item.edit.bitstreams.edit.live.select', + { bitstream: bitstreamName }); + this.liveRegionService.addMessage(message); + } + + /** + * Adds a message to the live region mentioning that the bitstream with the provided name was moved to the provided + * position. + * @param bitstreamName The name of the bitstream that moved. + * @param toPosition The zero-indexed position that the bitstream moved to. + */ + announceMove(bitstreamName: string, toPosition: number) { + const message = this.translateService.instant('item.edit.bitstreams.edit.live.move', + { bitstream: bitstreamName, toIndex: toPosition + 1 }); + this.liveRegionService.addMessage(message); + } + + /** + * Adds a message to the live region mentioning that the bitstream with the provided name is no longer selected and + * was returned to the provided position. + * @param bitstreamName The name of the bitstream that is no longer selected + * @param toPosition The zero-indexed position the bitstream returned to. + */ + announceCancel(bitstreamName: string, toPosition: number) { + const message = this.translateService.instant('item.edit.bitstreams.edit.live.cancel', + { bitstream: bitstreamName, toIndex: toPosition + 1 }); + this.liveRegionService.addMessage(message); + } + + /** + * Adds a message to the live region mentioning that the bitstream with the provided name is no longer selected. + * @param bitstreamName The name of the bitstream that is no longer selected. + */ + announceClear(bitstreamName: string) { + const message = this.translateService.instant('item.edit.bitstreams.edit.live.clear', + { bitstream: bitstreamName }); + this.liveRegionService.addMessage(message); + } + + /** + * Adds a message to the live region mentioning that the + */ + announceLoading() { + const message = this.translateService.instant('item.edit.bitstreams.edit.live.loading'); + this.liveRegionService.addMessage(message); + } +} diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.html b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.html index 42b9b9eb647..06201b1cbe7 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.html +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.html @@ -1,22 +1,140 @@ -
-
- -
- {{'item.edit.bitstreams.bundle.name' | translate:{ name: dsoNameService.getName(bundle) } }} -
-
-
-
- -
-
-
- + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ {{'item.edit.bitstreams.headers.name' | translate}} + + {{'item.edit.bitstreams.headers.description' | translate}} + + {{'item.edit.bitstreams.headers.format' | translate}} + + {{'item.edit.bitstreams.headers.actions' | translate}} +
+ {{'item.edit.bitstreams.bundle.name' | translate:{ name: bundleName } }} + + +
+ +
+ + + +
+
+ +
+
+ +
+ {{ entry.name }} +
+ {{ entry.description }} + + {{ (entry.format | async)?.shortDescription }} + +
+
+ + + + + + +
+
+
+ +
+
+
diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.scss b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.scss new file mode 100644 index 00000000000..bbd4e1e75ce --- /dev/null +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.scss @@ -0,0 +1,24 @@ +.header-row { + color: var(--bs-table-dark-color); + background-color: var(--bs-table-dark-bg); + border-color: var(--bs-table-dark-bg); +} + +.bundle-row { + color: var(--bs-table-head-color); + background-color: var(--bs-table-head-bg); + border-color: var(--bs-table-border-color); +} + +.row-element { + padding: 0.75em; + border-bottom: var(--bs-table-border-width) solid var(--bs-table-border-color); +} + +.bitstream-name { + font-weight: normal; +} + +.table { + margin-bottom: 0; +} diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.spec.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.spec.ts index d39f9eb265d..bd99b29c476 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.spec.ts +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.spec.ts @@ -1,3 +1,4 @@ +import { CdkDragDrop } from '@angular/cdk/drag-drop'; import { NO_ERRORS_SCHEMA, ViewContainerRef, @@ -8,11 +9,35 @@ import { waitForAsync, } from '@angular/core/testing'; import { TranslateModule } from '@ngx-translate/core'; +import { + of as observableOf, + of, + Subject, +} from 'rxjs'; +import { BundleDataService } from '../../../../core/data/bundle-data.service'; +import { FieldChangeType } from '../../../../core/data/object-updates/field-change-type.model'; +import { FieldUpdate } from '../../../../core/data/object-updates/field-update.model'; +import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service'; +import { RequestService } from '../../../../core/data/request.service'; +import { PaginationService } from '../../../../core/pagination/pagination.service'; import { Bundle } from '../../../../core/shared/bundle.model'; import { Item } from '../../../../core/shared/item.model'; +import { getMockRequestService } from '../../../../shared/mocks/request.service.mock'; +import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils'; import { ResponsiveColumnSizes } from '../../../../shared/responsive-table-sizes/responsive-column-sizes'; import { ResponsiveTableSizes } from '../../../../shared/responsive-table-sizes/responsive-table-sizes'; +import { PaginationServiceStub } from '../../../../shared/testing/pagination-service.stub'; +import { createPaginatedList } from '../../../../shared/testing/utils.test'; +import { + BitstreamTableEntry, + ItemBitstreamsService, + SelectedBitstreamTableEntry, +} from '../item-bitstreams.service'; +import { + getItemBitstreamsServiceStub, + ItemBitstreamsServiceStub, +} from '../item-bitstreams.service.stub'; import { ItemEditBitstreamBundleComponent } from './item-edit-bitstream-bundle.component'; describe('ItemEditBitstreamBundleComponent', () => { @@ -39,9 +64,32 @@ describe('ItemEditBitstreamBundleComponent', () => { }, }); + const restEndpoint = 'fake-rest-endpoint'; + const bundleService = jasmine.createSpyObj('bundleService', { + getBitstreamsEndpoint: observableOf(restEndpoint), + getBitstreams: createSuccessfulRemoteDataObject$(createPaginatedList([])), + }); + + let objectUpdatesService: any; + let itemBitstreamsService: ItemBitstreamsServiceStub; + beforeEach(waitForAsync(() => { + objectUpdatesService = jasmine.createSpyObj('objectUpdatesService', { + initialize: undefined, + getFieldUpdatesExclusive: of(null), + }); + + itemBitstreamsService = getItemBitstreamsServiceStub(); + TestBed.configureTestingModule({ imports: [TranslateModule.forRoot(), ItemEditBitstreamBundleComponent], + providers: [ + { provide: BundleDataService, useValue: bundleService }, + { provide: ObjectUpdatesService, useValue: objectUpdatesService }, + { provide: PaginationService, useValue: new PaginationServiceStub() }, + { provide: RequestService, useValue: getMockRequestService() }, + { provide: ItemBitstreamsService, useValue: itemBitstreamsService }, + ], schemas: [ NO_ERRORS_SCHEMA, ], @@ -62,4 +110,251 @@ describe('ItemEditBitstreamBundleComponent', () => { it('should create an embedded view of the component', () => { expect(viewContainerRef.createEmbeddedView).toHaveBeenCalled(); }); + + describe('on selected entry change', () => { + let paginationComponent: any; + let testSubject: Subject = new Subject(); + + beforeEach(() => { + paginationComponent = jasmine.createSpyObj('paginationComponent', { + doPageChange: undefined, + }); + comp.paginationComponent = paginationComponent; + + spyOn(comp, 'getCurrentPageSize').and.returnValue(2); + }); + + it('should move to the page the selected entry is on if were not on that page', () => { + + const entry: SelectedBitstreamTableEntry = { + bitstream: null, + bundle: bundle, + bundleSize: 5, + originalPosition: 1, + currentPosition: 2, + }; + + comp.handleSelectionAction({ action: 'Moved', selectedEntry: entry }); + expect(paginationComponent.doPageChange).toHaveBeenCalledWith(2); + }); + + it('should not change page when we are already on the correct page', () => { + const entry: SelectedBitstreamTableEntry = { + bitstream: null, + bundle: bundle, + bundleSize: 5, + originalPosition: 0, + currentPosition: 1, + }; + + comp.handleSelectionAction({ action: 'Moved', selectedEntry: entry }); + expect(paginationComponent.doPageChange).not.toHaveBeenCalled(); + }); + + it('should change to the original page when cancelling', () => { + const entry: SelectedBitstreamTableEntry = { + bitstream: null, + bundle: bundle, + bundleSize: 5, + originalPosition: 3, + currentPosition: 0, + }; + + comp.handleSelectionAction({ action: 'Cancelled', selectedEntry: entry }); + expect(paginationComponent.doPageChange).toHaveBeenCalledWith(2); + }); + + it('should not change page when we are already on the correct page when cancelling', () => { + const entry: SelectedBitstreamTableEntry = { + bitstream: null, + bundle: bundle, + bundleSize: 5, + originalPosition: 0, + currentPosition: 3, + }; + + comp.handleSelectionAction({ action: 'Cancelled', selectedEntry: entry }); + expect(paginationComponent.doPageChange).not.toHaveBeenCalled(); + }); + }); + + describe('getRowClass', () => { + it('should return \'table-info\' when the bitstream is the selected bitstream', () => { + itemBitstreamsService.getSelectedBitstream.and.returnValue({ + bitstream: { id: 'bitstream-id' }, + }); + + const bitstreamEntry = { + id: 'bitstream-id', + } as BitstreamTableEntry; + + expect(comp.getRowClass(undefined, bitstreamEntry)).toEqual('table-info'); + }); + + it('should return \'table-warning\' when the update is of type \'UPDATE\'', () => { + const update = { + changeType: FieldChangeType.UPDATE, + } as FieldUpdate; + + expect(comp.getRowClass(update, undefined)).toEqual('table-warning'); + }); + + it('should return \'table-success\' when the update is of type \'ADD\'', () => { + const update = { + changeType: FieldChangeType.ADD, + } as FieldUpdate; + + expect(comp.getRowClass(update, undefined)).toEqual('table-success'); + }); + + it('should return \'table-danger\' when the update is of type \'REMOVE\'', () => { + const update = { + changeType: FieldChangeType.REMOVE, + } as FieldUpdate; + + expect(comp.getRowClass(update, undefined)).toEqual('table-danger'); + }); + + it('should return \'bg-white\' in any other case', () => { + const update = { + changeType: undefined, + } as FieldUpdate; + + expect(comp.getRowClass(update, undefined)).toEqual('bg-white'); + }); + }); + + describe('drag', () => { + let dragTooltip; + let paginationComponent; + + beforeEach(() => { + dragTooltip = jasmine.createSpyObj('dragTooltip', { + open: undefined, + close: undefined, + }); + comp.dragTooltip = dragTooltip; + }); + + describe('Start', () => { + it('should open the tooltip when there are multiple pages', () => { + paginationComponent = jasmine.createSpyObj('paginationComponent', { + doPageChange: undefined, + }, { + shouldShowBottomPager: of(true), + }); + comp.paginationComponent = paginationComponent; + + comp.dragStart(); + expect(dragTooltip.open).toHaveBeenCalled(); + }); + + it('should not open the tooltip when there is only a single page', () => { + paginationComponent = jasmine.createSpyObj('paginationComponent', { + doPageChange: undefined, + }, { + shouldShowBottomPager: of(false), + }); + comp.paginationComponent = paginationComponent; + + comp.dragStart(); + expect(dragTooltip.open).not.toHaveBeenCalled(); + }); + }); + + describe('end', () => { + it('should always close the tooltip', () => { + paginationComponent = jasmine.createSpyObj('paginationComponent', { + doPageChange: undefined, + }, { + shouldShowBottomPager: of(false), + }); + comp.paginationComponent = paginationComponent; + + comp.dragEnd(); + expect(dragTooltip.close).toHaveBeenCalled(); + }); + }); + }); + + describe('drop', () => { + it('should correctly move the bitstream on drop', () => { + const event = { + previousIndex: 1, + currentIndex: 8, + dropPoint: { x: 100, y: 200 }, + } as CdkDragDrop; + + comp.drop(event); + expect(itemBitstreamsService.performBitstreamMoveRequest).toHaveBeenCalledWith(jasmine.any(Bundle), 1, 8, jasmine.any(Function)); + }); + + it('should not move the bitstream if dropped in the same place', () => { + const event = { + previousIndex: 1, + currentIndex: 1, + dropPoint: { x: 100, y: 200 }, + } as CdkDragDrop; + + comp.drop(event); + expect(itemBitstreamsService.performBitstreamMoveRequest).not.toHaveBeenCalled(); + }); + + it('should move to a different page if dropped on a page number', () => { + spyOn(document, 'elementFromPoint').and.returnValue({ + textContent: '2', + classList: { contains: (token: string) => true }, + } as Element); + + const event = { + previousIndex: 1, + currentIndex: 1, + dropPoint: { x: 100, y: 200 }, + } as CdkDragDrop; + + comp.drop(event); + expect(itemBitstreamsService.performBitstreamMoveRequest).toHaveBeenCalledWith(jasmine.any(Bundle), 1, 20, jasmine.any(Function)); + }); + }); + + describe('select', () => { + it('should select the bitstream', () => { + const event = new KeyboardEvent('keydown'); + spyOnProperty(event, 'repeat', 'get').and.returnValue(false); + + const entry = { } as BitstreamTableEntry; + comp.tableEntries$.next([entry]); + + comp.select(event, entry); + expect(itemBitstreamsService.selectBitstreamEntry).toHaveBeenCalledWith(jasmine.objectContaining({ bitstream: entry })); + }); + + it('should cancel the selection if the bitstream already is selected', () => { + const event = new KeyboardEvent('keydown'); + spyOnProperty(event, 'repeat', 'get').and.returnValue(false); + + const entry = { } as BitstreamTableEntry; + comp.tableEntries$.next([entry]); + + itemBitstreamsService.getSelectedBitstream.and.returnValue({ bitstream: entry }); + + comp.select(event, entry); + expect(itemBitstreamsService.selectBitstreamEntry).not.toHaveBeenCalled(); + expect(itemBitstreamsService.cancelSelection).toHaveBeenCalled(); + }); + + it('should not do anything if the user is holding down the select key', () => { + const event = new KeyboardEvent('keydown'); + spyOnProperty(event, 'repeat', 'get').and.returnValue(true); + + const entry = { } as BitstreamTableEntry; + comp.tableEntries$.next([entry]); + + itemBitstreamsService.getSelectedBitstream.and.returnValue({ bitstream: entry }); + + comp.select(event, entry); + expect(itemBitstreamsService.selectBitstreamEntry).not.toHaveBeenCalled(); + expect(itemBitstreamsService.cancelSelection).not.toHaveBeenCalled(); + }); + }); }); diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts index 60654108c94..bcdcc8d62db 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/item-edit-bitstream-bundle.component.ts @@ -1,34 +1,88 @@ +import { + CdkDrag, + CdkDragDrop, + CdkDropList, +} from '@angular/cdk/drag-drop'; +import { CommonModule } from '@angular/common'; import { Component, - EventEmitter, Input, OnDestroy, OnInit, - Output, ViewChild, ViewContainerRef, } from '@angular/core'; import { RouterLink } from '@angular/router'; +import { + NgbDropdownModule, + NgbTooltipModule, +} from '@ng-bootstrap/ng-bootstrap'; import { TranslateModule } from '@ngx-translate/core'; +import { + BehaviorSubject, + Observable, + shareReplay, + Subscription, + switchMap, +} from 'rxjs'; +import { + filter, + map, + take, + tap, +} from 'rxjs/operators'; +import { PaginatedList } from 'src/app/core/data/paginated-list.model'; +import { RemoteData } from 'src/app/core/data/remote-data'; +import { Bitstream } from 'src/app/core/shared/bitstream.model'; import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service'; +import { BundleDataService } from '../../../../core/data/bundle-data.service'; +import { FieldChangeType } from '../../../../core/data/object-updates/field-change-type.model'; +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 { RequestService } from '../../../../core/data/request.service'; +import { PaginationService } from '../../../../core/pagination/pagination.service'; import { Bundle } from '../../../../core/shared/bundle.model'; import { Item } from '../../../../core/shared/item.model'; +import { + getAllSucceededRemoteData, + paginatedListToArray, +} from '../../../../core/shared/operators'; +import { + hasNoValue, + hasValue, +} from '../../../../shared/empty.util'; +import { PaginationComponent } from '../../../../shared/pagination/pagination.component'; +import { PaginationComponentOptions } from '../../../../shared/pagination/pagination-component-options.model'; import { ResponsiveColumnSizes } from '../../../../shared/responsive-table-sizes/responsive-column-sizes'; import { ResponsiveTableSizes } from '../../../../shared/responsive-table-sizes/responsive-table-sizes'; +import { PaginatedSearchOptions } from '../../../../shared/search/models/paginated-search-options.model'; +import { BrowserOnlyPipe } from '../../../../shared/utils/browser-only.pipe'; +import { followLink } from '../../../../shared/utils/follow-link-config.model'; import { getItemPageRoute } from '../../../item-page-routing-paths'; -import { ItemEditBitstreamDragHandleComponent } from '../item-edit-bitstream-drag-handle/item-edit-bitstream-drag-handle.component'; -import { PaginatedDragAndDropBitstreamListComponent } from './paginated-drag-and-drop-bitstream-list/paginated-drag-and-drop-bitstream-list.component'; +import { + BitstreamTableEntry, + ItemBitstreamsService, + MOVE_KEY, + SelectedBitstreamTableEntry, + SelectionAction, +} from '../item-bitstreams.service'; @Component({ selector: 'ds-item-edit-bitstream-bundle', - styleUrls: ['../item-bitstreams.component.scss'], + styleUrls: ['../item-bitstreams.component.scss', './item-edit-bitstream-bundle.component.scss'], templateUrl: './item-edit-bitstream-bundle.component.html', imports: [ - PaginatedDragAndDropBitstreamListComponent, + CommonModule, TranslateModule, RouterLink, - ItemEditBitstreamDragHandleComponent, + PaginationComponent, + NgbTooltipModule, + CdkDropList, + NgbDropdownModule, + CdkDrag, + BrowserOnlyPipe, ], standalone: true, }) @@ -38,12 +92,23 @@ import { PaginatedDragAndDropBitstreamListComponent } from './paginated-drag-and * (which means it'll be added to the parents html without a wrapping ds-item-edit-bitstream-bundle element) */ export class ItemEditBitstreamBundleComponent implements OnInit, OnDestroy { + protected readonly FieldChangeType = FieldChangeType; /** * The view on the bundle information and bitstreams */ @ViewChild('bundleView', { static: true }) bundleView; + /** + * The view on the pagination component + */ + @ViewChild(PaginationComponent) paginationComponent: PaginationComponent; + + /** + * The view on the drag tooltip + */ + @ViewChild('dragTooltip') dragTooltip; + /** * The bundle to display bitstreams for */ @@ -60,11 +125,9 @@ export class ItemEditBitstreamBundleComponent implements OnInit, OnDestroy { @Input() columnSizes: ResponsiveTableSizes; /** - * Send an event when the user drops an object on the pagination - * The event contains details about the index the object came from and is dropped to (across the entirety of the list, - * not just within a single page) + * Whether this is the first in a series of bundle tables */ - @Output() dropObject: EventEmitter = new EventEmitter(); + @Input() isFirstTable = false; /** * The bootstrap sizes used for the Bundle Name column @@ -77,9 +140,65 @@ export class ItemEditBitstreamBundleComponent implements OnInit, OnDestroy { */ itemPageRoute: string; + /** + * The name of the bundle + */ + bundleName: string; + + /** + * The number of bitstreams in the bundle + */ + bundleSize: number; + + /** + * The bitstreams to show in the table + */ + bitstreamsRD$: Observable>>; + + /** + * The data to show in the table + */ + tableEntries$: BehaviorSubject = new BehaviorSubject([]); + + /** + * The initial page options to use for fetching the bitstreams + */ + paginationOptions: PaginationComponentOptions; + + /** + * The current page options + */ + currentPaginationOptions$: BehaviorSubject; + + /** + * The currently selected page size + */ + pageSize$: BehaviorSubject; + + /** + * The self url of the bundle, also used when retrieving fieldUpdates + */ + bundleUrl: string; + + /** + * The updates to the current bitstreams + */ + updates$: BehaviorSubject = new BehaviorSubject(null); + + /** + * Array containing all subscriptions created by this component + */ + subscriptions: Subscription[] = []; + + constructor( protected viewContainerRef: ViewContainerRef, public dsoNameService: DSONameService, + protected bundleService: BundleDataService, + protected objectUpdatesService: ObjectUpdatesService, + protected paginationService: PaginationService, + protected requestService: RequestService, + protected itemBitstreamsService: ItemBitstreamsService, ) { } @@ -87,10 +206,384 @@ export class ItemEditBitstreamBundleComponent implements OnInit, OnDestroy { this.bundleNameColumn = this.columnSizes.combineColumns(0, 2); this.viewContainerRef.createEmbeddedView(this.bundleView); this.itemPageRoute = getItemPageRoute(this.item); + this.bundleName = this.dsoNameService.getName(this.bundle); + this.bundleUrl = this.bundle.self; + + this.initializePagination(); + this.initializeBitstreams(); + this.initializeSelectionActions(); } - ngOnDestroy(): void { + ngOnDestroy() { this.viewContainerRef.clear(); + this.subscriptions.forEach(sub => sub?.unsubscribe()); + } + + protected initializePagination() { + this.paginationOptions = this.itemBitstreamsService.getInitialBitstreamsPaginationOptions(this.bundleName); + + this.currentPaginationOptions$ = new BehaviorSubject(this.paginationOptions); + this.pageSize$ = new BehaviorSubject(this.paginationOptions.pageSize); + + this.subscriptions.push( + this.paginationService.getCurrentPagination(this.paginationOptions.id, this.paginationOptions) + .subscribe((pagination) => { + this.currentPaginationOptions$.next(pagination); + this.pageSize$.next(pagination.pageSize); + }), + ); + + } + + protected initializeBitstreams() { + this.bitstreamsRD$ = this.currentPaginationOptions$.pipe( + switchMap((page: PaginationComponentOptions) => { + const paginatedOptions = new PaginatedSearchOptions({ pagination: Object.assign({}, page) }); + return this.bundleService.getBitstreamsEndpoint(this.bundle.id, paginatedOptions).pipe( + switchMap((href) => this.requestService.hasByHref$(href)), + switchMap(() => this.bundleService.getBitstreams( + this.bundle.id, + paginatedOptions, + followLink('format'), + )), + ); + }), + getAllSucceededRemoteData(), + shareReplay({ bufferSize: 1, refCount: true }), + ); + + this.subscriptions.push( + this.bitstreamsRD$.pipe( + take(1), + tap(bitstreamsRD => this.bundleSize = bitstreamsRD.payload.totalElements), + paginatedListToArray(), + ).subscribe((bitstreams) => { + this.objectUpdatesService.initialize(this.bundleUrl, bitstreams, new Date()); + }), + + this.bitstreamsRD$.pipe( + paginatedListToArray(), + switchMap((bitstreams) => this.objectUpdatesService.getFieldUpdatesExclusive(this.bundleUrl, bitstreams)), + ).subscribe((updates) => this.updates$.next(updates)), + + this.bitstreamsRD$.pipe( + paginatedListToArray(), + map((bitstreams) => this.itemBitstreamsService.mapBitstreamsToTableEntries(bitstreams)), + ).subscribe((tableEntries) => this.tableEntries$.next(tableEntries)), + ); + } + + protected initializeSelectionActions() { + this.subscriptions.push( + this.itemBitstreamsService.getSelectionAction$().subscribe( + selectionAction => this.handleSelectionAction(selectionAction)), + ); + } + + /** + * Handles a change in selected bitstream by changing the pagination if the change happened on a different page + * @param selectionAction + */ + handleSelectionAction(selectionAction: SelectionAction) { + if (hasNoValue(selectionAction) || selectionAction.selectedEntry.bundle !== this.bundle) { + return; + } + + if (selectionAction.action === 'Moved') { + // If the currently selected bitstream belongs to this bundle, it has possibly moved to a different page. + // In that case we want to change the pagination to the new page. + this.redirectToCurrentPage(selectionAction.selectedEntry); + } + + if (selectionAction.action === 'Cancelled') { + // If the selection is cancelled (and returned to its original position), it is possible the previously selected + // bitstream is returned to a different page. In that case we want to change the pagination to the place where + // the bitstream was returned to. + this.redirectToOriginalPage(selectionAction.selectedEntry); + } + + if (selectionAction.action === 'Cleared') { + // If the selection is cleared, it is possible the previously selected bitstream is on a different page. In that + // case we want to change the pagination to the place where the bitstream is. + this.redirectToCurrentPage(selectionAction.selectedEntry); + } + } + + /** + * Redirect the user to the current page of the provided bitstream if it is on a different page. + * @param bitstreamEntry The entry that the current position will be taken from to determine the page to move to + * @protected + */ + protected redirectToCurrentPage(bitstreamEntry: SelectedBitstreamTableEntry) { + const currentPage = this.getCurrentPage(); + const selectedEntryPage = this.bundleIndexToPage(bitstreamEntry.currentPosition); + + if (currentPage !== selectedEntryPage) { + this.changeToPage(selectedEntryPage); + } + } + + /** + * Redirect the user to the original page of the provided bitstream if it is on a different page. + * @param bitstreamEntry The entry that the original position will be taken from to determine the page to move to + * @protected + */ + protected redirectToOriginalPage(bitstreamEntry: SelectedBitstreamTableEntry) { + const currentPage = this.getCurrentPage(); + const originPage = this.bundleIndexToPage(bitstreamEntry.originalPosition); + + if (currentPage !== originPage) { + this.changeToPage(originPage); + } + } + + /** + * Check if a user should be allowed to remove this field + */ + canRemove(fieldUpdate: FieldUpdate): boolean { + return fieldUpdate.changeType !== FieldChangeType.REMOVE; + } + + /** + * Check if a user should be allowed to cancel the update to this field + */ + canUndo(fieldUpdate: FieldUpdate): boolean { + return fieldUpdate.changeType >= FieldChangeType.UPDATE; + } + + /** + * Sends a new remove update for this field to the object updates service + */ + remove(bitstream: Bitstream): void { + this.objectUpdatesService.saveRemoveFieldUpdate(this.bundleUrl, bitstream); + } + + /** + * Cancels the current update for this field in the object updates service + */ + undo(bitstream: Bitstream): void { + this.objectUpdatesService.removeSingleFieldUpdate(this.bundleUrl, bitstream.uuid); + } + + /** + * Returns the css class for a table row depending on the state of the table entry. + * @param update + * @param bitstream + */ + getRowClass(update: FieldUpdate, bitstream: BitstreamTableEntry): string { + const selected = this.itemBitstreamsService.getSelectedBitstream(); + + if (hasValue(selected) && bitstream.id === selected.bitstream.id) { + return 'table-info'; + } + + switch (update.changeType) { + case FieldChangeType.UPDATE: + return 'table-warning'; + case FieldChangeType.ADD: + return 'table-success'; + case FieldChangeType.REMOVE: + return 'table-danger'; + default: + return 'bg-white'; + } + } + + /** + * Changes the page size to the provided page size. + * @param pageSize + */ + public doPageSizeChange(pageSize: number) { + this.paginationComponent.doPageSizeChange(pageSize); + } + + /** + * Handles start of dragging by opening the tooltip mentioning that it is possible to drag a bitstream to a different + * page by dropping it on the page number, only if there are multiple pages. + */ + dragStart() { + // Only open the drag tooltip when there are multiple pages + this.paginationComponent.shouldShowBottomPager.pipe( + take(1), + filter((hasMultiplePages) => hasMultiplePages), + ).subscribe(() => { + this.dragTooltip.open(); + }); + } + + /** + * Handles end of dragging by closing the tooltip. + */ + dragEnd() { + this.dragTooltip.close(); + } + + /** + * Handles dropping by calculation the target position, and changing the page if the bitstream was dropped on a + * different page. + * @param event + */ + drop(event: CdkDragDrop) { + const dragIndex = event.previousIndex; + let dropIndex = event.currentIndex; + const dragPage = this.getCurrentPage(); + let dropPage = this.getCurrentPage(); + + // Check if the user is hovering over any of the pagination's pages at the time of dropping the object + const droppedOnElement = document.elementFromPoint(event.dropPoint.x, event.dropPoint.y); + if (hasValue(droppedOnElement) && hasValue(droppedOnElement.textContent) && droppedOnElement.classList.contains('page-link')) { + // The user is hovering over a page, fetch the page's number from the element + let droppedPage = Number(droppedOnElement.textContent); + if (hasValue(droppedPage) && !Number.isNaN(droppedPage)) { + droppedPage -= 1; + + if (droppedPage !== dragPage) { + dropPage = droppedPage; + + if (dropPage > dragPage) { + // When moving to later page, place bitstream at the top + dropIndex = 0; + } else { + // When moving to earlier page, place bitstream at the bottom + dropIndex = this.getCurrentPageSize() - 1; + } + } + } + } + + const fromIndex = this.pageIndexToBundleIndex(dragIndex, dragPage); + const toIndex = this.pageIndexToBundleIndex(dropIndex, dropPage); + + if (fromIndex === toIndex) { + return; + } + + const selectedBitstream = this.tableEntries$.value[dragIndex]; + + const finish = () => { + this.itemBitstreamsService.announceMove(selectedBitstream.name, toIndex); + + if (dropPage !== this.getCurrentPage()) { + this.changeToPage(dropPage); + } + + this.itemBitstreamsService.displaySuccessNotification(MOVE_KEY); + }; + + this.itemBitstreamsService.performBitstreamMoveRequest(this.bundle, fromIndex, toIndex, finish); + } + + /** + * Handles a select action for the provided bitstream entry. + * If the selected bitstream is currently selected, the selection is cleared. + * If no, or a different bitstream, is selected, the provided bitstream becomes the selected bitstream. + * @param event The event that triggered the select action + * @param bitstream The bitstream that is the target of the select action + */ + select(event: UIEvent, bitstream: BitstreamTableEntry) { + event.preventDefault(); + + if (event instanceof KeyboardEvent && event.repeat) { + // Don't handle hold events, otherwise it would change rapidly between being selected and unselected + return; + } + + const selectedBitstream = this.itemBitstreamsService.getSelectedBitstream(); + + if (hasValue(selectedBitstream) && selectedBitstream.bitstream === bitstream) { + this.itemBitstreamsService.cancelSelection(); + } else { + const selectionObject = this.createBitstreamSelectionObject(bitstream); + + if (hasNoValue(selectionObject)) { + console.warn('Failed to create selection object'); + return; + } + + this.itemBitstreamsService.selectBitstreamEntry(selectionObject); + } + } + + /** + * Creates a {@link SelectedBitstreamTableEntry} from the provided {@link BitstreamTableEntry} so it can be given + * to the {@link ItemBitstreamsService} to select the table entry. + * @param bitstream The table entry to create the selection object from. + * @protected + */ + protected createBitstreamSelectionObject(bitstream: BitstreamTableEntry): SelectedBitstreamTableEntry { + const pageIndex = this.findBitstreamPageIndex(bitstream); + + if (pageIndex === -1) { + return null; + } + + const position = this.pageIndexToBundleIndex(pageIndex, this.getCurrentPage()); + + return { + bitstream: bitstream, + bundle: this.bundle, + bundleSize: this.bundleSize, + currentPosition: position, + originalPosition: position, + }; + } + + /** + * Returns the index of the provided {@link BitstreamTableEntry} relative to the current page + * If the current page size is 10, it will return a value from 0 to 9 (inclusive) + * Returns -1 if the provided bitstream could not be found + * @protected + */ + protected findBitstreamPageIndex(bitstream: BitstreamTableEntry): number { + const entries = this.tableEntries$.value; + return entries.findIndex(entry => entry === bitstream); + } + + /** + * Returns the current zero-indexed page + * @protected + */ + protected getCurrentPage(): number { + // The pagination component uses one-based numbering while zero-based numbering is more convenient for calculations + return this.currentPaginationOptions$.value.currentPage - 1; + } + + /** + * Returns the current page size + * @protected + */ + protected getCurrentPageSize(): number { + return this.currentPaginationOptions$.value.pageSize; + } + + /** + * Converts an index relative to the page to an index relative to the bundle + * @param index The index relative to the page + * @param page The zero-indexed page number + * @protected + */ + protected pageIndexToBundleIndex(index: number, page: number) { + return page * this.getCurrentPageSize() + index; + } + + /** + * Calculates the zero-indexed page number from the index relative to the bundle + * @param index The index relative to the bundle + * @protected + */ + protected bundleIndexToPage(index: number) { + return Math.floor(index / this.getCurrentPageSize()); + } + + /** + * Change the pagination for this bundle to the provided zero-indexed page + * @param page The zero-indexed page to change to + * @protected + */ + protected changeToPage(page: number) { + // Increments page by one because zero-indexing is way easier for calculations but the pagination component + // uses one-indexing. + this.paginationComponent.doPageChange(page + 1); } } diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/paginated-drag-and-drop-bitstream-list/paginated-drag-and-drop-bitstream-list.component.html b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/paginated-drag-and-drop-bitstream-list/paginated-drag-and-drop-bitstream-list.component.html deleted file mode 100644 index 20b5dc708fa..00000000000 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/paginated-drag-and-drop-bitstream-list/paginated-drag-and-drop-bitstream-list.component.html +++ /dev/null @@ -1,32 +0,0 @@ - - -
- -
- -
- -
-
-
-
-
-
- -
diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/paginated-drag-and-drop-bitstream-list/paginated-drag-and-drop-bitstream-list.component.spec.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/paginated-drag-and-drop-bitstream-list/paginated-drag-and-drop-bitstream-list.component.spec.ts deleted file mode 100644 index cbb3314252a..00000000000 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/paginated-drag-and-drop-bitstream-list/paginated-drag-and-drop-bitstream-list.component.spec.ts +++ /dev/null @@ -1,164 +0,0 @@ -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { - ComponentFixture, - TestBed, - waitForAsync, -} from '@angular/core/testing'; -import { ActivatedRoute } from '@angular/router'; -import { TranslateModule } from '@ngx-translate/core'; -import { of as observableOf } from 'rxjs'; -import { take } from 'rxjs/operators'; - -import { BundleDataService } from '../../../../../core/data/bundle-data.service'; -import { ObjectUpdatesService } from '../../../../../core/data/object-updates/object-updates.service'; -import { RequestService } from '../../../../../core/data/request.service'; -import { PaginationService } from '../../../../../core/pagination/pagination.service'; -import { Bitstream } from '../../../../../core/shared/bitstream.model'; -import { BitstreamFormat } from '../../../../../core/shared/bitstream-format.model'; -import { Bundle } from '../../../../../core/shared/bundle.model'; -import { PaginationComponent } from '../../../../../shared/pagination/pagination.component'; -import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/remote-data.utils'; -import { ResponsiveColumnSizes } from '../../../../../shared/responsive-table-sizes/responsive-column-sizes'; -import { ResponsiveTableSizes } from '../../../../../shared/responsive-table-sizes/responsive-table-sizes'; -import { ActivatedRouteStub } from '../../../../../shared/testing/active-router.stub'; -import { PaginationServiceStub } from '../../../../../shared/testing/pagination-service.stub'; -import { createPaginatedList } from '../../../../../shared/testing/utils.test'; -import { ObjectValuesPipe } from '../../../../../shared/utils/object-values-pipe'; -import { VarDirective } from '../../../../../shared/utils/var.directive'; -import { PaginatedDragAndDropBitstreamListComponent } from './paginated-drag-and-drop-bitstream-list.component'; - -describe('PaginatedDragAndDropBitstreamListComponent', () => { - let comp: PaginatedDragAndDropBitstreamListComponent; - let fixture: ComponentFixture; - let objectUpdatesService: ObjectUpdatesService; - let bundleService: BundleDataService; - let objectValuesPipe: ObjectValuesPipe; - let requestService: RequestService; - let paginationService; - - const columnSizes = new ResponsiveTableSizes([ - new ResponsiveColumnSizes(2, 2, 3, 4, 4), - new ResponsiveColumnSizes(2, 3, 3, 3, 3), - new ResponsiveColumnSizes(2, 2, 2, 2, 2), - new ResponsiveColumnSizes(6, 5, 4, 3, 3), - ]); - - const bundle = Object.assign(new Bundle(), { - id: 'bundle-1', - uuid: 'bundle-1', - _links: { - self: { href: 'bundle-1-selflink' }, - }, - }); - const date = new Date(); - const format = Object.assign(new BitstreamFormat(), { - shortDescription: 'PDF', - }); - const bitstream1 = Object.assign(new Bitstream(), { - uuid: 'bitstreamUUID1', - name: 'Fake Bitstream 1', - bundleName: 'ORIGINAL', - description: 'Description', - format: createSuccessfulRemoteDataObject$(format), - }); - const fieldUpdate1 = { - field: bitstream1, - changeType: undefined, - }; - const bitstream2 = Object.assign(new Bitstream(), { - uuid: 'bitstreamUUID2', - name: 'Fake Bitstream 2', - bundleName: 'ORIGINAL', - description: 'Description', - format: createSuccessfulRemoteDataObject$(format), - }); - const fieldUpdate2 = { - field: bitstream2, - changeType: undefined, - }; - - beforeEach(waitForAsync(() => { - objectUpdatesService = jasmine.createSpyObj('objectUpdatesService', - { - getFieldUpdates: observableOf({ - [bitstream1.uuid]: fieldUpdate1, - [bitstream2.uuid]: fieldUpdate2, - }), - getFieldUpdatesExclusive: observableOf({ - [bitstream1.uuid]: fieldUpdate1, - [bitstream2.uuid]: fieldUpdate2, - }), - getFieldUpdatesByCustomOrder: observableOf({ - [bitstream1.uuid]: fieldUpdate1, - [bitstream2.uuid]: fieldUpdate2, - }), - saveMoveFieldUpdate: {}, - saveRemoveFieldUpdate: {}, - removeSingleFieldUpdate: {}, - saveAddFieldUpdate: {}, - discardFieldUpdates: {}, - reinstateFieldUpdates: observableOf(true), - initialize: {}, - getUpdatedFields: observableOf([bitstream1, bitstream2]), - getLastModified: observableOf(date), - hasUpdates: observableOf(true), - isReinstatable: observableOf(false), - isValidPage: observableOf(true), - initializeWithCustomOrder: {}, - addPageToCustomOrder: {}, - }, - ); - - bundleService = jasmine.createSpyObj('bundleService', { - getBitstreams: createSuccessfulRemoteDataObject$(createPaginatedList([bitstream1, bitstream2])), - getBitstreamsEndpoint: observableOf(''), - }); - - objectValuesPipe = new ObjectValuesPipe(); - - requestService = jasmine.createSpyObj('requestService', { - hasByHref$: observableOf(true), - }); - - paginationService = new PaginationServiceStub(); - - TestBed.configureTestingModule({ - imports: [TranslateModule.forRoot(), PaginatedDragAndDropBitstreamListComponent, VarDirective], - providers: [ - { provide: ObjectUpdatesService, useValue: objectUpdatesService }, - { provide: BundleDataService, useValue: bundleService }, - { provide: ObjectValuesPipe, useValue: objectValuesPipe }, - { provide: RequestService, useValue: requestService }, - { provide: PaginationService, useValue: paginationService }, - { provide: ActivatedRoute, useValue: new ActivatedRouteStub() }, - ], schemas: [ - NO_ERRORS_SCHEMA, - ], - }) - .overrideComponent(PaginatedDragAndDropBitstreamListComponent, { - remove: { - imports: [PaginationComponent], - }, - }) - .compileComponents(); - })); - - beforeEach(() => { - fixture = TestBed.createComponent(PaginatedDragAndDropBitstreamListComponent); - comp = fixture.componentInstance; - comp.bundle = bundle; - comp.columnSizes = columnSizes; - fixture.detectChanges(); - }); - - it('should initialize the objectsRD$', (done) => { - comp.objectsRD$.pipe(take(1)).subscribe((objects) => { - expect(objects.payload.page).toEqual([bitstream1, bitstream2]); - done(); - }); - }); - - it('should initialize the URL', () => { - expect(comp.url).toEqual(bundle.self); - }); -}); diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/paginated-drag-and-drop-bitstream-list/paginated-drag-and-drop-bitstream-list.component.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/paginated-drag-and-drop-bitstream-list/paginated-drag-and-drop-bitstream-list.component.ts deleted file mode 100644 index 24d827ac61a..00000000000 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-bundle/paginated-drag-and-drop-bitstream-list/paginated-drag-and-drop-bitstream-list.component.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { - CdkDrag, - CdkDragHandle, - CdkDropList, -} from '@angular/cdk/drag-drop'; -import { - AsyncPipe, - NgClass, - NgForOf, - NgIf, -} from '@angular/common'; -import { - Component, - ElementRef, - Input, - OnInit, -} from '@angular/core'; -import { TranslateModule } from '@ngx-translate/core'; -import { switchMap } from 'rxjs/operators'; - -import { BundleDataService } from '../../../../../core/data/bundle-data.service'; -import { ObjectUpdatesService } from '../../../../../core/data/object-updates/object-updates.service'; -import { RequestService } from '../../../../../core/data/request.service'; -import { PaginationService } from '../../../../../core/pagination/pagination.service'; -import { Bitstream } from '../../../../../core/shared/bitstream.model'; -import { Bundle } from '../../../../../core/shared/bundle.model'; -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 { AbstractPaginatedDragAndDropListComponent } from '../../../../../shared/pagination-drag-and-drop/abstract-paginated-drag-and-drop-list.component'; -import { ResponsiveTableSizes } from '../../../../../shared/responsive-table-sizes/responsive-table-sizes'; -import { PaginatedSearchOptions } from '../../../../../shared/search/models/paginated-search-options.model'; -import { followLink } from '../../../../../shared/utils/follow-link-config.model'; -import { ObjectValuesPipe } from '../../../../../shared/utils/object-values-pipe'; -import { VarDirective } from '../../../../../shared/utils/var.directive'; -import { ItemEditBitstreamComponent } from '../../item-edit-bitstream/item-edit-bitstream.component'; -import { ItemEditBitstreamDragHandleComponent } from '../../item-edit-bitstream-drag-handle/item-edit-bitstream-drag-handle.component'; - -@Component({ - selector: 'ds-paginated-drag-and-drop-bitstream-list', - styleUrls: ['../../item-bitstreams.component.scss'], - templateUrl: './paginated-drag-and-drop-bitstream-list.component.html', - imports: [ - AsyncPipe, - NgIf, - PaginationComponent, - NgClass, - VarDirective, - CdkDropList, - NgForOf, - CdkDrag, - ItemEditBitstreamComponent, - ItemEditBitstreamDragHandleComponent, - CdkDragHandle, - ThemedLoadingComponent, - TranslateModule, - ], - standalone: true, -}) -/** - * A component listing edit-bitstream rows for each bitstream within the given bundle. - * This component makes use of the AbstractPaginatedDragAndDropListComponent, allowing for users to drag and drop - * bitstreams within the paginated list. To drag and drop a bitstream between two pages, drag the row on top of the - * page number you want the bitstream to end up at. Doing so will add the bitstream to the top of that page. - */ -export class PaginatedDragAndDropBitstreamListComponent extends AbstractPaginatedDragAndDropListComponent implements OnInit { - /** - * The bundle to display bitstreams for - */ - @Input() bundle: Bundle; - - /** - * The bootstrap sizes used for the columns within this table - */ - @Input() columnSizes: ResponsiveTableSizes; - - constructor(protected objectUpdatesService: ObjectUpdatesService, - protected elRef: ElementRef, - protected objectValuesPipe: ObjectValuesPipe, - protected bundleService: BundleDataService, - protected paginationService: PaginationService, - protected requestService: RequestService) { - super(objectUpdatesService, elRef, objectValuesPipe, paginationService); - } - - ngOnInit() { - super.ngOnInit(); - } - - /** - * Initialize the bitstreams observable depending on currentPage$ - */ - initializeObjectsRD(): void { - this.objectsRD$ = this.currentPage$.pipe( - switchMap((page: PaginationComponentOptions) => { - const paginatedOptions = new PaginatedSearchOptions({ pagination: Object.assign({}, page) }); - return this.bundleService.getBitstreamsEndpoint(this.bundle.id, paginatedOptions).pipe( - switchMap((href) => this.requestService.hasByHref$(href)), - switchMap(() => this.bundleService.getBitstreams( - this.bundle.id, - paginatedOptions, - followLink('format'), - )), - ); - }), - ); - } - - /** - * Initialize the URL used for the field-update store, in this case the bundle's self-link - */ - initializeURL(): void { - this.url = this.bundle.self; - } -} diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-drag-handle/item-edit-bitstream-drag-handle.component.html b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-drag-handle/item-edit-bitstream-drag-handle.component.html deleted file mode 100644 index 1bce8667ee1..00000000000 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-drag-handle/item-edit-bitstream-drag-handle.component.html +++ /dev/null @@ -1,5 +0,0 @@ - -
- -
-
diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-drag-handle/item-edit-bitstream-drag-handle.component.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-drag-handle/item-edit-bitstream-drag-handle.component.ts deleted file mode 100644 index b993eb71930..00000000000 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream-drag-handle/item-edit-bitstream-drag-handle.component.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { - Component, - OnDestroy, - OnInit, - ViewChild, - ViewContainerRef, -} from '@angular/core'; -import { TranslateModule } from '@ngx-translate/core'; - -@Component({ - selector: 'ds-item-edit-bitstream-drag-handle', - styleUrls: ['../item-bitstreams.component.scss'], - templateUrl: './item-edit-bitstream-drag-handle.component.html', - imports: [ - TranslateModule, - ], - standalone: true, -}) -/** - * Component displaying a drag handle for the item-edit-bitstream page - * Creates an embedded view of the contents - * (which means it'll be added to the parents html without a wrapping ds-item-edit-bitstream-drag-handle element) - */ -export class ItemEditBitstreamDragHandleComponent implements OnInit, OnDestroy { - /** - * The view on the drag-handle - */ - @ViewChild('handleView', { static: true }) handleView; - - constructor(private viewContainerRef: ViewContainerRef) { - } - - ngOnInit(): void { - this.viewContainerRef.createEmbeddedView(this.handleView); - } - - ngOnDestroy(): void { - this.viewContainerRef.clear(); - } - -} diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream/item-edit-bitstream.component.html b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream/item-edit-bitstream.component.html deleted file mode 100644 index 7d52ab220f1..00000000000 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream/item-edit-bitstream.component.html +++ /dev/null @@ -1,55 +0,0 @@ - -
- -
- - {{ bitstreamName }} - -
-
-
-
-
- {{ bitstream?.firstMetadataValue('dc.description') }} -
-
-
-
-
- - {{ (format$ | async)?.shortDescription }} - -
-
-
-
-
- - - - - - -
-
-
-
diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream/item-edit-bitstream.component.spec.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream/item-edit-bitstream.component.spec.ts deleted file mode 100644 index bceb0a12071..00000000000 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream/item-edit-bitstream.component.spec.ts +++ /dev/null @@ -1,153 +0,0 @@ -import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { - ComponentFixture, - TestBed, - waitForAsync, -} from '@angular/core/testing'; -import { By } from '@angular/platform-browser'; -import { ActivatedRoute } from '@angular/router'; -import { RouterTestingModule } from '@angular/router/testing'; -import { TranslateModule } from '@ngx-translate/core'; -import { of as observableOf } from 'rxjs'; - -import { getBitstreamDownloadRoute } from '../../../../app-routing-paths'; -import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service'; -import { Bitstream } from '../../../../core/shared/bitstream.model'; -import { BitstreamFormat } from '../../../../core/shared/bitstream-format.model'; -import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils'; -import { ResponsiveColumnSizes } from '../../../../shared/responsive-table-sizes/responsive-column-sizes'; -import { ResponsiveTableSizes } from '../../../../shared/responsive-table-sizes/responsive-table-sizes'; -import { ActivatedRouteStub } from '../../../../shared/testing/active-router.stub'; -import { VarDirective } from '../../../../shared/utils/var.directive'; -import { ItemEditBitstreamComponent } from './item-edit-bitstream.component'; - -let comp: ItemEditBitstreamComponent; -let fixture: ComponentFixture; - -const columnSizes = new ResponsiveTableSizes([ - new ResponsiveColumnSizes(2, 2, 3, 4, 4), - new ResponsiveColumnSizes(2, 3, 3, 3, 3), - new ResponsiveColumnSizes(2, 2, 2, 2, 2), - new ResponsiveColumnSizes(6, 5, 4, 3, 3), -]); - -const format = Object.assign(new BitstreamFormat(), { - shortDescription: 'PDF', -}); -const bitstream = Object.assign(new Bitstream(), { - uuid: 'bitstreamUUID', - name: 'Fake Bitstream', - bundleName: 'ORIGINAL', - description: 'Description', - _links: { - content: { href: 'content-link' }, - }, - - format: createSuccessfulRemoteDataObject$(format), -}); -const fieldUpdate = { - field: bitstream, - changeType: undefined, -}; -const date = new Date(); -const url = 'thisUrl'; - -let objectUpdatesService: ObjectUpdatesService; - -describe('ItemEditBitstreamComponent', () => { - beforeEach(waitForAsync(() => { - objectUpdatesService = jasmine.createSpyObj('objectUpdatesService', - { - getFieldUpdates: observableOf({ - [bitstream.uuid]: fieldUpdate, - }), - getFieldUpdatesExclusive: observableOf({ - [bitstream.uuid]: fieldUpdate, - }), - saveRemoveFieldUpdate: {}, - removeSingleFieldUpdate: {}, - saveAddFieldUpdate: {}, - discardFieldUpdates: {}, - reinstateFieldUpdates: observableOf(true), - initialize: {}, - getUpdatedFields: observableOf([bitstream]), - getLastModified: observableOf(date), - hasUpdates: observableOf(true), - isReinstatable: observableOf(false), - isValidPage: observableOf(true), - }, - ); - - TestBed.configureTestingModule({ - imports: [ - TranslateModule.forRoot(), - RouterTestingModule.withRoutes([]), - ItemEditBitstreamComponent, - VarDirective, - ], - providers: [ - { provide: ObjectUpdatesService, useValue: objectUpdatesService }, - { provide: ActivatedRoute, useValue: new ActivatedRouteStub() }, - ], schemas: [ - NO_ERRORS_SCHEMA, - ], - }).compileComponents(); - })); - - beforeEach(() => { - fixture = TestBed.createComponent(ItemEditBitstreamComponent); - comp = fixture.componentInstance; - comp.fieldUpdate = fieldUpdate; - comp.bundleUrl = url; - comp.columnSizes = columnSizes; - comp.ngOnChanges(undefined); - fixture.detectChanges(); - }); - - describe('when remove is called', () => { - beforeEach(() => { - comp.remove(); - }); - - it('should call saveRemoveFieldUpdate on objectUpdatesService', () => { - expect(objectUpdatesService.saveRemoveFieldUpdate).toHaveBeenCalledWith(url, bitstream); - }); - }); - - describe('when undo is called', () => { - beforeEach(() => { - comp.undo(); - }); - - it('should call removeSingleFieldUpdate on objectUpdatesService', () => { - expect(objectUpdatesService.removeSingleFieldUpdate).toHaveBeenCalledWith(url, bitstream.uuid); - }); - }); - - describe('when canRemove is called', () => { - it('should return true', () => { - expect(comp.canRemove()).toEqual(true); - }); - }); - - describe('when canUndo is called', () => { - it('should return false', () => { - expect(comp.canUndo()).toEqual(false); - }); - }); - - describe('when the component loads', () => { - it('should contain download button with a valid link to the bitstreams download page', () => { - fixture.detectChanges(); - const downloadBtnHref = fixture.debugElement.query(By.css('[data-test="download-button"]')).nativeElement.getAttribute('href'); - expect(downloadBtnHref).toEqual(comp.bitstreamDownloadUrl); - }); - }); - - describe('when the bitstreamDownloadUrl property gets populated', () => { - it('should contain the bitstream download page route', () => { - expect(comp.bitstreamDownloadUrl).not.toEqual(bitstream._links.content.href); - expect(comp.bitstreamDownloadUrl).toEqual(getBitstreamDownloadRoute(bitstream)); - }); - }); -}); diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream/item-edit-bitstream.component.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream/item-edit-bitstream.component.ts deleted file mode 100644 index 5706805661b..00000000000 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-edit-bitstream/item-edit-bitstream.component.ts +++ /dev/null @@ -1,151 +0,0 @@ -import { - AsyncPipe, - NgIf, -} from '@angular/common'; -import { - Component, - Input, - OnChanges, - OnDestroy, - OnInit, - SimpleChanges, - ViewChild, - ViewContainerRef, -} from '@angular/core'; -import { RouterLink } from '@angular/router'; -import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'; -import { TranslateModule } from '@ngx-translate/core'; -import cloneDeep from 'lodash/cloneDeep'; -import { Observable } from 'rxjs'; - -import { getBitstreamDownloadRoute } from '../../../../app-routing-paths'; -import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service'; -import { FieldChangeType } from '../../../../core/data/object-updates/field-change-type.model'; -import { FieldUpdate } from '../../../../core/data/object-updates/field-update.model'; -import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service'; -import { Bitstream } from '../../../../core/shared/bitstream.model'; -import { BitstreamFormat } from '../../../../core/shared/bitstream-format.model'; -import { - getFirstSucceededRemoteData, - getRemoteDataPayload, -} from '../../../../core/shared/operators'; -import { ResponsiveTableSizes } from '../../../../shared/responsive-table-sizes/responsive-table-sizes'; -import { BrowserOnlyPipe } from '../../../../shared/utils/browser-only.pipe'; - -@Component({ - selector: 'ds-item-edit-bitstream', - styleUrls: ['../item-bitstreams.component.scss'], - templateUrl: './item-edit-bitstream.component.html', - imports: [ - RouterLink, - TranslateModule, - BrowserOnlyPipe, - NgbTooltipModule, - AsyncPipe, - NgIf, - ], - standalone: true, -}) -/** - * Component that displays a single bitstream of an item on the edit page - * Creates an embedded view of the contents - * (which means it'll be added to the parents html without a wrapping ds-item-edit-bitstream element) - */ -export class ItemEditBitstreamComponent implements OnChanges, OnDestroy, OnInit { - - /** - * The view on the bitstream - */ - @ViewChild('bitstreamView', { static: true }) bitstreamView; - - /** - * The current field, value and state of the bitstream - */ - @Input() fieldUpdate: FieldUpdate; - - /** - * The url of the bundle - */ - @Input() bundleUrl: string; - - /** - * The bootstrap sizes used for the columns within this table - */ - @Input() columnSizes: ResponsiveTableSizes; - - /** - * The bitstream of this field - */ - bitstream: Bitstream; - - /** - * The bitstream's name - */ - bitstreamName: string; - - /** - * The bitstream's download url - */ - bitstreamDownloadUrl: string; - - /** - * The format of the bitstream - */ - format$: Observable; - - constructor(private objectUpdatesService: ObjectUpdatesService, - private dsoNameService: DSONameService, - private viewContainerRef: ViewContainerRef) { - } - - ngOnInit(): void { - this.viewContainerRef.createEmbeddedView(this.bitstreamView); - } - - ngOnDestroy(): void { - this.viewContainerRef.clear(); - } - - /** - * Update the current bitstream and its format on changes - * @param changes - */ - ngOnChanges(changes: SimpleChanges): void { - this.bitstream = cloneDeep(this.fieldUpdate.field) as Bitstream; - this.bitstreamName = this.dsoNameService.getName(this.bitstream); - this.bitstreamDownloadUrl = getBitstreamDownloadRoute(this.bitstream); - this.format$ = this.bitstream.format.pipe( - getFirstSucceededRemoteData(), - getRemoteDataPayload(), - ); - } - - /** - * Sends a new remove update for this field to the object updates service - */ - remove(): void { - this.objectUpdatesService.saveRemoveFieldUpdate(this.bundleUrl, this.bitstream); - } - - /** - * Cancels the current update for this field in the object updates service - */ - undo(): void { - this.objectUpdatesService.removeSingleFieldUpdate(this.bundleUrl, this.bitstream.uuid); - } - - /** - * Check if a user should be allowed to remove this field - */ - canRemove(): boolean { - return this.fieldUpdate.changeType !== FieldChangeType.REMOVE; - } - - /** - * Check if a user should be allowed to cancel the update to this field - */ - canUndo(): boolean { - return this.fieldUpdate.changeType >= 0; - } - -} diff --git a/src/app/item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.html b/src/app/item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.html index 57287fdfe2e..0aa7b27d2e4 100644 --- a/src/app/item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.html +++ b/src/app/item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.html @@ -22,18 +22,18 @@

{{'item.edit.item-mapper.head' | translate}}

-
  • +
  • {{'item.edit.item-mapper.tabs.map' | translate}}
    - - +
    diff --git a/src/app/item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts b/src/app/item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts index bc962e27ffe..3c93780a873 100644 --- a/src/app/item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts +++ b/src/app/item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts @@ -61,6 +61,7 @@ import { NotificationsService } from '../../../shared/notifications/notification import { CollectionSelectComponent } from '../../../shared/object-select/collection-select/collection-select.component'; import { PaginatedSearchOptions } from '../../../shared/search/models/paginated-search-options.model'; import { ThemedSearchFormComponent } from '../../../shared/search-form/themed-search-form.component'; +import { BrowserOnlyPipe } from '../../../shared/utils/browser-only.pipe'; import { getItemPageRoute } from '../../item-page-routing-paths'; @Component({ @@ -79,6 +80,7 @@ import { getItemPageRoute } from '../../item-page-routing-paths'; AsyncPipe, TranslateModule, NgIf, + BrowserOnlyPipe, ], standalone: true, }) diff --git a/src/app/item-page/edit-item-page/item-delete/item-delete.component.html b/src/app/item-page/edit-item-page/item-delete/item-delete.component.html index 5191e705d0b..ed6ba3eabcf 100644 --- a/src/app/item-page/edit-item-page/item-delete/item-delete.component.html +++ b/src/app/item-page/edit-item-page/item-delete/item-delete.component.html @@ -6,30 +6,29 @@

    {{headerMessage | translate: {id: item.handle} }}

    {{descriptionMessage | translate}}

    - +
    {{'virtual-metadata.delete-item.info' | translate}} -
    - -
    +
    -
    +
    -
    - {{getRelationshipMessageKey(getLabel(type) | async) | translate}} +
    + {{getRelationshipMessageKey(typeDto.label$ | async) | translate}}
    -
    - +
    -
    +
    diff --git a/src/app/item-page/edit-item-page/item-delete/item-delete.component.ts b/src/app/item-page/edit-item-page/item-delete/item-delete.component.ts index dd1e08c98d1..ae791ccd8e7 100644 --- a/src/app/item-page/edit-item-page/item-delete/item-delete.component.ts +++ b/src/app/item-page/edit-item-page/item-delete/item-delete.component.ts @@ -1,3 +1,4 @@ +// eslint-disable-next-line max-classes-per-file import { AsyncPipe, NgForOf, @@ -68,6 +69,34 @@ import { ModifyItemOverviewComponent } from '../modify-item-overview/modify-item import { AbstractSimpleItemActionComponent } from '../simple-item-action/abstract-simple-item-action.component'; import { VirtualMetadata } from '../virtual-metadata/virtual-metadata.component'; +/** + * Data Transfer Object used to prevent the HTML template to call function returning Observables + */ +class RelationshipTypeDTO { + + relationshipType: RelationshipType; + + isSelected$: Observable; + + label$: Observable; + + relationshipDTOs$: Observable; + +} + +/** + * Data Transfer Object used to prevent the HTML template to call function returning Observables + */ +class RelationshipDTO { + + relationship: Relationship; + + relatedItem$: Observable; + + virtualMetadata$: Observable; + +} + @Component({ selector: 'ds-item-delete', templateUrl: '../item-delete/item-delete.component.html', @@ -106,7 +135,7 @@ export class ItemDeleteComponent * A list of the relationship types for which this item has relations as an observable. * The list doesn't contain duplicates. */ - types$: BehaviorSubject = new BehaviorSubject([]); + typeDTOs$: BehaviorSubject = new BehaviorSubject([]); /** * A map which stores the relationships of this item for each type as observable lists @@ -135,6 +164,8 @@ export class ItemDeleteComponent */ private subs: Subscription[] = []; + public isDeleting$: BehaviorSubject = new BehaviorSubject(false); + constructor(protected route: ActivatedRoute, protected router: Router, protected notificationsService: NotificationsService, @@ -189,13 +220,24 @@ export class ItemDeleteComponent ), ); }), - ).subscribe((types: RelationshipType[]) => this.types$.next(types))); + ).subscribe((types: RelationshipType[]) => this.typeDTOs$.next(types.map((relationshipType: RelationshipType) => Object.assign(new RelationshipTypeDTO(), { + relationshipType: relationshipType, + isSelected$: this.isSelected(relationshipType), + label$: this.getLabel(relationshipType), + relationshipDTOs$: this.getRelationships(relationshipType).pipe( + map((relationships: Relationship[]) => relationships.map((relationship: Relationship) => Object.assign(new RelationshipDTO(), { + relationship: relationship, + relatedItem$: this.getRelatedItem(relationship), + virtualMetadata$: this.getVirtualMetadata(relationship), + } as RelationshipDTO))), + ), + }))))); } - this.subs.push(this.types$.pipe( + this.subs.push(this.typeDTOs$.pipe( take(1), - ).subscribe((types) => - this.objectUpdatesService.initialize(this.url, types, this.item.lastModified), + ).subscribe((types: RelationshipTypeDTO[]) => + this.objectUpdatesService.initialize(this.url, types.map((relationshipTypeDto: RelationshipTypeDTO) => relationshipTypeDto.relationshipType), this.item.lastModified), )); } @@ -278,8 +320,8 @@ export class ItemDeleteComponent this.linkService.resolveLinks( relationship, followLink('relationshipType'), - followLink('leftItem'), - followLink('rightItem'), + followLink('leftItem', undefined, followLink('accessStatus')), + followLink('rightItem', undefined, followLink('accessStatus')), ); return relationship.relationshipType.pipe( getFirstSucceededRemoteData(), @@ -368,34 +410,33 @@ export class ItemDeleteComponent * @param selected whether the type should be selected */ setSelected(type: RelationshipType, selected: boolean): void { - this.objectUpdatesService.setSelectedVirtualMetadata(this.url, this.item.uuid, type.uuid, selected); + if (this.isDeleting$.value === false) { + this.objectUpdatesService.setSelectedVirtualMetadata(this.url, this.item.uuid, type.uuid, selected); + } } /** * Perform the delete operation */ - performAction() { - - this.subs.push(this.types$.pipe( - switchMap((types) => + performAction(): void { + this.isDeleting$.next(true); + this.subs.push(this.typeDTOs$.pipe( + switchMap((types: RelationshipTypeDTO[]) => combineLatest( - types.map((type) => this.isSelected(type)), + types.map((type: RelationshipTypeDTO) => type.isSelected$), ).pipe( defaultIfEmpty([]), - map((selection) => types.filter( - (type, index) => selection[index], + map((selection: boolean[]) => types.filter( + (type: RelationshipTypeDTO, index: number) => selection[index], )), - map((selectedTypes) => selectedTypes.map((type) => type.id)), + map((selectedDtoTypes: RelationshipTypeDTO[]) => selectedDtoTypes.map((typeDto: RelationshipTypeDTO) => typeDto.relationshipType.id)), ), ), - switchMap((types) => - this.itemDataService.delete(this.item.id, types).pipe(getFirstCompletedRemoteData()), - ), - ).subscribe( - (rd: RemoteData) => { - this.notify(rd.hasSucceeded); - }, - )); + switchMap((types: string[]) => this.itemDataService.delete(this.item.id, types)), + getFirstCompletedRemoteData(), + ).subscribe((rd: RemoteData) => { + this.notify(rd.hasSucceeded); + })); } /** @@ -405,10 +446,10 @@ export class ItemDeleteComponent notify(succeeded: boolean) { if (succeeded) { this.notificationsService.success(this.translateService.get('item.edit.' + this.messageKey + '.success')); - this.router.navigate(['']); + void this.router.navigate(['']); } else { this.notificationsService.error(this.translateService.get('item.edit.' + this.messageKey + '.error')); - this.router.navigate([getItemEditRoute(this.item)]); + void this.router.navigate([getItemEditRoute(this.item)]); } } diff --git a/src/app/item-page/edit-item-page/item-move/item-move.component.html b/src/app/item-page/edit-item-page/item-move/item-move.component.html index d136f003f4e..8c47d4f781a 100644 --- a/src/app/item-page/edit-item-page/item-move/item-move.component.html +++ b/src/app/item-page/edit-item-page/item-move/item-move.component.html @@ -6,7 +6,7 @@

    {{'item.edit.move.head' | translate: {id: (itemRD$ | async)?.payload?.handle
    -
    {{'dso-selector.placeholder' | translate: { type: 'collection' } }}
    +
    {{'dso-selector.placeholder' | translate: { type: 'dso-selector.placeholder.type.collection' | translate } }}
    - +
    + {{'item.edit.tabs.status.buttons.' + operation.operationKey + '.label' | translate}}
    -
    +
    - - + +
    + +
    +
    +
    +
    - - -
    - -
    -
    - -
    -
    -
    - - - -
    +
    +
    +
    +
    - -
    + + + + {{ 'item.edit.relationships.no-entity-type' | translate }} + + + + + + + + +
    + + + +
    +
    diff --git a/src/app/item-page/edit-item-page/item-relationships/item-relationships.component.spec.ts b/src/app/item-page/edit-item-page/item-relationships/item-relationships.component.spec.ts index 106edb08efc..28380b3f279 100644 --- a/src/app/item-page/edit-item-page/item-relationships/item-relationships.component.spec.ts +++ b/src/app/item-page/edit-item-page/item-relationships/item-relationships.component.spec.ts @@ -13,12 +13,10 @@ import { Router, } from '@angular/router'; import { TranslateModule } from '@ngx-translate/core'; -import { getTestScheduler } from 'jasmine-marbles'; import { combineLatest as observableCombineLatest, of as observableOf, } from 'rxjs'; -import { TestScheduler } from 'rxjs/testing'; import { ObjectCacheService } from '../../../core/cache/object-cache.service'; import { RestResponse } from '../../../core/cache/response.models'; @@ -33,6 +31,7 @@ import { Item } from '../../../core/shared/item.model'; import { ItemType } from '../../../core/shared/item-relationships/item-type.model'; import { Relationship } from '../../../core/shared/item-relationships/relationship.model'; import { RelationshipType } from '../../../core/shared/item-relationships/relationship-type.model'; +import { AlertComponent } from '../../../shared/alert/alert.component'; import { getMockThemeService } from '../../../shared/mocks/theme-service.mock'; import { INotification, @@ -44,6 +43,7 @@ import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$, } from '../../../shared/remote-data.utils'; +import { ItemDataServiceStub } from '../../../shared/testing/item-data.service.stub'; import { relationshipTypes } from '../../../shared/testing/relationship-types.mock'; import { RouterStub } from '../../../shared/testing/router.stub'; import { createPaginatedList } from '../../../shared/testing/utils.test'; @@ -72,12 +72,11 @@ const notificationsService = jasmine.createSpyObj('notificationsService', const router = new RouterStub(); let relationshipTypeService; let routeStub; -let itemService; +let itemService: ItemDataServiceStub; const url = 'http://test-url.com/test-url'; router.url = url; -let scheduler: TestScheduler; let item; let author1; let author2; @@ -157,10 +156,7 @@ describe('ItemRelationshipsComponent', () => { changeType: FieldChangeType.REMOVE, }; - itemService = jasmine.createSpyObj('itemService', { - findByHref: createSuccessfulRemoteDataObject$(item), - findById: createSuccessfulRemoteDataObject$(item), - }); + itemService = new ItemDataServiceStub(); routeStub = { data: observableOf({}), parent: { @@ -228,7 +224,6 @@ describe('ItemRelationshipsComponent', () => { }, ); - scheduler = getTestScheduler(); TestBed.configureTestingModule({ imports: [TranslateModule.forRoot(), ItemRelationshipsComponent], providers: [ @@ -247,10 +242,18 @@ describe('ItemRelationshipsComponent', () => { ], schemas: [ NO_ERRORS_SCHEMA, ], + }).overrideComponent(ItemRelationshipsComponent, { + remove: { + imports: [ + AlertComponent, + ], + }, }).compileComponents(); })); beforeEach(() => { + spyOn(itemService, 'findByHref').and.returnValue(item); + spyOn(itemService, 'findById').and.returnValue(item); fixture = TestBed.createComponent(ItemRelationshipsComponent); comp = fixture.componentInstance; de = fixture.debugElement; @@ -285,7 +288,7 @@ describe('ItemRelationshipsComponent', () => { }); it('it should delete the correct relationship', () => { - expect(relationshipService.deleteRelationship).toHaveBeenCalledWith(relationships[1].uuid, 'left'); + expect(relationshipService.deleteRelationship).toHaveBeenCalledWith(relationships[1].uuid, 'left', false); }); }); diff --git a/src/app/item-page/edit-item-page/item-relationships/item-relationships.component.ts b/src/app/item-page/edit-item-page/item-relationships/item-relationships.component.ts index ea9b571cd62..7b0cc466474 100644 --- a/src/app/item-page/edit-item-page/item-relationships/item-relationships.component.ts +++ b/src/app/item-page/edit-item-page/item-relationships/item-relationships.component.ts @@ -2,6 +2,7 @@ import { AsyncPipe, NgForOf, NgIf, + NgTemplateOutlet, } from '@angular/common'; import { ChangeDetectorRef, @@ -17,63 +18,55 @@ import { TranslateService, } from '@ngx-translate/core'; import { - combineLatest as observableCombineLatest, + BehaviorSubject, Observable, - of as observableOf, - zip as observableZip, } from 'rxjs'; import { + distinctUntilChanged, map, - startWith, - switchMap, - take, } from 'rxjs/operators'; import { ObjectCacheService } from '../../../core/cache/object-cache.service'; import { EntityTypeDataService } from '../../../core/data/entity-type-data.service'; import { ItemDataService } from '../../../core/data/item-data.service'; -import { FieldChangeType } from '../../../core/data/object-updates/field-change-type.model'; -import { FieldUpdate } from '../../../core/data/object-updates/field-update.model'; -import { FieldUpdates } from '../../../core/data/object-updates/field-updates.model'; -import { - DeleteRelationship, - RelationshipIdentifiable, -} from '../../../core/data/object-updates/object-updates.reducer'; import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service'; import { PaginatedList } from '../../../core/data/paginated-list.model'; import { RelationshipDataService } from '../../../core/data/relationship-data.service'; import { RelationshipTypeDataService } from '../../../core/data/relationship-type-data.service'; -import { RemoteData } from '../../../core/data/remote-data'; import { RequestService } from '../../../core/data/request.service'; -import { Item } from '../../../core/shared/item.model'; import { ItemType } from '../../../core/shared/item-relationships/item-type.model'; -import { Relationship } from '../../../core/shared/item-relationships/relationship.model'; import { RelationshipType } from '../../../core/shared/item-relationships/relationship-type.model'; -import { NoContent } from '../../../core/shared/NoContent.model'; import { getFirstSucceededRemoteData, getRemoteDataPayload, } from '../../../core/shared/operators'; -import { hasValue } from '../../../shared/empty.util'; +import { AlertComponent } from '../../../shared/alert/alert.component'; +import { AlertType } from '../../../shared/alert/alert-type'; import { ThemedLoadingComponent } from '../../../shared/loading/themed-loading.component'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { followLink } from '../../../shared/utils/follow-link-config.model'; import { VarDirective } from '../../../shared/utils/var.directive'; +import { compareArraysUsingIds } from '../../simple/item-types/shared/item-relationships-utils'; import { AbstractItemUpdateComponent } from '../abstract-item-update/abstract-item-update.component'; +import { EditItemRelationshipsService } from './edit-item-relationships.service'; import { EditRelationshipListComponent } from './edit-relationship-list/edit-relationship-list.component'; +import { EditRelationshipListWrapperComponent } from './edit-relationship-list-wrapper/edit-relationship-list-wrapper.component'; @Component({ selector: 'ds-item-relationships', styleUrls: ['./item-relationships.component.scss'], templateUrl: './item-relationships.component.html', imports: [ - ThemedLoadingComponent, + AlertComponent, AsyncPipe, - TranslateModule, - NgIf, EditRelationshipListComponent, NgForOf, + NgIf, + NgTemplateOutlet, + ThemedLoadingComponent, + TranslateModule, VarDirective, + EditRelationshipListWrapperComponent, ], standalone: true, }) @@ -91,7 +84,13 @@ export class ItemRelationshipsComponent extends AbstractItemUpdateComponent { /** * The item's entity type as an observable */ - entityType$: Observable; + entityType$: BehaviorSubject = new BehaviorSubject(undefined); + + get isSaving$(): BehaviorSubject { + return this.editItemRelationshipsService.isSaving$; + } + + readonly AlertType = AlertType; constructor( public itemService: ItemDataService, @@ -107,6 +106,7 @@ export class ItemRelationshipsComponent extends AbstractItemUpdateComponent { protected relationshipTypeService: RelationshipTypeDataService, public cdr: ChangeDetectorRef, protected modalService: NgbModal, + protected editItemRelationshipsService: EditItemRelationshipsService, ) { super(itemService, objectUpdatesService, router, notificationsService, translateService, route); } @@ -118,18 +118,18 @@ export class ItemRelationshipsComponent extends AbstractItemUpdateComponent { const label = this.item.firstMetadataValue('dspace.entity.type'); if (label !== undefined) { - this.relationshipTypes$ = this.relationshipTypeService.searchByEntityType(label, true, true, ...this.getRelationshipTypeFollowLinks()) - .pipe( - map((relationshipTypes: PaginatedList) => relationshipTypes.page), - ); + this.relationshipTypes$ = this.relationshipTypeService.searchByEntityType(label, true, true, ...this.getRelationshipTypeFollowLinks()).pipe( + map((relationshipTypes: PaginatedList) => relationshipTypes.page), + distinctUntilChanged(compareArraysUsingIds()), + ); - this.entityType$ = this.entityTypeService.getEntityTypeByLabel(label).pipe( + this.entityTypeService.getEntityTypeByLabel(label).pipe( getFirstSucceededRemoteData(), getRemoteDataPayload(), - ); + ).subscribe((type) => this.entityType$.next(type)); } else { - this.entityType$ = observableOf(undefined); + this.entityType$.next(undefined); } } @@ -145,127 +145,24 @@ export class ItemRelationshipsComponent extends AbstractItemUpdateComponent { * Make sure the lists are refreshed afterwards and notifications are sent for success and errors */ public submit(): void { - - // Get all the relationships that should be removed - const removedRelationshipIDs$: Observable = this.relationshipService.getItemRelationshipsArray(this.item).pipe( - startWith([]), - map((relationships: Relationship[]) => relationships.map((relationship) => - Object.assign(new Relationship(), relationship, { uuid: relationship.id }), - )), - switchMap((relationships: Relationship[]) => { - return this.objectUpdatesService.getFieldUpdatesExclusive(this.url, relationships) as Observable; - }), - map((fieldUpdates: FieldUpdates) => - Object.values(fieldUpdates) - .filter((fieldUpdate: FieldUpdate) => fieldUpdate.changeType === FieldChangeType.REMOVE) - .map((fieldUpdate: FieldUpdate) => fieldUpdate.field as DeleteRelationship), - ), - ); - - const addRelatedItems$: Observable = this.objectUpdatesService.getFieldUpdates(this.url, []).pipe( - map((fieldUpdates: FieldUpdates) => - Object.values(fieldUpdates) - .filter((fieldUpdate: FieldUpdate) => hasValue(fieldUpdate)) - .filter((fieldUpdate: FieldUpdate) => fieldUpdate.changeType === FieldChangeType.ADD) - .map((fieldUpdate: FieldUpdate) => fieldUpdate.field as RelationshipIdentifiable), - ), - ); - - observableCombineLatest( - removedRelationshipIDs$, - addRelatedItems$, - ).pipe( - take(1), - ).subscribe(([removeRelationshipIDs, addRelatedItems]) => { - const actions = [ - this.deleteRelationships(removeRelationshipIDs), - this.addRelationships(addRelatedItems), - ]; - actions.forEach((action) => - action.subscribe((response) => { - if (response.length > 0) { - this.initializeOriginalFields(); - this.cdr.detectChanges(); - this.displayNotifications(response); - this.modalService.dismissAll(); - } - }), - ); - }); - } - - deleteRelationships(deleteRelationshipIDs: DeleteRelationship[]): Observable[]> { - return observableZip(...deleteRelationshipIDs.map((deleteRelationship) => { - let copyVirtualMetadata: string; - if (deleteRelationship.keepLeftVirtualMetadata && deleteRelationship.keepRightVirtualMetadata) { - copyVirtualMetadata = 'all'; - } else if (deleteRelationship.keepLeftVirtualMetadata) { - copyVirtualMetadata = 'left'; - } else if (deleteRelationship.keepRightVirtualMetadata) { - copyVirtualMetadata = 'right'; - } else { - copyVirtualMetadata = 'none'; - } - return this.relationshipService.deleteRelationship(deleteRelationship.uuid, copyVirtualMetadata); - }, - )); + this.editItemRelationshipsService.submit(this.item, this.url); } - addRelationships(addRelatedItems: RelationshipIdentifiable[]): Observable[]> { - return observableZip(...addRelatedItems.map((addRelationship) => - this.entityType$.pipe( - switchMap((entityType) => this.entityTypeService.isLeftType(addRelationship.type, entityType)), - switchMap((isLeftType) => { - let leftItem: Item; - let rightItem: Item; - let leftwardValue: string; - let rightwardValue: string; - if (isLeftType) { - leftItem = this.item; - rightItem = addRelationship.relatedItem; - leftwardValue = null; - rightwardValue = addRelationship.nameVariant; - } else { - leftItem = addRelationship.relatedItem; - rightItem = this.item; - leftwardValue = addRelationship.nameVariant; - rightwardValue = null; - } - return this.relationshipService.addRelationship(addRelationship.type.id, leftItem, rightItem, leftwardValue, rightwardValue); - }), - ), - )); - } - - /** - * Display notifications - * - Error notification for each failed response with their message - * - Success notification in case there's at least one successful response - * @param responses - */ - displayNotifications(responses: RemoteData[]) { - const failedResponses = responses.filter((response: RemoteData) => response.hasFailed); - const successfulResponses = responses.filter((response: RemoteData) => response.hasSucceeded); - - failedResponses.forEach((response: RemoteData) => { - this.notificationsService.error(this.getNotificationTitle('failed'), response.errorMessage); - }); - if (successfulResponses.length > 0) { - this.notificationsService.success(this.getNotificationTitle('saved'), this.getNotificationContent('saved')); - } - } /** * Sends all initial values of this item to the object updates service */ public initializeOriginalFields() { - return this.relationshipService.getRelatedItems(this.item).pipe( - take(1), - ).subscribe((items: Item[]) => { - this.objectUpdatesService.initialize(this.url, items, this.item.lastModified); - }); + return this.editItemRelationshipsService.initializeOriginalFields(this.item, this.url); } + /** + * Method to prevent unnecessary for loop re-rendering + */ + trackById(index: number, relationshipType: RelationshipType): string { + return relationshipType.id; + } + getRelationshipTypeFollowLinks() { return [ followLink('leftType'), diff --git a/src/app/item-page/edit-item-page/item-status/item-status.component.html b/src/app/item-page/edit-item-page/item-status/item-status.component.html index faad0e679b8..7d6e2e95599 100644 --- a/src/app/item-page/edit-item-page/item-status/item-status.component.html +++ b/src/app/item-page/edit-item-page/item-status/item-status.component.html @@ -1,28 +1,28 @@

    {{'item.edit.tabs.status.description' | translate}}

    -
    -
    +
    +
    {{'item.edit.tabs.status.labels.' + statusKey | translate}}:
    -
    +
    {{statusData[statusKey]}}
    -
    +
    -
    +
    {{identifier.identifierType.toLocaleUpperCase()}}
    -
    {{identifier.value}} +
    {{identifier.value}} ({{"item.edit.identifiers.doi.status."+identifier.identifierStatus|translate}})
    -
    +
    {{'item.edit.tabs.status.labels.itemPage' | translate}}:
    -
    + diff --git a/src/app/item-page/edit-item-page/item-status/item-status.component.ts b/src/app/item-page/edit-item-page/item-status/item-status.component.ts index f330c2e457c..1bb17dc77c5 100644 --- a/src/app/item-page/edit-item-page/item-status/item-status.component.ts +++ b/src/app/item-page/edit-item-page/item-status/item-status.component.ts @@ -7,6 +7,7 @@ import { import { ChangeDetectionStrategy, Component, + OnDestroy, OnInit, } from '@angular/core'; import { @@ -58,7 +59,7 @@ import { ItemOperationComponent } from '../item-operation/item-operation.compone import { ItemOperation } from '../item-operation/itemOperation.model'; @Component({ - selector: 'ds-item-status', + selector: 'ds-base-item-status', templateUrl: './item-status.component.html', changeDetection: ChangeDetectionStrategy.Default, animations: [ @@ -79,7 +80,7 @@ import { ItemOperation } from '../item-operation/itemOperation.model'; /** * Component for displaying an item's status */ -export class ItemStatusComponent implements OnInit { +export class ItemStatusComponent implements OnInit, OnDestroy { /** * The item to display the status for diff --git a/src/app/item-page/edit-item-page/item-status/themed-item-status.component.ts b/src/app/item-page/edit-item-page/item-status/themed-item-status.component.ts index 705ef6d8adc..c3ba17a4b51 100644 --- a/src/app/item-page/edit-item-page/item-status/themed-item-status.component.ts +++ b/src/app/item-page/edit-item-page/item-status/themed-item-status.component.ts @@ -4,10 +4,11 @@ import { ThemedComponent } from '../../../shared/theme-support/themed.component' import { ItemStatusComponent } from './item-status.component'; @Component({ - selector: 'ds-themed-item-status', + selector: 'ds-item-status', styleUrls: [], templateUrl: '../../../shared/theme-support/themed.component.html', standalone: true, + imports: [ItemStatusComponent], }) export class ThemedItemStatusComponent extends ThemedComponent { protected getComponentName(): string { diff --git a/src/app/item-page/edit-item-page/item-version-history/item-version-history.component.ts b/src/app/item-page/edit-item-page/item-version-history/item-version-history.component.ts index 9c612311648..a414af0ce4a 100644 --- a/src/app/item-page/edit-item-page/item-version-history/item-version-history.component.ts +++ b/src/app/item-page/edit-item-page/item-version-history/item-version-history.component.ts @@ -2,7 +2,10 @@ import { AsyncPipe, NgIf, } from '@angular/common'; -import { Component } from '@angular/core'; +import { + Component, + OnInit, +} from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; @@ -28,7 +31,7 @@ import { ItemVersionsComponent } from '../../versions/item-versions.component'; /** * Component for listing and managing an item's version history */ -export class ItemVersionHistoryComponent { +export class ItemVersionHistoryComponent implements OnInit { /** * The item to display the version history for */ diff --git a/src/app/item-page/edit-item-page/modify-item-overview/modify-item-overview.component.spec.ts b/src/app/item-page/edit-item-page/modify-item-overview/modify-item-overview.component.spec.ts index 45759aa90bc..a722d07c9da 100644 --- a/src/app/item-page/edit-item-page/modify-item-overview/modify-item-overview.component.spec.ts +++ b/src/app/item-page/edit-item-page/modify-item-overview/modify-item-overview.component.spec.ts @@ -37,6 +37,7 @@ describe('ModifyItemOverviewComponent', () => { fixture = TestBed.createComponent(ModifyItemOverviewComponent); comp = fixture.componentInstance; comp.item = mockItem; + comp.ngOnChanges(); fixture.detectChanges(); }); diff --git a/src/app/item-page/edit-item-page/modify-item-overview/modify-item-overview.component.ts b/src/app/item-page/edit-item-page/modify-item-overview/modify-item-overview.component.ts index 2d4b622ea92..3bd5024837d 100644 --- a/src/app/item-page/edit-item-page/modify-item-overview/modify-item-overview.component.ts +++ b/src/app/item-page/edit-item-page/modify-item-overview/modify-item-overview.component.ts @@ -5,7 +5,7 @@ import { import { Component, Input, - OnInit, + OnChanges, } from '@angular/core'; import { TranslateModule } from '@ngx-translate/core'; @@ -21,12 +21,12 @@ import { MetadataMap } from '../../../core/shared/metadata.models'; /** * Component responsible for rendering a table containing the metadatavalues from the to be edited item */ -export class ModifyItemOverviewComponent implements OnInit { +export class ModifyItemOverviewComponent implements OnChanges { @Input() item: Item; metadata: MetadataMap; - ngOnInit(): void { - this.metadata = this.item.metadata; + ngOnChanges(): void { + this.metadata = this.item?.metadata; } } diff --git a/src/app/item-page/edit-item-page/virtual-metadata/virtual-metadata.component.html b/src/app/item-page/edit-item-page/virtual-metadata/virtual-metadata.component.html index b83b93d8f10..927f7dbb48d 100644 --- a/src/app/item-page/edit-item-page/virtual-metadata/virtual-metadata.component.html +++ b/src/app/item-page/edit-item-page/virtual-metadata/virtual-metadata.component.html @@ -5,9 +5,9 @@

    @@ -156,7 +156,7 @@

    -
    +
    +
    -
    +
    - +
    {{ (outputLogs$ | async) }}

    - + {{tableEntry.info}} -
    - +
    - - + +
    - +
    - +
    @@ -29,5 +29,7 @@
    - +
    + + diff --git a/src/app/root/root.component.spec.ts b/src/app/root/root.component.spec.ts index 9f7f8f4f0e8..1abc3ffd176 100644 --- a/src/app/root/root.component.spec.ts +++ b/src/app/root/root.component.spec.ts @@ -5,46 +5,22 @@ import { TestBed, } from '@angular/core/testing'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; -import { - ActivatedRoute, - Router, -} from '@angular/router'; -import { StoreModule } from '@ngrx/store'; -import { provideMockStore } from '@ngrx/store/testing'; -import { - TranslateLoader, - TranslateModule, -} from '@ngx-translate/core'; +import { Router } from '@angular/router'; +import { TranslateModule } from '@ngx-translate/core'; import { ThemedAdminSidebarComponent } from '../admin/admin-sidebar/themed-admin-sidebar.component'; -import { storeModuleConfig } from '../app.reducer'; import { ThemedBreadcrumbsComponent } from '../breadcrumbs/themed-breadcrumbs.component'; -import { authReducer } from '../core/auth/auth.reducer'; -import { AuthService } from '../core/auth/auth.service'; -import { LocaleService } from '../core/locale/locale.service'; -import { MetadataService } from '../core/metadata/metadata.service'; -import { RouteService } from '../core/services/route.service'; -import { - NativeWindowRef, - NativeWindowService, -} from '../core/services/window.service'; import { ThemedFooterComponent } from '../footer/themed-footer.component'; import { ThemedHeaderNavbarWrapperComponent } from '../header-nav-wrapper/themed-header-navbar-wrapper.component'; import { HostWindowService } from '../shared/host-window.service'; import { ThemedLoadingComponent } from '../shared/loading/themed-loading.component'; import { MenuService } from '../shared/menu/menu.service'; -import { MockActivatedRoute } from '../shared/mocks/active-router.mock'; -import { AngularticsProviderMock } from '../shared/mocks/angulartics-provider.service.mock'; -import { AuthServiceMock } from '../shared/mocks/auth.service.mock'; -import { MetadataServiceMock } from '../shared/mocks/metadata-service.mock'; import { RouterMock } from '../shared/mocks/router.mock'; -import { TranslateLoaderMock } from '../shared/mocks/translate-loader.mock'; import { NotificationsBoardComponent } from '../shared/notifications/notifications-board/notifications-board.component'; import { CSSVariableService } from '../shared/sass-helper/css-variable.service'; import { CSSVariableServiceStub } from '../shared/testing/css-variable-service.stub'; import { HostWindowServiceStub } from '../shared/testing/host-window-service.stub'; import { MenuServiceStub } from '../shared/testing/menu-service.stub'; -import { Angulartics2DSpace } from '../statistics/angulartics/dspace-provider'; import { SystemWideAlertBannerComponent } from '../system-wide-alert/alert-banner/system-wide-alert-banner.component'; import { RootComponent } from './root.component'; @@ -57,29 +33,14 @@ describe('RootComponent', () => { imports: [ CommonModule, NoopAnimationsModule, - StoreModule.forRoot(authReducer, storeModuleConfig), - TranslateModule.forRoot({ - loader: { - provide: TranslateLoader, - useClass: TranslateLoaderMock, - }, - }), + TranslateModule.forRoot(), RootComponent, ], providers: [ - { provide: NativeWindowService, useValue: new NativeWindowRef() }, - { provide: MetadataService, useValue: new MetadataServiceMock() }, - { provide: Angulartics2DSpace, useValue: new AngularticsProviderMock() }, - { provide: AuthService, useValue: new AuthServiceMock() }, { provide: Router, useValue: new RouterMock() }, - { provide: ActivatedRoute, useValue: new MockActivatedRoute() }, { provide: MenuService, useValue: new MenuServiceStub() }, { provide: CSSVariableService, useClass: CSSVariableServiceStub }, { provide: HostWindowService, useValue: new HostWindowServiceStub(800) }, - { provide: LocaleService, useValue: {} }, - provideMockStore({ core: { auth: { loading: false } } } as any), - RootComponent, - RouteService, ], schemas: [CUSTOM_ELEMENTS_SCHEMA], }) diff --git a/src/app/root/root.component.ts b/src/app/root/root.component.ts index d98fd85b0a7..b5c1c9be48e 100644 --- a/src/app/root/root.component.ts +++ b/src/app/root/root.component.ts @@ -1,9 +1,11 @@ import { AsyncPipe, + NgClass, NgIf, } from '@angular/common'; import { Component, + Inject, Input, OnInit, } from '@angular/core'; @@ -13,6 +15,7 @@ import { } from '@angular/router'; import { TranslateModule } from '@ngx-translate/core'; import { + BehaviorSubject, combineLatest as combineLatestObservable, Observable, of, @@ -30,10 +33,15 @@ import { environment } from '../../environments/environment'; import { ThemedAdminSidebarComponent } from '../admin/admin-sidebar/themed-admin-sidebar.component'; import { getPageInternalServerErrorRoute } from '../app-routing-paths'; import { ThemedBreadcrumbsComponent } from '../breadcrumbs/themed-breadcrumbs.component'; +import { + NativeWindowRef, + NativeWindowService, +} from '../core/services/window.service'; import { ThemedFooterComponent } from '../footer/themed-footer.component'; import { ThemedHeaderNavbarWrapperComponent } from '../header-nav-wrapper/themed-header-navbar-wrapper.component'; import { slideSidebarPadding } from '../shared/animations/slide'; import { HostWindowService } from '../shared/host-window.service'; +import { LiveRegionComponent } from '../shared/live-region/live-region.component'; import { ThemedLoadingComponent } from '../shared/loading/themed-loading.component'; import { MenuService } from '../shared/menu/menu.service'; import { MenuID } from '../shared/menu/menu-id.model'; @@ -42,12 +50,26 @@ import { CSSVariableService } from '../shared/sass-helper/css-variable.service'; import { SystemWideAlertBannerComponent } from '../system-wide-alert/alert-banner/system-wide-alert-banner.component'; @Component({ - selector: 'ds-root', + selector: 'ds-base-root', templateUrl: './root.component.html', styleUrls: ['./root.component.scss'], animations: [slideSidebarPadding], standalone: true, - imports: [TranslateModule, ThemedAdminSidebarComponent, SystemWideAlertBannerComponent, ThemedHeaderNavbarWrapperComponent, ThemedBreadcrumbsComponent, NgIf, ThemedLoadingComponent, RouterOutlet, ThemedFooterComponent, NotificationsBoardComponent, AsyncPipe], + imports: [ + TranslateModule, + ThemedAdminSidebarComponent, + SystemWideAlertBannerComponent, + ThemedHeaderNavbarWrapperComponent, + ThemedBreadcrumbsComponent, + NgIf, + NgClass, + ThemedLoadingComponent, + RouterOutlet, + ThemedFooterComponent, + NotificationsBoardComponent, + AsyncPipe, + LiveRegionComponent, + ], }) export class RootComponent implements OnInit { theme: Observable = of({} as any); @@ -58,6 +80,8 @@ export class RootComponent implements OnInit { notificationOptions: INotificationBoardOptions; models: any; + browserOsClasses = new BehaviorSubject([]); + /** * Whether or not to show a full screen loader */ @@ -73,11 +97,23 @@ export class RootComponent implements OnInit { private cssService: CSSVariableService, private menuService: MenuService, private windowService: HostWindowService, + @Inject(NativeWindowService) private _window: NativeWindowRef, ) { this.notificationOptions = environment.notifications; } ngOnInit() { + const browserName = this.getBrowserName(); + if (browserName) { + const browserOsClasses = new Array(); + browserOsClasses.push(`browser-${browserName}`); + const osName = this.getOSName(); + if (osName) { + browserOsClasses.push(`browser-${browserName}-${osName}`); + } + this.browserOsClasses.next(browserOsClasses); + } + this.isSidebarVisible$ = this.menuService.isMenuVisibleWithVisibleSections(MenuID.ADMIN); this.expandedSidebarWidth$ = this.cssService.getVariable('--ds-admin-sidebar-total-width').pipe( @@ -108,4 +144,23 @@ export class RootComponent implements OnInit { mainContent.focus(); } } + + getBrowserName(): string { + const userAgent = this._window.nativeWindow.navigator?.userAgent; + if (/Firefox/.test(userAgent)) { + return 'firefox'; + } + if (/Safari/.test(userAgent)) { + return 'safari'; + } + return undefined; + } + + getOSName(): string { + const userAgent = this._window.nativeWindow.navigator?.userAgent; + if (/Windows/.test(userAgent)) { + return 'windows'; + } + return undefined; + } } diff --git a/src/app/root/themed-root.component.ts b/src/app/root/themed-root.component.ts index b2ea91c75da..fe88efa546d 100644 --- a/src/app/root/themed-root.component.ts +++ b/src/app/root/themed-root.component.ts @@ -7,10 +7,11 @@ import { ThemedComponent } from '../shared/theme-support/themed.component'; import { RootComponent } from './root.component'; @Component({ - selector: 'ds-themed-root', + selector: 'ds-root', styleUrls: [], templateUrl: '../shared/theme-support/themed.component.html', standalone: true, + imports: [RootComponent], }) export class ThemedRootComponent extends ThemedComponent { /** diff --git a/src/app/search-navbar/search-navbar.component.ts b/src/app/search-navbar/search-navbar.component.ts index 006a217d57e..3aca706ade9 100644 --- a/src/app/search-navbar/search-navbar.component.ts +++ b/src/app/search-navbar/search-navbar.component.ts @@ -20,7 +20,7 @@ import { ClickOutsideDirective } from '../shared/utils/click-outside.directive'; * The search box in the header that expands on focus and collapses on focus out */ @Component({ - selector: 'ds-search-navbar', + selector: 'ds-base-search-navbar', templateUrl: './search-navbar.component.html', styleUrls: ['./search-navbar.component.scss'], animations: [expandSearchInput], @@ -31,7 +31,7 @@ export class SearchNavbarComponent { // The search form searchForm; - // Whether or not the search bar is expanded, boolean for html ngIf, string fo AngularAnimation state change + // Whether or not the search bar is expanded, boolean for html ngIf, string for AngularAnimation state change searchExpanded = false; isExpanded = 'collapsed'; diff --git a/src/app/search-navbar/themed-search-navbar.component.ts b/src/app/search-navbar/themed-search-navbar.component.ts index 240b39314b8..82348f79430 100644 --- a/src/app/search-navbar/themed-search-navbar.component.ts +++ b/src/app/search-navbar/themed-search-navbar.component.ts @@ -4,10 +4,11 @@ import { ThemedComponent } from '../shared/theme-support/themed.component'; import { SearchNavbarComponent } from './search-navbar.component'; @Component({ - selector: 'ds-themed-search-navbar', + selector: 'ds-search-navbar', styleUrls: [], templateUrl: '../shared/theme-support/themed.component.html', standalone: true, + imports: [SearchNavbarComponent], }) export class ThemedSearchNavbarComponent extends ThemedComponent { diff --git a/src/app/search-page/configuration-search-page.component.spec.ts b/src/app/search-page/configuration-search-page.component.spec.ts index 417e6598992..f3931a2016f 100644 --- a/src/app/search-page/configuration-search-page.component.spec.ts +++ b/src/app/search-page/configuration-search-page.component.spec.ts @@ -8,23 +8,23 @@ import { waitForAsync, } from '@angular/core/testing'; import { Router } from '@angular/router'; +import { of } from 'rxjs'; import { RouteService } from '../core/services/route.service'; import { SearchConfigurationService } from '../core/shared/search/search-configuration.service'; import { configureSearchComponentTestingModule } from '../shared/search/search.component.spec'; import { ConfigurationSearchPageComponent } from './configuration-search-page.component'; import createSpy = jasmine.createSpy; -import { of } from 'rxjs'; const CONFIGURATION = 'test-configuration'; const QUERY = 'test query'; @Component({ template: ` - - + `, imports: [ ConfigurationSearchPageComponent, diff --git a/src/app/search-page/configuration-search-page.component.ts b/src/app/search-page/configuration-search-page.component.ts index 3b28ed447a0..afbe34d550c 100644 --- a/src/app/search-page/configuration-search-page.component.ts +++ b/src/app/search-page/configuration-search-page.component.ts @@ -34,7 +34,7 @@ import { ViewModeSwitchComponent } from '../shared/view-mode-switch/view-mode-sw * This component renders a search page using a configuration as input. */ @Component({ - selector: 'ds-configuration-search-page', + selector: 'ds-base-configuration-search-page', styleUrls: ['../shared/search/search.component.scss'], templateUrl: '../shared/search/search.component.html', changeDetection: ChangeDetectionStrategy.OnPush, diff --git a/src/app/search-page/search-page.component.html b/src/app/search-page/search-page.component.html index 36ba53a885e..8d310607833 100644 --- a/src/app/search-page/search-page.component.html +++ b/src/app/search-page/search-page.component.html @@ -1 +1 @@ - + diff --git a/src/app/search-page/search-page.component.ts b/src/app/search-page/search-page.component.ts index 272987a795c..817cc6818e1 100644 --- a/src/app/search-page/search-page.component.ts +++ b/src/app/search-page/search-page.component.ts @@ -5,7 +5,7 @@ import { SEARCH_CONFIG_SERVICE } from '../my-dspace-page/my-dspace-configuration import { ThemedSearchComponent } from '../shared/search/themed-search.component'; @Component({ - selector: 'ds-search-page', + selector: 'ds-base-search-page', templateUrl: './search-page.component.html', providers: [ { diff --git a/src/app/search-page/themed-configuration-search-page.component.ts b/src/app/search-page/themed-configuration-search-page.component.ts index 0d39dca4303..cf84c9c9d4a 100644 --- a/src/app/search-page/themed-configuration-search-page.component.ts +++ b/src/app/search-page/themed-configuration-search-page.component.ts @@ -15,58 +15,134 @@ import { ConfigurationSearchPageComponent } from './configuration-search-page.co * Themed wrapper for ConfigurationSearchPageComponent */ @Component({ - selector: 'ds-themed-configuration-search-page', + selector: 'ds-configuration-search-page', templateUrl: '../shared/theme-support/themed.component.html', standalone: true, + imports: [ConfigurationSearchPageComponent], }) export class ThemedConfigurationSearchPageComponent extends ThemedComponent { - - @Input() configurationList: SearchConfigurationOption[] = []; - + /** + * The list of available configuration options + */ + @Input() configurationList: SearchConfigurationOption[]; + + /** + * The current context + * If empty, 'search' is used + */ @Input() context: Context; + /** + * The configuration to use for the search options + * If empty, 'default' is used + */ @Input() configuration: string; + /** + * The actual query for the fixed filter. + * If empty, the query will be determined by the route parameter called 'filter' + */ @Input() fixedFilterQuery: string; + /** + * If this is true, the request will only be sent if there's + * no valid cached version. Defaults to true + */ @Input() useCachedVersionIfAvailable: boolean; + /** + * True when the search component should show results on the current page + */ @Input() inPlaceSearch: boolean; + /** + * The link type of the listed search results + */ @Input() linkType: CollectionElementLinkType; + /** + * The pagination id used in the search + */ @Input() paginationId: string; + /** + * Whether or not the search bar should be visible + */ @Input() searchEnabled: boolean; + /** + * The width of the sidebar (bootstrap columns) + */ @Input() sideBarWidth: number; + /** + * The placeholder of the search form input + */ @Input() searchFormPlaceholder: string; + /** + * A boolean representing if result entries are selectable + */ @Input() selectable: boolean; + /** + * The config option used for selection functionality + */ @Input() selectionConfig: SelectionConfig; + /** + * A boolean representing if show csv export button + */ @Input() showCsvExport: boolean; + /** + * A boolean representing if show search sidebar button + */ @Input() showSidebar: boolean; + /** + * Whether to show the thumbnail preview + */ @Input() showThumbnails: boolean; + /** + * Whether to show the view mode switch + */ @Input() showViewModes: boolean; + /** + * List of available view mode + */ @Input() useUniquePageId: boolean; + /** + * List of available view mode + */ @Input() viewModeList: ViewMode[]; + /** + * Defines whether or not to show the scope selector + */ @Input() showScopeSelector: boolean; + /** + * Whether or not to track search statistics by sending updates to the rest api + */ @Input() trackStatistics: boolean; + /** + * The default value for the search query when none is already defined in the {@link SearchConfigurationService} + */ @Input() query: string; + /** + * The fallback scope when no scope is defined in the url, if this is also undefined no scope will be set + */ @Input() scope: string; + /** + * Hides the scope in the url, this can be useful when you hardcode the scope in another way + */ @Input() hideScopeInUrl: boolean; protected inAndOutputNames: (keyof ConfigurationSearchPageComponent & keyof this)[] = [ diff --git a/src/app/search-page/themed-search-page.component.ts b/src/app/search-page/themed-search-page.component.ts index 30e6e6db11d..649ad8a2466 100644 --- a/src/app/search-page/themed-search-page.component.ts +++ b/src/app/search-page/themed-search-page.component.ts @@ -7,10 +7,11 @@ import { SearchPageComponent } from './search-page.component'; * Themed wrapper for SearchPageComponent */ @Component({ - selector: 'ds-themed-search-page', + selector: 'ds-search-page', styleUrls: [], templateUrl: '../shared/theme-support/themed.component.html', standalone: true, + imports: [SearchPageComponent], }) export class ThemedSearchPageComponent extends ThemedComponent { diff --git a/src/app/shared/auth-nav-menu/auth-nav-menu.component.html b/src/app/shared/auth-nav-menu/auth-nav-menu.component.html index 98749da7b54..70d7a382f6e 100644 --- a/src/app/shared/auth-nav-menu/auth-nav-menu.component.html +++ b/src/app/shared/auth-nav-menu/auth-nav-menu.component.html @@ -13,13 +13,13 @@
  • (next)="goNext()"> - +
    - +
    -
    +
    -
    - -
    @@ -43,7 +38,8 @@ [formModel]="formModel" [displayCancel]="false" (submitForm)="onSubmit()"> - + diff --git a/src/app/shared/comcol/comcol-forms/comcol-form/comcol-form.component.spec.ts b/src/app/shared/comcol/comcol-forms/comcol-form/comcol-form.component.spec.ts index 73d00e81c3a..2035c313fe6 100644 --- a/src/app/shared/comcol/comcol-forms/comcol-form/comcol-form.component.spec.ts +++ b/src/app/shared/comcol/comcol-forms/comcol-form/comcol-form.component.spec.ts @@ -29,10 +29,7 @@ import { hasValue } from '../../../empty.util'; import { FormComponent } from '../../../form/form.component'; import { AuthServiceMock } from '../../../mocks/auth.service.mock'; import { NotificationsService } from '../../../notifications/notifications.service'; -import { - createFailedRemoteDataObject$, - createSuccessfulRemoteDataObject$, -} from '../../../remote-data.utils'; +import { createSuccessfulRemoteDataObject$ } from '../../../remote-data.utils'; import { NotificationsServiceStub } from '../../../testing/notifications-service.stub'; import { UploaderComponent } from '../../../upload/uploader/uploader.component'; import { VarDirective } from '../../../utils/var.directive'; @@ -80,6 +77,7 @@ describe('ComColFormComponent', () => { const dsoService = Object.assign({ getLogoEndpoint: () => observableOf(logoEndpoint), deleteLogo: () => createSuccessfulRemoteDataObject$({}), + findById: () => createSuccessfulRemoteDataObject$({}), }); const notificationsService = new NotificationsServiceStub(); @@ -89,6 +87,7 @@ describe('ComColFormComponent', () => { const requestServiceStub = jasmine.createSpyObj('requestService', { removeByHrefSubstring: {}, + setStaleByHrefSubstring: {}, }); const objectCacheStub = jasmine.createSpyObj('objectCache', { remove: {}, @@ -175,8 +174,6 @@ describe('ComColFormComponent', () => { type: Community.type, }, ), - uploader: undefined, - deleteLogo: false, operations: operations, }, ); @@ -185,32 +182,22 @@ describe('ComColFormComponent', () => { describe('onCompleteItem', () => { beforeEach(() => { - spyOn(comp.finish, 'emit'); comp.onCompleteItem(); }); it('should show a success notification', () => { expect(notificationsService.success).toHaveBeenCalled(); }); - - it('should emit finish', () => { - expect(comp.finish.emit).toHaveBeenCalled(); - }); }); describe('onUploadError', () => { beforeEach(() => { - spyOn(comp.finish, 'emit'); comp.onUploadError(); }); it('should show an error notification', () => { expect(notificationsService.error).toHaveBeenCalled(); }); - - it('should emit finish', () => { - expect(comp.finish.emit).toHaveBeenCalled(); - }); }); }); @@ -231,6 +218,11 @@ describe('ComColFormComponent', () => { it('should initialize the uploadFilesOptions with a POST method', () => { expect(comp.uploadFilesOptions.method).toEqual(RestRequestMethod.POST); }); + + it('should not show the delete logo button', () => { + const button = fixture.debugElement.query(By.css('#logo-section .btn-danger')); + expect(button).toBeFalsy(); + }); }); describe('and the dso contains a logo', () => { @@ -249,96 +241,71 @@ describe('ComColFormComponent', () => { expect(comp.uploadFilesOptions.url).toEqual(logoEndpoint); }); - it('should initialize the uploadFilesOptions with a PUT method', () => { - expect(comp.uploadFilesOptions.method).toEqual(RestRequestMethod.PUT); + it('should show the delete logo button', () => { + const button = fixture.debugElement.query(By.css('#logo-section .btn-danger')); + expect(button).toBeTruthy(); }); - describe('submit with logo marked for deletion', () => { + describe('when the delete logo button is clicked', () => { beforeEach(() => { - spyOn(dsoService, 'deleteLogo').and.callThrough(); - comp.markLogoForDeletion = true; - }); - - it('should call dsoService.deleteLogo on the DSO', () => { - comp.onSubmit(); + spyOn(dsoService, 'deleteLogo').and.returnValue(createSuccessfulRemoteDataObject$({})); + spyOn(comp, 'handleLogoDeletion').and.callThrough(); + spyOn(comp, 'createConfirmationModal').and.callThrough(); + spyOn(comp, 'subscribeToConfirmationResponse').and.callThrough(); + const deleteButton = fixture.debugElement.query(By.css('#logo-section .btn-danger')); + deleteButton.nativeElement.click(); fixture.detectChanges(); - - expect(dsoService.deleteLogo).toHaveBeenCalledWith(comp.dso); }); - describe('when dsoService.deleteLogo returns a successful response', () => { - beforeEach(() => { - dsoService.deleteLogo.and.returnValue(createSuccessfulRemoteDataObject$({})); - comp.onSubmit(); - }); + it('should create a confirmation modal with the correct labels and properties', () => { + const modalServiceSpy = spyOn((comp as any).modalService, 'open').and.callThrough(); - it('should display a success notification', () => { - expect(notificationsService.success).toHaveBeenCalled(); - }); - }); + const modalRef = comp.createConfirmationModal(); - describe('when dsoService.deleteLogo returns an error response', () => { - beforeEach(() => { - dsoService.deleteLogo.and.returnValue(createFailedRemoteDataObject$('Error', 500)); - comp.onSubmit(); - }); + expect(modalServiceSpy).toHaveBeenCalled(); - it('should display an error notification', () => { - expect(notificationsService.error).toHaveBeenCalled(); - }); - }); - }); + expect(modalRef).toBeDefined(); + expect(modalRef.componentInstance).toBeDefined(); - describe('deleteLogo', () => { - beforeEach(() => { - comp.deleteLogo(); - fixture.detectChanges(); + expect(modalRef.componentInstance.headerLabel).toBe('community-collection.edit.logo.delete.title'); + expect(modalRef.componentInstance.infoLabel).toBe('confirmation-modal.delete-community-collection-logo.info'); + expect(modalRef.componentInstance.cancelLabel).toBe('form.cancel'); + expect(modalRef.componentInstance.confirmLabel).toBe('community-collection.edit.logo.delete.title'); + expect(modalRef.componentInstance.confirmIcon).toBe('fas fa-trash'); }); - it('should set markLogoForDeletion to true', () => { - expect(comp.markLogoForDeletion).toEqual(true); + it('should call createConfirmationModal method', () => { + expect(comp.createConfirmationModal).toHaveBeenCalled(); }); - it('should mark the logo section with a danger alert', () => { - const logoSection = fixture.debugElement.query(By.css('#logo-section.alert-danger')); - expect(logoSection).toBeTruthy(); + it('should call subscribeToConfirmationResponse method', () => { + expect(comp.subscribeToConfirmationResponse).toHaveBeenCalled(); }); - it('should hide the delete button', () => { - const button = fixture.debugElement.query(By.css('#logo-section .btn-danger')); - expect(button).not.toBeTruthy(); - }); + describe('when the modal is closed', () => { - it('should show the undo button', () => { - const button = fixture.debugElement.query(By.css('#logo-section .btn-warning')); - expect(button).toBeTruthy(); - }); - }); + let modalRef; - describe('undoDeleteLogo', () => { - beforeEach(() => { - comp.markLogoForDeletion = true; - comp.undoDeleteLogo(); - fixture.detectChanges(); - }); + beforeEach(() => { + modalRef = comp.createConfirmationModal(); + comp.subscribeToConfirmationResponse(modalRef); + }); - it('should set markLogoForDeletion to false', () => { - expect(comp.markLogoForDeletion).toEqual(false); - }); + it('should call handleLogoDeletion and dsoService.deleteLogo methods when deletion is confirmed', waitForAsync(() => { + modalRef.componentInstance.confirmPressed(); - it('should disable the danger alert on the logo section', () => { - const logoSection = fixture.debugElement.query(By.css('#logo-section.alert-danger')); - expect(logoSection).not.toBeTruthy(); - }); + expect(comp.handleLogoDeletion).toHaveBeenCalled(); + expect(dsoService.deleteLogo).toHaveBeenCalled(); - it('should show the delete button', () => { - const button = fixture.debugElement.query(By.css('#logo-section .btn-danger')); - expect(button).toBeTruthy(); - }); + })); + + it('should not call handleLogoDeletion and dsoService.deleteLogo methods when deletion is refused', waitForAsync(() => { + modalRef.componentInstance.cancelPressed(); + + expect(comp.handleLogoDeletion).not.toHaveBeenCalled(); + expect(dsoService.deleteLogo).not.toHaveBeenCalled(); + })); - it('should hide the undo button', () => { - const button = fixture.debugElement.query(By.css('#logo-section .btn-warning')); - expect(button).not.toBeTruthy(); }); }); }); diff --git a/src/app/shared/comcol/comcol-forms/comcol-form/comcol-form.component.ts b/src/app/shared/comcol/comcol-forms/comcol-form/comcol-form.component.ts index dfca0e1ae4b..0c80ce0d1d9 100644 --- a/src/app/shared/comcol/comcol-forms/comcol-form/comcol-form.component.ts +++ b/src/app/shared/comcol/comcol-forms/comcol-form/comcol-form.component.ts @@ -13,6 +13,10 @@ import { ViewChild, } from '@angular/core'; import { UntypedFormGroup } from '@angular/forms'; +import { + NgbModal, + NgbModalRef, +} from '@ng-bootstrap/ng-bootstrap'; import { DynamicFormControlModel, DynamicFormService, @@ -27,15 +31,20 @@ import { FileUploader } from 'ng2-file-upload'; import { BehaviorSubject, combineLatest as observableCombineLatest, + Observable, Subscription, + switchMap, } from 'rxjs'; +import { + filter, + take, +} from 'rxjs/operators'; import { AuthService } from '../../../../core/auth/auth.service'; import { ObjectCacheService } from '../../../../core/cache/object-cache.service'; import { ComColDataService } from '../../../../core/data/comcol-data.service'; import { RemoteData } from '../../../../core/data/remote-data'; import { RequestService } from '../../../../core/data/request.service'; -import { RestRequestMethod } from '../../../../core/data/rest-request-method'; import { Bitstream } from '../../../../core/shared/bitstream.model'; import { Collection } from '../../../../core/shared/collection.model'; import { Community } from '../../../../core/shared/community.model'; @@ -46,6 +55,7 @@ import { import { NoContent } from '../../../../core/shared/NoContent.model'; import { getFirstCompletedRemoteData } from '../../../../core/shared/operators'; import { ResourceType } from '../../../../core/shared/resource-type'; +import { ConfirmationModalComponent } from '../../../confirmation-modal/confirmation-modal.component'; import { hasValue, isNotEmpty, @@ -54,6 +64,7 @@ import { FormComponent } from '../../../form/form.component'; import { NotificationsService } from '../../../notifications/notifications.service'; import { UploaderComponent } from '../../../upload/uploader/uploader.component'; import { UploaderOptions } from '../../../upload/uploader/uploader-options.model'; +import { followLink } from '../../../utils/follow-link-config.model'; import { VarDirective } from '../../../utils/var.directive'; import { ComcolPageLogoComponent } from '../../comcol-page-logo/comcol-page-logo.component'; @@ -88,6 +99,11 @@ export class ComColFormComponent implements On */ @Input() dso: T; + /** + * Boolean that represents if the comcol is being created or already exists + */ + @Input() isCreation!: boolean; + /** * Type of DSpaceObject that the form represents */ @@ -126,9 +142,8 @@ export class ComColFormComponent implements On */ @Output() submitForm: EventEmitter<{ dso: T, - uploader: FileUploader, - deleteLogo: boolean, operations: Operation[], + uploader?: FileUploader }> = new EventEmitter(); /** @@ -137,21 +152,17 @@ export class ComColFormComponent implements On @Output() back: EventEmitter = new EventEmitter(); /** - * Fires an event when the logo has finished uploading (with or without errors) or was removed + * Event emitted on finish */ @Output() finish: EventEmitter = new EventEmitter(); + /** - * Observable keeping track whether or not the uploader has finished initializing + * Observable keeping track whether the uploader has finished initializing * Used to start rendering the uploader component */ initializedUploaderOptions = new BehaviorSubject(false); - /** - * Is the logo marked to be deleted? - */ - markLogoForDeletion = false; - /** * Array to track all subscriptions and unsubscribe them onDestroy * @type {Array} @@ -163,15 +174,22 @@ export class ComColFormComponent implements On */ protected dsoService: ComColDataService; + public uploader = new FileUploader(this.uploadFilesOptions); + + protected readonly refreshDSO$ = new EventEmitter(); + public constructor(protected formService: DynamicFormService, protected translate: TranslateService, protected notificationsService: NotificationsService, protected authService: AuthService, protected requestService: RequestService, - protected objectCache: ObjectCacheService) { + protected objectCache: ObjectCacheService, + protected modalService: NgbModal){ } ngOnInit(): void { + this.uploadFilesOptions.autoUpload = !this.isCreation; + if (hasValue(this.formModel)) { this.formModel.forEach( (fieldModel: DynamicInputModel) => { @@ -194,10 +212,6 @@ export class ComColFormComponent implements On ]).subscribe(([href, logoRD]: [string, RemoteData]) => { this.uploadFilesOptions.url = href; this.uploadFilesOptions.authToken = this.authService.buildAuthHeader(); - // If the object already contains a logo, send out a PUT request instead of POST for setting a new logo - if (hasValue(logoRD.payload)) { - this.uploadFilesOptions.method = RestRequestMethod.PUT; - } this.initializedUploaderOptions.next(true); }), ); @@ -208,33 +222,20 @@ export class ComColFormComponent implements On this.initializedUploaderOptions.next(true); } } + + this.subs.push( + this.refreshDSO$.pipe( + switchMap(() => this.refreshDsoCache()), + filter(rd => rd.hasSucceeded), + ).subscribe(({ payload }) => this.dso = payload), + ); + } /** * Checks which new fields were added and sends the updated version of the DSO to the parent component */ onSubmit() { - if (this.markLogoForDeletion && hasValue(this.dso.id) && hasValue(this.dso._links.logo)) { - this.dsoService.deleteLogo(this.dso).pipe( - getFirstCompletedRemoteData(), - ).subscribe((response: RemoteData) => { - if (response.hasSucceeded) { - this.notificationsService.success( - this.translate.get(this.type.value + '.edit.logo.notifications.delete.success.title'), - this.translate.get(this.type.value + '.edit.logo.notifications.delete.success.content'), - ); - } else { - this.notificationsService.error( - this.translate.get(this.type.value + '.edit.logo.notifications.delete.error.title'), - response.errorMessage, - ); - } - this.dso.logo = undefined; - this.uploadFilesOptions.method = RestRequestMethod.POST; - this.finish.emit(); - }); - } - const formMetadata = {} as MetadataMap; this.formModel.forEach((fieldModel: DynamicInputModel) => { const value: MetadataValue = { @@ -270,12 +271,18 @@ export class ComColFormComponent implements On } }); - this.submitForm.emit({ - dso: updatedDSO, - uploader: hasValue(this.uploaderComponent) ? this.uploaderComponent.uploader : undefined, - deleteLogo: this.markLogoForDeletion, - operations: operations, - }); + if (this.isCreation) { + this.submitForm.emit({ + dso: updatedDSO, + uploader: hasValue(this.uploaderComponent) ? this.uploaderComponent.uploader : undefined, + operations: operations, + }); + } else { + this.submitForm.emit({ + dso: updatedDSO, + operations: operations, + }); + } } /** @@ -296,37 +303,126 @@ export class ComColFormComponent implements On } /** - * Mark the logo to be deleted - * Send out a delete request to remove the logo from the community/collection and display notifications + * Helper method that confirms the deletion of the logo opening a confirmation modal + */ + confirmLogoDeleteWithModal(): void { + const modalRef = this.createConfirmationModal(); + this.subscribeToConfirmationResponse(modalRef); + } + + /** + * Creates and opens the confirmation modal + * @returns Reference to the opened modal + */ + createConfirmationModal(): NgbModalRef { + const modalRef = this.modalService.open(ConfirmationModalComponent); + modalRef.componentInstance.headerLabel = 'community-collection.edit.logo.delete.title'; + modalRef.componentInstance.infoLabel = 'confirmation-modal.delete-community-collection-logo.info'; + modalRef.componentInstance.cancelLabel = 'form.cancel'; + modalRef.componentInstance.confirmLabel = 'community-collection.edit.logo.delete.title'; + modalRef.componentInstance.confirmIcon = 'fas fa-trash'; + modalRef.componentInstance.brandColor = 'danger'; + return modalRef; + } + + /** + * Subscribes to the confirmation modal's response and calls the logo deletion handler if confirmed + * @param modalRef References to the opened confirmation modal + */ + subscribeToConfirmationResponse(modalRef: NgbModalRef): void { + modalRef.componentInstance.response.pipe( + take(1), + ).subscribe((confirmed: boolean) => { + if (confirmed) { + this.handleLogoDeletion(); + } + }); + } + + /** + * Method that confirms the deletion of the logo, handling both possible outcomes + */ + handleLogoDeletion(): void { + if (hasValue(this.dso.id) && hasValue(this.dso._links.logo)) { + this.dsoService.deleteLogo(this.dso).pipe( + getFirstCompletedRemoteData(), + ).subscribe((response: RemoteData) => { + const successMessageKey = `${this.type.value}.edit.logo.notifications.delete.success`; + const errorMessageKey = `${this.type.value}.edit.logo.notifications.delete.error`; + + if (response.hasSucceeded) { + this.handleSuccessfulDeletion(successMessageKey); + } else { + this.handleFailedDeletion(errorMessageKey, response.errorMessage); + } + + }); + } + } + + + /** + * Handles successful logo deletion + * @param successMessageKey Translation key for success message + */ + private handleSuccessfulDeletion(successMessageKey: string): void { + this.refreshDSO$.next(); + this.notificationsService.success( + this.translate.get(`${successMessageKey}.title`), + this.translate.get(`${successMessageKey}.content`), + ); + } + + /** + * Handles failed logo deletion + * @param errorMessageKey Translation key for error message + * @param errorMessage Error message from the response */ - deleteLogo() { - this.markLogoForDeletion = true; + private handleFailedDeletion(errorMessageKey: string, errorMessage: string): void { + this.notificationsService.error( + this.translate.get(`${errorMessageKey}.title`), + errorMessage, + ); } /** - * Undo marking the logo to be deleted + * Refresh the object's cache to obtain the latest version */ - undoDeleteLogo() { - this.markLogoForDeletion = false; + private refreshDsoCache() { + this.clearDsoCache(); + return this.fetchUpdatedDso(); } /** - * Refresh the object's cache to ensure the latest version + * Clears the cache related to the current dso */ - private refreshCache() { - this.requestService.removeByHrefSubstring(this.dso._links.self.href); + private clearDsoCache() { + this.requestService.setStaleByHrefSubstring(this.dso.id); this.objectCache.remove(this.dso._links.self.href); } + /** + * Fetches the latest data for the dso + */ + private fetchUpdatedDso(): Observable> { + return this.dsoService.findById(this.dso.id, false, true, followLink('logo')).pipe( + getFirstCompletedRemoteData(), + ) as Observable>; + } + + + /** * The request was successful, display a success notification */ public onCompleteItem() { if (hasValue(this.dso.id)) { - this.refreshCache(); + this.refreshDSO$.next(); + } + if (this.isCreation) { + this.finish.emit(); } this.notificationsService.success(null, this.translate.get(this.type.value + '.edit.logo.notifications.add.success')); - this.finish.emit(); } /** @@ -334,13 +430,13 @@ export class ComColFormComponent implements On */ public onUploadError() { this.notificationsService.error(null, this.translate.get(this.type.value + '.edit.logo.notifications.add.error')); - this.finish.emit(); } /** * Unsubscribe from open subscriptions */ ngOnDestroy(): void { + this.refreshDSO$.complete(); this.subs .filter((subscription) => hasValue(subscription)) .forEach((subscription) => subscription.unsubscribe()); diff --git a/src/app/shared/comcol/comcol-forms/create-comcol-page/create-comcol-page.component.ts b/src/app/shared/comcol/comcol-forms/create-comcol-page/create-comcol-page.component.ts index 398ffeb6ba9..cb2b5a9f0c5 100644 --- a/src/app/shared/comcol/comcol-forms/create-comcol-page/create-comcol-page.component.ts +++ b/src/app/shared/comcol/comcol-forms/create-comcol-page/create-comcol-page.component.ts @@ -4,12 +4,19 @@ import { } from '@angular/core'; import { Router } from '@angular/router'; import { TranslateService } from '@ngx-translate/core'; -import { Observable } from 'rxjs'; import { + BehaviorSubject, + Observable, + of, +} from 'rxjs'; +import { + map, mergeMap, take, + tap, } from 'rxjs/operators'; +import { getHomePageRoute } from '../../../../app-routing-paths'; import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service'; import { RequestParam } from '../../../../core/cache/models/request-param.model'; import { ComColDataService } from '../../../../core/data/comcol-data.service'; @@ -62,6 +69,11 @@ export class CreateComColPageComponent i */ protected type: ResourceType; + /** + * The + */ + isLoading$: BehaviorSubject = new BehaviorSubject(false); + public constructor( protected dsoDataService: ComColDataService, public dsoNameService: DSONameService, @@ -89,6 +101,7 @@ export class CreateComColPageComponent i * @param event The event returned by the community/collection form. Contains the new dso and logo uploader */ onSubmit(event) { + this.isLoading$.next(true); const dso = event.dso; const uploader = event.uploader; @@ -99,29 +112,44 @@ export class CreateComColPageComponent i return this.dsoDataService.create(dso, ...params) .pipe(getFirstSucceededRemoteDataPayload(), ); - })) - .subscribe((dsoRD: TDomain) => { + }), + mergeMap((dsoRD: TDomain) => { if (isNotUndefined(dsoRD)) { this.newUUID = dsoRD.uuid; if (uploader.queue.length > 0) { - this.dsoDataService.getLogoEndpoint(this.newUUID).pipe(take(1)).subscribe((href: string) => { - uploader.options.url = href; - uploader.uploadAll(); - }); + return this.dsoDataService.getLogoEndpoint(this.newUUID).pipe( + take(1), + tap((href: string) => { + uploader.options.url = href; + uploader.onCompleteAll = () => { + this.isLoading$.next(false); + this.navigateToNewPage(); + this.notificationsService.success(null, this.translate.get(this.type.value + '.create.notifications.success')); + }; + uploader.uploadAll(); + }), + map(() => false), + ); } else { - this.navigateToNewPage(); + this.dsoDataService.refreshCache(dsoRD); + return of(true); } - this.dsoDataService.refreshCache(dsoRD); } + }), + ).subscribe((notify: boolean) => { + if (notify) { + this.isLoading$.next(false); + this.navigateToNewPage(); this.notificationsService.success(null, this.translate.get(this.type.value + '.create.notifications.success')); - }); + } + }); } /** * Navigate to home page */ navigateToHome() { - this.router.navigate(['/home']); + this.router.navigate([getHomePageRoute()]); } /** diff --git a/src/app/shared/comcol/comcol-forms/edit-comcol-page/comcol-metadata/comcol-metadata.component.spec.ts b/src/app/shared/comcol/comcol-forms/edit-comcol-page/comcol-metadata/comcol-metadata.component.spec.ts index e3d12d81ea6..e2b71b39414 100644 --- a/src/app/shared/comcol/comcol-forms/edit-comcol-page/comcol-metadata/comcol-metadata.component.spec.ts +++ b/src/app/shared/comcol/comcol-forms/edit-comcol-page/comcol-metadata/comcol-metadata.component.spec.ts @@ -36,8 +36,6 @@ describe('ComColMetadataComponent', () => { let routerStub; let routeStub; - const logoEndpoint = 'rest/api/logo/endpoint'; - function initializeVars() { community = Object.assign(new Community(), { uuid: 'a20da287-e174-466a-9926-f66b9300d347', @@ -58,7 +56,6 @@ describe('ComColMetadataComponent', () => { communityDataServiceStub = { update: (com, uuid?) => createSuccessfulRemoteDataObject$(newCommunity), patch: () => null, - getLogoEndpoint: () => observableOf(logoEndpoint), }; routerStub = { @@ -125,7 +122,6 @@ describe('ComColMetadataComponent', () => { }, /* eslint-enable no-empty,@typescript-eslint/no-empty-function */ }, - deleteLogo: false, }; spyOn(router, 'navigate'); }); @@ -157,48 +153,84 @@ describe('ComColMetadataComponent', () => { }); }); - describe('with at least one item in the uploader\'s queue', () => { + describe('with an empty operations array', () => { beforeEach(() => { data = { - dso: Object.assign(new Community(), { - metadata: [{ - key: 'dc.title', - value: 'test', - }], - }), + operations: [], + dso: new Community(), uploader: { options: { url: '', }, - queue: [ - {}, - ], + queue: [], /* eslint-disable no-empty,@typescript-eslint/no-empty-function */ uploadAll: () => { }, - /* eslint-enable no-empty, @typescript-eslint/no-empty-function */ + /* eslint-enable no-empty,@typescript-eslint/no-empty-function */ }, }; + spyOn(router, 'navigate'); }); - it('should not navigate', () => { - spyOn(router, 'navigate'); + it('should navigate', () => { comp.onSubmit(data); fixture.detectChanges(); - expect(router.navigate).not.toHaveBeenCalled(); + expect(router.navigate).toHaveBeenCalled(); }); + }); - it('should set the uploader\'s url to the logo\'s endpoint', () => { - comp.onSubmit(data); - fixture.detectChanges(); - expect(data.uploader.options.url).toEqual(logoEndpoint); + describe('with a not empty operations array', () => { + beforeEach(() => { + data = { + operations: [ + { + op: 'replace', + path: '/metadata/dc.title', + value: { + value: 'test', + language: null, + }, + }, + ], + dso: new Community(), + uploader: { + options: { + url: '', + }, + queue: [], + /* eslint-disable no-empty,@typescript-eslint/no-empty-function */ + uploadAll: () => { + }, + /* eslint-enable no-empty,@typescript-eslint/no-empty-function */ + }, + }; + spyOn(router, 'navigate'); }); - it('should call the uploader\'s uploadAll', () => { - spyOn(data.uploader, 'uploadAll'); - comp.onSubmit(data); - fixture.detectChanges(); - expect(data.uploader.uploadAll).toHaveBeenCalled(); + describe('when successful', () => { + + beforeEach(() => { + spyOn(dsoDataService, 'patch').and.returnValue(createSuccessfulRemoteDataObject$({})); + }); + + it('should navigate', () => { + comp.onSubmit(data); + fixture.detectChanges(); + expect(router.navigate).toHaveBeenCalled(); + }); + }); + + describe('on failure', () => { + + beforeEach(() => { + spyOn(dsoDataService, 'patch').and.returnValue(createFailedRemoteDataObject$('Error', 500)); + }); + + it('should not navigate', () => { + comp.onSubmit(data); + fixture.detectChanges(); + expect(router.navigate).not.toHaveBeenCalled(); + }); }); }); }); diff --git a/src/app/shared/comcol/comcol-forms/edit-comcol-page/comcol-metadata/comcol-metadata.component.ts b/src/app/shared/comcol/comcol-forms/edit-comcol-page/comcol-metadata/comcol-metadata.component.ts index c74153f0834..b895f6c80d9 100644 --- a/src/app/shared/comcol/comcol-forms/edit-comcol-page/comcol-metadata/comcol-metadata.component.ts +++ b/src/app/shared/comcol/comcol-forms/edit-comcol-page/comcol-metadata/comcol-metadata.component.ts @@ -23,10 +23,7 @@ import { getFirstSucceededRemoteData, } from '../../../../../core/shared/operators'; import { ResourceType } from '../../../../../core/shared/resource-type'; -import { - hasValue, - isEmpty, -} from '../../../../empty.util'; +import { isEmpty } from '../../../../empty.util'; import { NotificationsService } from '../../../../notifications/notifications.service'; @Component({ @@ -35,15 +32,14 @@ import { NotificationsService } from '../../../../notifications/notifications.se standalone: true, }) export class ComcolMetadataComponent implements OnInit { - /** - * Frontend endpoint for this type of DSO - */ - protected frontendURL: string; /** * The initial DSO object */ public dsoRD$: Observable>; - + /** + * Frontend endpoint for this type of DSO + */ + protected frontendURL: string; /** * The type of the dso */ @@ -67,36 +63,25 @@ export class ComcolMetadataComponent imp * @param event The event returned by the community/collection form. Contains the new dso and logo uploader */ onSubmit(event) { - - const uploader = event.uploader; - const deleteLogo = event.deleteLogo; - - const newLogo = hasValue(uploader) && uploader.queue.length > 0; - if (newLogo) { - this.dsoDataService.getLogoEndpoint(event.dso.uuid).pipe(take(1)).subscribe((href: string) => { - uploader.options.url = href; - uploader.uploadAll(); - }); - } - if (!isEmpty(event.operations)) { - this.dsoDataService.patch(event.dso, event.operations).pipe( - getFirstCompletedRemoteData(), - ).subscribe((response: RemoteData) => { - if (response.hasSucceeded) { - this.router.navigate([this.frontendURL + event.dso.uuid]); // todo: ok not to await this? - this.notificationsService.success(null, this.translate.get(`${this.type.value}.edit.notifications.success`)); - } else if (response.statusCode === 403) { - this.notificationsService.error(null, this.translate.get(`${this.type.value}.edit.notifications.unauthorized`)); - } else { - this.notificationsService.error(null, this.translate.get(`${this.type.value}.edit.notifications.error`)); - } - }); + this.dsoDataService.patch(event.dso, event.operations).pipe(getFirstCompletedRemoteData()) + .subscribe( (response: RemoteData) => { + if (response.hasSucceeded) { + this.router.navigate([this.frontendURL, event.dso.uuid]); // todo: ok not to await this? + this.notificationsService.success(null, this.translate.get(`${this.type.value}.edit.notifications.success`)); + } else if (response.statusCode === 403) { + this.notificationsService.error(null, this.translate.get(`${this.type.value}.edit.notifications.unauthorized`)); + } else { + this.notificationsService.error(null, this.translate.get(`${this.type.value}.edit.notifications.error`)); + } + }); + } else { + this.router.navigate([this.frontendURL, event.dso.uuid]); } } /** - * Navigate to the home page of the object + * Navigate to the relative DSO page */ navigateToHomePage() { this.dsoRD$.pipe( diff --git a/src/app/shared/comcol/comcol-forms/edit-comcol-page/comcol-role/comcol-role.component.html b/src/app/shared/comcol/comcol-forms/edit-comcol-page/comcol-role/comcol-role.component.html index fa9dd5da099..2639a4c099f 100644 --- a/src/app/shared/comcol/comcol-forms/edit-comcol-page/comcol-role/comcol-role.component.html +++ b/src/app/shared/comcol/comcol-forms/edit-comcol-page/comcol-role/comcol-role.component.html @@ -13,7 +13,7 @@

    - +
    {{'comcol-role.edit.no-group' | translate}}
    diff --git a/src/app/shared/comcol/comcol-forms/edit-comcol-page/comcol-role/comcol-role.component.ts b/src/app/shared/comcol/comcol-forms/edit-comcol-page/comcol-role/comcol-role.component.ts index 8fa11331787..22c4ed49f3a 100644 --- a/src/app/shared/comcol/comcol-forms/edit-comcol-page/comcol-role/comcol-role.component.ts +++ b/src/app/shared/comcol/comcol-forms/edit-comcol-page/comcol-role/comcol-role.component.ts @@ -71,7 +71,7 @@ export class ComcolRoleComponent implements OnInit { * The community or collection to manage. */ @Input() - dso: Community | Collection; + dso: Community | Collection; /** * The role to manage diff --git a/src/app/shared/comcol/comcol-forms/edit-comcol-page/edit-comcol-page.component.html b/src/app/shared/comcol/comcol-forms/edit-comcol-page/edit-comcol-page.component.html index a8ca674c263..7797ba13c91 100644 --- a/src/app/shared/comcol/comcol-forms/edit-comcol-page/edit-comcol-page.component.html +++ b/src/app/shared/comcol/comcol-forms/edit-comcol-page/edit-comcol-page.component.html @@ -11,9 +11,10 @@

    {{ type + '.edit.head' | translate }}

    -
    {{'dso-selector.create.community.sub-level' | translate}}

    diff --git a/src/app/shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component.spec.ts b/src/app/shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component.spec.ts index cfef8a6d685..04922d4deb4 100644 --- a/src/app/shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component.spec.ts +++ b/src/app/shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component.spec.ts @@ -7,13 +7,16 @@ import { TestBed, waitForAsync, } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; import { ActivatedRoute, Router, } from '@angular/router'; import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; import { TranslateModule } from '@ngx-translate/core'; +import { of as observableOf } from 'rxjs'; +import { AuthorizationDataService } from '../../../../core/data/feature-authorization/authorization-data.service'; import { Community } from '../../../../core/shared/community.model'; import { MetadataValue } from '../../../../core/shared/metadata.models'; import { createSuccessfulRemoteDataObject } from '../../../remote-data.utils'; @@ -38,7 +41,9 @@ describe('CreateCommunityParentSelectorComponent', () => { const communityRD = createSuccessfulRemoteDataObject(community); const modalStub = jasmine.createSpyObj('modalStub', ['close']); const createPath = '/communities/create'; - + const mockAuthorizationDataService = jasmine.createSpyObj('authorizationService', { + isAuthorized: observableOf(true), + }); beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [TranslateModule.forRoot(), CreateCommunityParentSelectorComponent], @@ -59,6 +64,7 @@ describe('CreateCommunityParentSelectorComponent', () => { { provide: Router, useValue: router, }, + { provide: AuthorizationDataService, useValue: mockAuthorizationDataService }, ], schemas: [NO_ERRORS_SCHEMA], }) @@ -85,4 +91,20 @@ describe('CreateCommunityParentSelectorComponent', () => { expect(router.navigate).toHaveBeenCalledWith([createPath], { queryParams: { parent: community.uuid } }); }); + it('should show the div when user is an admin', (waitForAsync(() => { + component.isAdmin$ = observableOf(true); + fixture.detectChanges(); + + const divElement = fixture.debugElement.query(By.css('div[data-test="admin-div"]')); + expect(divElement).toBeTruthy(); + }))); + + it('should hide the div when user is not an admin', (waitForAsync(() => { + component.isAdmin$ = observableOf(false); + fixture.detectChanges(); + + const divElement = fixture.debugElement.query(By.css('div[data-test="admin-div"]')); + expect(divElement).toBeFalsy(); + }))); + }); diff --git a/src/app/shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component.ts b/src/app/shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component.ts index 27b09b749cb..d4e9b312451 100644 --- a/src/app/shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component.ts +++ b/src/app/shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component.ts @@ -1,3 +1,7 @@ +import { + AsyncPipe, + NgIf, +} from '@angular/common'; import { Component, OnInit, @@ -9,6 +13,7 @@ import { } from '@angular/router'; import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; import { TranslateModule } from '@ngx-translate/core'; +import { Observable } from 'rxjs'; import { environment } from '../../../../../environments/environment'; import { @@ -19,6 +24,8 @@ import { SortDirection, SortOptions, } from '../../../../core/cache/models/sort-options.model'; +import { AuthorizationDataService } from '../../../../core/data/feature-authorization/authorization-data.service'; +import { FeatureID } from '../../../../core/data/feature-authorization/feature-id'; import { DSpaceObject } from '../../../../core/shared/dspace-object.model'; import { DSpaceObjectType } from '../../../../core/shared/dspace-object-type.model'; import { hasValue } from '../../../empty.util'; @@ -36,22 +43,27 @@ import { */ @Component({ - selector: 'ds-create-community-parent-selector', + selector: 'ds-base-create-community-parent-selector', styleUrls: ['./create-community-parent-selector.component.scss'], templateUrl: './create-community-parent-selector.component.html', standalone: true, - imports: [DSOSelectorComponent, TranslateModule], + imports: [DSOSelectorComponent, TranslateModule, NgIf, AsyncPipe], }) export class CreateCommunityParentSelectorComponent extends DSOSelectorModalWrapperComponent implements OnInit { objectType = DSpaceObjectType.COMMUNITY; selectorTypes = [DSpaceObjectType.COMMUNITY]; action = SelectorActionType.CREATE; defaultSort = new SortOptions(environment.comcolSelectionSort.sortField, environment.comcolSelectionSort.sortDirection as SortDirection); + isAdmin$: Observable; - constructor(protected activeModal: NgbActiveModal, protected route: ActivatedRoute, private router: Router) { + constructor(protected activeModal: NgbActiveModal, protected route: ActivatedRoute, private router: Router, protected authorizationService: AuthorizationDataService) { super(activeModal, route); } + ngOnInit() { + this.isAdmin$ = this.authorizationService.isAuthorized(FeatureID.AdministratorOf); + } + /** * Navigate to the community create page */ diff --git a/src/app/shared/dso-selector/modal-wrappers/create-community-parent-selector/themed-create-community-parent-selector.component.ts b/src/app/shared/dso-selector/modal-wrappers/create-community-parent-selector/themed-create-community-parent-selector.component.ts index 9568cc261da..e9f66099666 100644 --- a/src/app/shared/dso-selector/modal-wrappers/create-community-parent-selector/themed-create-community-parent-selector.component.ts +++ b/src/app/shared/dso-selector/modal-wrappers/create-community-parent-selector/themed-create-community-parent-selector.component.ts @@ -7,10 +7,11 @@ import { CreateCommunityParentSelectorComponent } from './create-community-paren * Themed wrapper for CreateCommunityParentSelectorComponent */ @Component({ - selector: 'ds-themed-create-community-parent-selector', + selector: 'ds-create-community-parent-selector', styleUrls: [], templateUrl: '../../../theme-support/themed.component.html', standalone: true, + imports: [CreateCommunityParentSelectorComponent], }) export class ThemedCreateCommunityParentSelectorComponent extends ThemedComponent { diff --git a/src/app/shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component.ts b/src/app/shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component.ts index 4c649ba85b1..ede3cc36095 100644 --- a/src/app/shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component.ts +++ b/src/app/shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component.ts @@ -31,7 +31,7 @@ import { */ @Component({ - selector: 'ds-create-item-parent-selector', + selector: 'ds-base-create-item-parent-selector', // styleUrls: ['./create-item-parent-selector.component.scss'], // templateUrl: '../dso-selector-modal-wrapper.component.html', templateUrl: './create-item-parent-selector.component.html', diff --git a/src/app/shared/dso-selector/modal-wrappers/create-item-parent-selector/themed-create-item-parent-selector.component.ts b/src/app/shared/dso-selector/modal-wrappers/create-item-parent-selector/themed-create-item-parent-selector.component.ts index 68bf918c45e..f6cd552629f 100644 --- a/src/app/shared/dso-selector/modal-wrappers/create-item-parent-selector/themed-create-item-parent-selector.component.ts +++ b/src/app/shared/dso-selector/modal-wrappers/create-item-parent-selector/themed-create-item-parent-selector.component.ts @@ -10,10 +10,11 @@ import { CreateItemParentSelectorComponent } from './create-item-parent-selector * Themed wrapper for CreateItemParentSelectorComponent */ @Component({ - selector: 'ds-themed-create-item-parent-selector', + selector: 'ds-create-item-parent-selector', styleUrls: [], templateUrl: '../../../theme-support/themed.component.html', standalone: true, + imports: [CreateItemParentSelectorComponent], }) export class ThemedCreateItemParentSelectorComponent extends ThemedComponent { diff --git a/src/app/shared/dso-selector/modal-wrappers/edit-collection-selector/edit-collection-selector.component.ts b/src/app/shared/dso-selector/modal-wrappers/edit-collection-selector/edit-collection-selector.component.ts index 65424e643da..611a4f13dec 100644 --- a/src/app/shared/dso-selector/modal-wrappers/edit-collection-selector/edit-collection-selector.component.ts +++ b/src/app/shared/dso-selector/modal-wrappers/edit-collection-selector/edit-collection-selector.component.ts @@ -30,7 +30,7 @@ import { */ @Component({ - selector: 'ds-edit-collection-selector', + selector: 'ds-base-edit-collection-selector', templateUrl: '../dso-selector-modal-wrapper.component.html', standalone: true, imports: [NgIf, DSOSelectorComponent, TranslateModule], diff --git a/src/app/shared/dso-selector/modal-wrappers/edit-collection-selector/themed-edit-collection-selector.component.ts b/src/app/shared/dso-selector/modal-wrappers/edit-collection-selector/themed-edit-collection-selector.component.ts index 834b4a5f0b3..28604c18b66 100644 --- a/src/app/shared/dso-selector/modal-wrappers/edit-collection-selector/themed-edit-collection-selector.component.ts +++ b/src/app/shared/dso-selector/modal-wrappers/edit-collection-selector/themed-edit-collection-selector.component.ts @@ -7,10 +7,11 @@ import { EditCollectionSelectorComponent } from './edit-collection-selector.comp * Themed wrapper for EditCollectionSelectorComponent */ @Component({ - selector: 'ds-themed-edit-collection-selector', + selector: 'ds-edit-collection-selector', styleUrls: [], templateUrl: '../../../theme-support/themed.component.html', standalone: true, + imports: [EditCollectionSelectorComponent], }) export class ThemedEditCollectionSelectorComponent extends ThemedComponent { diff --git a/src/app/shared/dso-selector/modal-wrappers/edit-community-selector/edit-community-selector.component.ts b/src/app/shared/dso-selector/modal-wrappers/edit-community-selector/edit-community-selector.component.ts index 6f781cb9eb8..3f7ede0de0d 100644 --- a/src/app/shared/dso-selector/modal-wrappers/edit-community-selector/edit-community-selector.component.ts +++ b/src/app/shared/dso-selector/modal-wrappers/edit-community-selector/edit-community-selector.component.ts @@ -30,7 +30,7 @@ import { */ @Component({ - selector: 'ds-edit-community-selector', + selector: 'ds-base-edit-community-selector', templateUrl: '../dso-selector-modal-wrapper.component.html', standalone: true, imports: [NgIf, DSOSelectorComponent, TranslateModule], diff --git a/src/app/shared/dso-selector/modal-wrappers/edit-community-selector/themed-edit-community-selector.component.ts b/src/app/shared/dso-selector/modal-wrappers/edit-community-selector/themed-edit-community-selector.component.ts index 27798879ff6..0e33746edc9 100644 --- a/src/app/shared/dso-selector/modal-wrappers/edit-community-selector/themed-edit-community-selector.component.ts +++ b/src/app/shared/dso-selector/modal-wrappers/edit-community-selector/themed-edit-community-selector.component.ts @@ -7,10 +7,11 @@ import { EditCommunitySelectorComponent } from './edit-community-selector.compon * Themed wrapper for EditCommunitySelectorComponent */ @Component({ - selector: 'ds-themed-edit-community-selector', + selector: 'ds-edit-community-selector', styleUrls: [], templateUrl: '../../../theme-support/themed.component.html', standalone: true, + imports: [EditCommunitySelectorComponent], }) export class ThemedEditCommunitySelectorComponent extends ThemedComponent { diff --git a/src/app/shared/dso-selector/modal-wrappers/edit-item-selector/edit-item-selector.component.ts b/src/app/shared/dso-selector/modal-wrappers/edit-item-selector/edit-item-selector.component.ts index ebcc936a3ec..903f4327344 100644 --- a/src/app/shared/dso-selector/modal-wrappers/edit-item-selector/edit-item-selector.component.ts +++ b/src/app/shared/dso-selector/modal-wrappers/edit-item-selector/edit-item-selector.component.ts @@ -26,7 +26,7 @@ import { */ @Component({ - selector: 'ds-edit-item-selector', + selector: 'ds-base-edit-item-selector', templateUrl: 'edit-item-selector.component.html', standalone: true, imports: [NgIf, DSOSelectorComponent, TranslateModule], diff --git a/src/app/shared/dso-selector/modal-wrappers/edit-item-selector/themed-edit-item-selector.component.ts b/src/app/shared/dso-selector/modal-wrappers/edit-item-selector/themed-edit-item-selector.component.ts index e7b0a456d21..fbb6850a021 100644 --- a/src/app/shared/dso-selector/modal-wrappers/edit-item-selector/themed-edit-item-selector.component.ts +++ b/src/app/shared/dso-selector/modal-wrappers/edit-item-selector/themed-edit-item-selector.component.ts @@ -7,10 +7,11 @@ import { EditItemSelectorComponent } from './edit-item-selector.component'; * Themed wrapper for EditItemSelectorComponent */ @Component({ - selector: 'ds-themed-edit-item-selector', + selector: 'ds-edit-item-selector', styleUrls: [], templateUrl: '../../../theme-support/themed.component.html', standalone: true, + imports: [EditItemSelectorComponent], }) export class ThemedEditItemSelectorComponent extends ThemedComponent { diff --git a/src/app/shared/dso-selector/modal-wrappers/export-batch-selector/export-batch-selector.component.ts b/src/app/shared/dso-selector/modal-wrappers/export-batch-selector/export-batch-selector.component.ts index 86ae24758c3..0fb80e769bd 100644 --- a/src/app/shared/dso-selector/modal-wrappers/export-batch-selector/export-batch-selector.component.ts +++ b/src/app/shared/dso-selector/modal-wrappers/export-batch-selector/export-batch-selector.component.ts @@ -54,7 +54,7 @@ import { * Used to choose a dso from to export metadata of */ @Component({ - selector: 'ds-export-metadata-selector', + selector: 'ds-export-batch-selector', templateUrl: '../dso-selector-modal-wrapper.component.html', standalone: true, imports: [NgIf, DSOSelectorComponent, TranslateModule], diff --git a/src/app/shared/dso-selector/modal-wrappers/import-batch-selector/import-batch-selector.component.ts b/src/app/shared/dso-selector/modal-wrappers/import-batch-selector/import-batch-selector.component.ts index 599b9977ef2..2b3785c0f9c 100644 --- a/src/app/shared/dso-selector/modal-wrappers/import-batch-selector/import-batch-selector.component.ts +++ b/src/app/shared/dso-selector/modal-wrappers/import-batch-selector/import-batch-selector.component.ts @@ -40,7 +40,7 @@ export class ImportBatchSelectorComponent extends DSOSelectorModalWrapperCompone * An event fired when the modal is closed */ @Output() - response = new EventEmitter(); + response = new EventEmitter(); constructor(protected activeModal: NgbActiveModal, protected route: ActivatedRoute) { diff --git a/src/app/shared/entity-dropdown/entity-dropdown.component.html b/src/app/shared/entity-dropdown/entity-dropdown.component.html index 374c589e3e5..049f5c81bed 100644 --- a/src/app/shared/entity-dropdown/entity-dropdown.component.html +++ b/src/app/shared/entity-dropdown/entity-dropdown.component.html @@ -23,8 +23,8 @@
  • diff --git a/src/app/shared/entity-dropdown/entity-dropdown.component.ts b/src/app/shared/entity-dropdown/entity-dropdown.component.ts index a02c6d82212..61bfd853ca8 100644 --- a/src/app/shared/entity-dropdown/entity-dropdown.component.ts +++ b/src/app/shared/entity-dropdown/entity-dropdown.component.ts @@ -146,7 +146,7 @@ export class EntityDropdownComponent implements OnInit, OnDestroy { } /** - * Method used from infitity scroll for retrive more data on scroll down + * Method used from infitity scroll for retrieve more data on scroll down */ public onScrollDown() { if ( this.hasNextPage ) { diff --git a/src/app/shared/eperson-group-list/eperson-group-list.component.ts b/src/app/shared/eperson-group-list/eperson-group-list.component.ts index 663418d20cd..a600e479c48 100644 --- a/src/app/shared/eperson-group-list/eperson-group-list.component.ts +++ b/src/app/shared/eperson-group-list/eperson-group-list.component.ts @@ -143,7 +143,6 @@ export class EpersonGroupListComponent implements OnInit, OnDestroy { const lazyProvider$: Observable = lazyDataService(this.dataServiceMap, resourceType.value, this.parentInjector); lazyProvider$.subscribe((dataService: EPersonDataService | GroupDataService) => { this.dataService = dataService; - console.log(dataService); this.paginationOptions.id = uniqueId('egl'); this.paginationOptions.pageSize = 5; diff --git a/src/app/shared/error/error.component.ts b/src/app/shared/error/error.component.ts index 900ea678870..168415c0b10 100644 --- a/src/app/shared/error/error.component.ts +++ b/src/app/shared/error/error.component.ts @@ -1,6 +1,8 @@ import { Component, Input, + OnDestroy, + OnInit, } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; import { Subscription } from 'rxjs'; @@ -15,7 +17,7 @@ import { AlertType } from '../alert/alert-type'; standalone: true, imports: [AlertComponent], }) -export class ErrorComponent { +export class ErrorComponent implements OnDestroy, OnInit { @Input() message = 'Error...'; @@ -31,7 +33,7 @@ export class ErrorComponent { } - ngOnInit() { + ngOnInit(): void { if (this.message === undefined) { this.subscription = this.translate.get('error.default').subscribe((message: string) => { this.message = message; @@ -39,7 +41,7 @@ export class ErrorComponent { } } - ngOnDestroy() { + ngOnDestroy(): void { if (this.subscription !== undefined) { this.subscription.unsubscribe(); } diff --git a/src/app/shared/file-download-link/file-download-link.component.html b/src/app/shared/file-download-link/file-download-link.component.html index d649c0ad4e2..aea5c4562cb 100644 --- a/src/app/shared/file-download-link/file-download-link.component.html +++ b/src/app/shared/file-download-link/file-download-link.component.html @@ -1,4 +1,8 @@ -
    + diff --git a/src/app/shared/file-download-link/file-download-link.component.ts b/src/app/shared/file-download-link/file-download-link.component.ts index 991f5ba8645..3080a94bd3e 100644 --- a/src/app/shared/file-download-link/file-download-link.component.ts +++ b/src/app/shared/file-download-link/file-download-link.component.ts @@ -22,6 +22,7 @@ import { getBitstreamDownloadRoute, getBitstreamRequestACopyRoute, } from '../../app-routing-paths'; +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 { Bitstream } from '../../core/shared/bitstream.model'; @@ -32,7 +33,7 @@ import { } from '../empty.util'; @Component({ - selector: 'ds-file-download-link', + selector: 'ds-base-file-download-link', templateUrl: './file-download-link.component.html', styleUrls: ['./file-download-link.component.scss'], standalone: true, @@ -73,6 +74,7 @@ export class FileDownloadLinkComponent implements OnInit { constructor( private authorizationService: AuthorizationDataService, + public dsoNameService: DSONameService, ) { } diff --git a/src/app/shared/file-download-link/themed-file-download-link.component.ts b/src/app/shared/file-download-link/themed-file-download-link.component.ts index e4085b06f91..c0fc4597f5c 100644 --- a/src/app/shared/file-download-link/themed-file-download-link.component.ts +++ b/src/app/shared/file-download-link/themed-file-download-link.component.ts @@ -9,10 +9,11 @@ import { ThemedComponent } from '../theme-support/themed.component'; import { FileDownloadLinkComponent } from './file-download-link.component'; @Component({ - selector: 'ds-themed-file-download-link', + selector: 'ds-file-download-link', styleUrls: [], templateUrl: '../theme-support/themed.component.html', standalone: true, + imports: [FileDownloadLinkComponent], }) export class ThemedFileDownloadLinkComponent extends ThemedComponent { diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.html index 606db29db37..7224f1843dd 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.html @@ -10,7 +10,7 @@
    + 'd-none': this.metadataService.isVirtual(value) && (model.hasSelectableMetadata || context?.index > 0)}">
    @@ -53,7 +53,7 @@
    - + { { provide: DsDynamicTypeBindRelationService, useValue: getMockDsDynamicTypeBindRelationService() }, { provide: RelationshipDataService, useValue: {} }, { provide: SelectableListService, useValue: {} }, - { provide: ItemDataService, useValue: {} }, { provide: Store, useValue: {} }, { provide: RelationshipDataService, useValue: {} }, { provide: SelectableListService, useValue: {} }, - { provide: FormService, useValue: {} }, { provide: FormBuilderService, useValue: {} }, { provide: SubmissionService, useValue: {} }, { @@ -241,7 +237,6 @@ describe('DsDynamicFormControlContainerComponent test suite', () => { findById: () => observableOf(createSuccessfulRemoteDataObject(testWSI)), }, }, - { provide: NgZone, useValue: new NgZone({}) }, { provide: APP_CONFIG, useValue: environment }, { provide: APP_DATA_SERVICES_MAP, useValue: {} }, { provide: DYNAMIC_FORM_CONTROL_MAP_FN, useValue: dsDynamicFormControlMapFn }, diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts index 1ef9a9e3504..7d89f64e5c2 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts @@ -6,15 +6,16 @@ import { NgTemplateOutlet, } from '@angular/common'; import { + AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ComponentFactoryResolver, ContentChildren, + DoCheck, EventEmitter, Inject, Input, - NgZone, OnChanges, OnDestroy, OnInit, @@ -77,10 +78,10 @@ import { AppConfig, } from '../../../../../config/app-config.interface'; import { AppState } from '../../../../app.reducer'; -import { ItemDataService } from '../../../../core/data/item-data.service'; import { PaginatedList } from '../../../../core/data/paginated-list.model'; import { RelationshipDataService } from '../../../../core/data/relationship-data.service'; import { RemoteData } from '../../../../core/data/remote-data'; +import { MetadataService } from '../../../../core/metadata/metadata.service'; import { Collection } from '../../../../core/shared/collection.model'; import { DSpaceObject } from '../../../../core/shared/dspace-object.model'; import { Item } from '../../../../core/shared/item.model'; @@ -112,7 +113,6 @@ import { SelectableListService } from '../../../object-list/selectable-list/sele import { SearchResult } from '../../../search/models/search-result.model'; import { followLink } from '../../../utils/follow-link-config.model'; import { itemLinksToFollow } from '../../../utils/relation-query.utils'; -import { FormService } from '../../form.service'; import { FormBuilderService } from '../form-builder.service'; import { FormFieldMetadataValueObject } from '../models/form-field-metadata-value.model'; import { RelationshipOptions } from '../models/relationship-options.model'; @@ -145,7 +145,8 @@ import { DsDynamicLookupRelationModalComponent } from './relation-lookup-modal/d ], standalone: true, }) -export class DsDynamicFormControlContainerComponent extends DynamicFormControlContainerComponent implements OnInit, OnChanges, OnDestroy { +export class DsDynamicFormControlContainerComponent extends DynamicFormControlContainerComponent + implements OnInit, OnChanges, OnDestroy, AfterViewInit, DoCheck { @ContentChildren(DynamicTemplateDirective) contentTemplateList: QueryList; // eslint-disable-next-line @angular-eslint/no-input-rename @Input('templates') inputTemplateList: QueryList; @@ -155,7 +156,7 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo @Input() formModel: DynamicFormControlModel[]; @Input() asBootstrapFormGroup = false; @Input() bindId = true; - @Input() context: any | null = null; + @Input() context: any = null; @Input() group: UntypedFormGroup; @Input() hostClass: string[]; @Input() hasErrorMessaging = false; @@ -202,17 +203,15 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo protected typeBindRelationService: DsDynamicTypeBindRelationService, protected translateService: TranslateService, protected relationService: DynamicFormRelationService, - private modalService: NgbModal, - private relationshipService: RelationshipDataService, - private selectableListService: SelectableListService, - private itemService: ItemDataService, - private zone: NgZone, - private store: Store, - private submissionObjectService: SubmissionObjectDataService, - private ref: ChangeDetectorRef, - private formService: FormService, - public formBuilderService: FormBuilderService, - private submissionService: SubmissionService, + protected modalService: NgbModal, + protected relationshipService: RelationshipDataService, + protected selectableListService: SelectableListService, + protected store: Store, + protected submissionObjectService: SubmissionObjectDataService, + protected ref: ChangeDetectorRef, + protected formBuilderService: FormBuilderService, + protected submissionService: SubmissionService, + protected metadataService: MetadataService, @Inject(APP_CONFIG) protected appConfig: AppConfig, @Inject(DYNAMIC_FORM_CONTROL_MAP_FN) protected dynamicFormControlFn: DynamicFormControlMapFn, ) { @@ -277,11 +276,11 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo this.value = Object.assign(new FormFieldMetadataValueObject(), this.model.value); } - if (hasValue(this.value) && this.value.isVirtual) { - const relationship$ = this.relationshipService.findById(this.value.virtualValue, + if (hasValue(this.value) && this.metadataService.isVirtual(this.value)) { + const relationship$ = this.relationshipService.findById(this.metadataService.virtualValue(this.value), true, true, - ... itemLinksToFollow(this.fetchThumbnail)).pipe( + ... itemLinksToFollow(this.fetchThumbnail, this.appConfig.item.showAccessStatuses)).pipe( getAllSucceededRemoteData(), getRemoteDataPayload()); this.relationshipValue$ = observableCombineLatest([this.item$.pipe(take(1)), relationship$]).pipe( diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.html index 001e0bc339c..944e4650abd 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component.html @@ -1,7 +1,7 @@
    - + diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/existing-relation-list-element/existing-relation-list-element.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/existing-relation-list-element/existing-relation-list-element.component.html index 5a5bd2ff2ae..08efff42197 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/existing-relation-list-element/existing-relation-list-element.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/existing-relation-list-element/existing-relation-list-element.component.html @@ -1,7 +1,7 @@
    - + diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.component.ts index 4589171e364..62a6b8044fd 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.component.ts @@ -96,6 +96,8 @@ export class DsDatePickerComponent extends DynamicFormControlComponent implement this.initialDay = now.getUTCDate(); if (this.model && this.model.value !== null) { + // todo: model value could object or Date according to its type annotation + // eslint-disable-next-line @typescript-eslint/no-base-to-string const values = this.model.value.toString().split(DS_DATE_PICKER_SEPARATOR); if (values.length > 0) { this.initialYear = parseInt(values[0], 10); diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list-radio-group.model.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list-radio-group.model.ts index d6ab989cd53..af5f65802d1 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list-radio-group.model.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list-radio-group.model.ts @@ -1,5 +1,6 @@ import { DynamicFormControlLayout, + DynamicFormControlRelation, DynamicRadioGroupModel, DynamicRadioGroupModelConfig, serializable, @@ -16,12 +17,14 @@ export interface DynamicListModelConfig extends DynamicRadioGroupModelConfig { @serializable() vocabularyOptions: VocabularyOptions; @serializable() repeatable: boolean; + @serializable() typeBindRelations: DynamicFormControlRelation[]; @serializable() groupLength: number; @serializable() required: boolean; @serializable() hint: string; @@ -36,6 +39,7 @@ export class DynamicListRadioGroupModel extends DynamicRadioGroupModel { this.required = config.required; this.hint = config.hint; this.value = config.value; + this.typeBindRelations = config.typeBindRelations ? config.typeBindRelations : []; } get hasAuthority(): boolean { diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component.ts index d65e06313f8..22f8dce2444 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component.ts @@ -9,6 +9,7 @@ import { Component, EventEmitter, Input, + OnDestroy, OnInit, Output, ViewChild, @@ -92,7 +93,7 @@ import { DynamicOneboxModel } from './dynamic-onebox.model'; ], standalone: true, }) -export class DsDynamicOneboxComponent extends DsDynamicVocabularyComponent implements OnInit { +export class DsDynamicOneboxComponent extends DsDynamicVocabularyComponent implements OnDestroy, OnInit { @Input() group: UntypedFormGroup; @Input() model: DynamicOneboxModel; @@ -293,7 +294,7 @@ export class DsDynamicOneboxComponent extends DsDynamicVocabularyComponent imple const modalRef: NgbModalRef = this.modalService.open(VocabularyTreeviewModalComponent, { size: 'lg', windowClass: 'treeview' }); modalRef.componentInstance.vocabularyOptions = this.model.vocabularyOptions; modalRef.componentInstance.preloadLevel = preloadLevel; - modalRef.componentInstance.selectedItems = this.currentValue ? [this.currentValue.value] : []; + modalRef.componentInstance.selectedItems = this.currentValue ? [this.currentValue] : []; modalRef.result.then((result: VocabularyEntryDetail) => { if (result) { this.currentValue = result; diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.component.html index e661c458b67..38f15a5ed1c 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.component.html @@ -57,7 +57,7 @@
    - +
    +
    - - +

    {{ 'submission.sections.describe.relationship-lookup.selection-tab.title.' + externalSource.id | translate}}

    @@ -21,8 +21,8 @@

    {{ 'submission.sections.describe.relationship-lookup.selection-tab.title.' + [importConfig]="importConfig" (importObject)="import($event)"> - +
    diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component.spec.ts index d7090b55c8f..79831fe8ca2 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component.spec.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component.spec.ts @@ -174,7 +174,7 @@ describe('DsDynamicLookupRelationExternalSourceTabComponent', () => { }); it('should display a ds-themed-loading component', () => { - const loading = fixture.debugElement.query(By.css('ds-themed-loading')); + const loading = fixture.debugElement.query(By.css('ds-loading')); expect(loading).not.toBeNull(); }); }); diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component.ts index 484abce7d65..5a1f13544ff 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/dynamic-lookup-relation-external-source-tab.component.ts @@ -63,7 +63,7 @@ import { ExternalSourceEntryImportModalComponent } from './external-source-entry import { ThemedExternalSourceEntryImportModalComponent } from './external-source-entry-import-modal/themed-external-source-entry-import-modal.component'; @Component({ - selector: 'ds-dynamic-lookup-relation-external-source-tab', + selector: 'ds-base-dynamic-lookup-relation-external-source-tab', styleUrls: ['./dynamic-lookup-relation-external-source-tab.component.scss'], templateUrl: './dynamic-lookup-relation-external-source-tab.component.html', providers: [ diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/external-source-entry-import-modal/external-source-entry-import-modal.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/external-source-entry-import-modal/external-source-entry-import-modal.component.spec.ts index c0090f36a30..e2a4bbf4bbc 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/external-source-entry-import-modal/external-source-entry-import-modal.component.spec.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/external-source-entry-import-modal/external-source-entry-import-modal.component.spec.ts @@ -21,7 +21,7 @@ import { NotificationsService } from '../../../../../../notifications/notificati import { ItemSearchResult } from '../../../../../../object-collection/shared/item-search-result.model'; import { SelectableListService } from '../../../../../../object-list/selectable-list/selectable-list.service'; import { createSuccessfulRemoteDataObject$ } from '../../../../../../remote-data.utils'; -import { SearchResultsComponent } from '../../../../../../search/search-results/search-results.component'; +import { ThemedSearchResultsComponent } from '../../../../../../search/search-results/themed-search-results.component'; import { createPaginatedList } from '../../../../../../testing/utils.test'; import { RelationshipOptions } from '../../../../models/relationship-options.model'; import { @@ -97,7 +97,7 @@ describe('DsDynamicLookupRelationExternalSourceTabComponent', () => { schemas: [NO_ERRORS_SCHEMA], }) .overrideComponent(ExternalSourceEntryImportModalComponent, { - remove: { imports: [SearchResultsComponent] }, + remove: { imports: [ThemedSearchResultsComponent] }, }) .compileComponents(); })); diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/external-source-entry-import-modal/external-source-entry-import-modal.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/external-source-entry-import-modal/external-source-entry-import-modal.component.ts index d9581dcd85c..c2516ca9a81 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/external-source-entry-import-modal/external-source-entry-import-modal.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/external-source-entry-import-modal/external-source-entry-import-modal.component.ts @@ -46,7 +46,7 @@ import { SelectableListService } from '../../../../../../object-list/selectable- import { PaginationComponentOptions } from '../../../../../../pagination/pagination-component-options.model'; import { PaginatedSearchOptions } from '../../../../../../search/models/paginated-search-options.model'; import { SearchResult } from '../../../../../../search/models/search-result.model'; -import { SearchResultsComponent } from '../../../../../../search/search-results/search-results.component'; +import { ThemedSearchResultsComponent } from '../../../../../../search/search-results/themed-search-results.component'; import { RelationshipOptions } from '../../../../models/relationship-options.model'; /** @@ -61,12 +61,12 @@ export enum ImportType { } @Component({ - selector: 'ds-external-source-entry-import-modal', + selector: 'ds-base-external-source-entry-import-modal', styleUrls: ['./external-source-entry-import-modal.component.scss'], templateUrl: './external-source-entry-import-modal.component.html', imports: [ TranslateModule, - SearchResultsComponent, + ThemedSearchResultsComponent, NgIf, AsyncPipe, ], diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/external-source-entry-import-modal/themed-external-source-entry-import-modal.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/external-source-entry-import-modal/themed-external-source-entry-import-modal.component.ts index f9d2137f9bb..35dba9ca543 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/external-source-entry-import-modal/themed-external-source-entry-import-modal.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/external-source-entry-import-modal/themed-external-source-entry-import-modal.component.ts @@ -4,10 +4,11 @@ import { ThemedComponent } from '../../../../../../theme-support/themed.componen import { ExternalSourceEntryImportModalComponent } from './external-source-entry-import-modal.component'; @Component({ - selector: 'ds-themed-external-source-entry-import-modal', + selector: 'ds-external-source-entry-import-modal', styleUrls: [], templateUrl: '../../../../../../../shared/theme-support/themed.component.html', standalone: true, + imports: [ExternalSourceEntryImportModalComponent], }) export class ThemedExternalSourceEntryImportModalComponent extends ThemedComponent { protected getComponentName(): string { diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/themed-dynamic-lookup-relation-external-source-tab.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/themed-dynamic-lookup-relation-external-source-tab.component.ts index 0673bd41af3..9e4df4f5f8e 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/themed-dynamic-lookup-relation-external-source-tab.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/external-source-tab/themed-dynamic-lookup-relation-external-source-tab.component.ts @@ -15,10 +15,11 @@ import { RelationshipOptions } from '../../../models/relationship-options.model' import { DsDynamicLookupRelationExternalSourceTabComponent } from './dynamic-lookup-relation-external-source-tab.component'; @Component({ - selector: 'ds-themed-dynamic-lookup-relation-external-source-tab', + selector: 'ds-dynamic-lookup-relation-external-source-tab', styleUrls: [], templateUrl: '../../../../../theme-support/themed.component.html', standalone: true, + imports: [DsDynamicLookupRelationExternalSourceTabComponent], }) export class ThemedDynamicLookupRelationExternalSourceTabComponent extends ThemedComponent { protected inAndOutputNames: (keyof DsDynamicLookupRelationExternalSourceTabComponent & keyof this)[] = ['label', 'listId', diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/relationship.effects.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/relationship.effects.spec.ts index 66684058cd2..0588bab7d14 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/relationship.effects.spec.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/relationship.effects.spec.ts @@ -197,6 +197,8 @@ describe('RelationshipEffects', () => { let action; describe('When the last value in the debounceMap is also an ADD_RELATIONSHIP action', () => { beforeEach(() => { + jasmine.getEnv().allowRespy(true); + spyOn((relationEffects as any), 'addRelationship').and.returnValue(createSuccessfulRemoteDataObject$(relationship)); (relationEffects as any).initialActionMap[identifier] = RelationshipActionTypes.ADD_RELATIONSHIP; ((relationEffects as any).debounceTime as jasmine.Spy).and.returnValue((v) => v); }); @@ -262,6 +264,8 @@ describe('RelationshipEffects', () => { let action; describe('When the last value in the debounceMap is also an REMOVE_RELATIONSHIP action', () => { beforeEach(() => { + jasmine.getEnv().allowRespy(true); + spyOn((relationEffects as any), 'removeRelationship').and.returnValue(createSuccessfulRemoteDataObject$(undefined)); ((relationEffects as any).debounceTime as jasmine.Spy).and.returnValue((v) => v); (relationEffects as any).initialActionMap[identifier] = RelationshipActionTypes.REMOVE_RELATIONSHIP; }); @@ -271,7 +275,7 @@ describe('RelationshipEffects', () => { actions = hot('--a-|', { a: action }); const expected = cold('--b-|', { b: undefined }); expect(relationEffects.mapLastActions$).toBeObservable(expected); - expect((relationEffects as any).removeRelationship).toHaveBeenCalledWith(leftItem, rightItem, relationshipType.leftwardType, '1234'); + expect((relationEffects as any).removeRelationship).toHaveBeenCalledWith(leftItem, rightItem, relationshipType.leftwardType); }); }); diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/relationship.effects.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/relationship.effects.ts index 51b3b701364..cb49c831155 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/relationship.effects.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/relationship.effects.ts @@ -1,3 +1,4 @@ + import { Inject, Injectable, @@ -12,13 +13,16 @@ import { TranslateService } from '@ngx-translate/core'; import { BehaviorSubject, Observable, + Subject, } from 'rxjs'; import { + concatMap, filter, map, mergeMap, switchMap, take, + tap, } from 'rxjs/operators'; import { ObjectCacheService } from '../../../../../core/cache/object-cache.service'; @@ -58,11 +62,31 @@ import { const DEBOUNCE_TIME = 500; +enum RelationOperationType { + Add, + Remove, +} + +interface RelationOperation { + type: RelationOperationType + item1: Item + item2: Item + relationshipType: string + submissionId: string + nameVariant?: string +} + /** * NGRX effects for RelationshipEffects */ @Injectable() export class RelationshipEffects { + + /** + * Queue to hold all requests, so we can ensure they get sent one at a time + */ + private requestQueue: Subject = new Subject(); + /** * Map that keeps track of the latest RelationshipEffects for each relationship's composed identifier */ @@ -104,9 +128,22 @@ export class RelationshipEffects { nameVariant = this.nameVariantUpdates[identifier]; delete this.nameVariantUpdates[identifier]; } - this.addRelationship(item1, item2, relationshipType, submissionId, nameVariant); + this.requestQueue.next({ + type: RelationOperationType.Add, + item1, + item2, + relationshipType, + submissionId, + nameVariant, + }); } else { - this.removeRelationship(item1, item2, relationshipType, submissionId); + this.requestQueue.next({ + type: RelationOperationType.Remove, + item1, + item2, + relationshipType, + submissionId, + }); } } delete this.debounceMap[identifier]; @@ -183,8 +220,41 @@ export class RelationshipEffects { private selectableListService: SelectableListService, @Inject(DEBOUNCE_TIME_OPERATOR) private debounceTime: (dueTime: number) => (source: Observable) => Observable, ) { + this.executeRequestsInQueue(); } + /** + * Subscribe to the request queue, execute the requests inside. Wait for each request to complete + * before sending the next one + * @private + */ + private executeRequestsInQueue() { + this.requestQueue.pipe( + // concatMap ensures the next request in the queue will only start after the previous one has emitted + concatMap((next: RelationOperation) => { + switch (next.type) { + case RelationOperationType.Add: + return this.addRelationship(next.item1, next.item2, next.relationshipType, next.submissionId, next.nameVariant).pipe( + map(() => next), + ); + case RelationOperationType.Remove: + return this.removeRelationship(next.item1, next.item2, next.relationshipType).pipe( + map(() => next), + ); + default: + return [next]; + } + }), + // refresh the workspaceitem after each request. It would be great if we could find a way to + // optimize this so it only happens when the queue is empty. + switchMap((next: RelationOperation) => this.refreshWorkspaceItemInCache(next.submissionId)), + // update the form after the workspaceitem is refreshed + ).subscribe((next: SubmissionObject) => { + this.store.dispatch(new SaveSubmissionSectionFormSuccessAction(next.id, [next], false)); + }); + } + + private createIdentifier(item1: Item, item2: Item, relationshipType: string): string { return `${item1.uuid}-${item2.uuid}-${relationshipType}`; } @@ -207,7 +277,7 @@ export class RelationshipEffects { } }), take(1), - switchMap((rd: RemoteData) => { + tap((rd: RemoteData) => { if (hasNoValue(rd) || rd.hasFailed) { // An error occurred, deselect the object from the selectable list and display an error notification const listId = `list-${submissionId}-${relationshipType}`; @@ -225,19 +295,15 @@ export class RelationshipEffects { } this.notificationsService.error(this.translateService.instant('relationships.add.error.title'), errorContent); } - return this.refreshWorkspaceItemInCache(submissionId); }), - ).subscribe((submissionObject: SubmissionObject) => this.store.dispatch(new SaveSubmissionSectionFormSuccessAction(submissionId, [submissionObject], false))); + ); } - private removeRelationship(item1: Item, item2: Item, relationshipType: string, submissionId: string) { - this.relationshipService.getRelationshipByItemsAndLabel(item1, item2, relationshipType).pipe( + private removeRelationship(item1: Item, item2: Item, relationshipType: string) { + return this.relationshipService.getRelationshipByItemsAndLabel(item1, item2, relationshipType).pipe( mergeMap((relationship: Relationship) => this.relationshipService.deleteRelationship(relationship.id, 'none')), take(1), - switchMap(() => this.refreshWorkspaceItemInCache(submissionId)), - ).subscribe((submissionObject: SubmissionObject) => { - this.store.dispatch(new SaveSubmissionSectionFormSuccessAction(submissionId, [submissionObject], false)); - }); + ); } /** diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component.html index 12bc93ab0e6..02dec72baa4 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component.html @@ -1,7 +1,8 @@ -
    -
    -
    - - - - - -
    -
    -
    +
    -
    + diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component.scss b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component.scss index f73f8b8c34b..b4bda36e164 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component.scss +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component.scss @@ -1,3 +1,7 @@ .position-absolute { right: var(--bs-spacer); } + +.dropdown-button { + z-index: 3; +} diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component.ts index 8bf1c9c8b4c..b72fbf40a80 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component.ts @@ -53,7 +53,7 @@ import { RelationshipOptions } from '../../../models/relationship-options.model' @Component({ - selector: 'ds-dynamic-lookup-relation-search-tab', + selector: 'ds-base-dynamic-lookup-relation-search-tab', styleUrls: ['./dynamic-lookup-relation-search-tab.component.scss'], templateUrl: './dynamic-lookup-relation-search-tab.component.html', providers: [ @@ -129,15 +129,20 @@ export class DsDynamicLookupRelationSearchTabComponent implements OnInit, OnDest */ @Input() isEditRelationship: boolean; + /** + * A hidden query that will be used but not displayed in the url/searchbar + */ + @Input() hiddenQuery: string; + /** * Send an event to deselect an object from the list */ - @Output() deselectObject: EventEmitter = new EventEmitter(); + @Output() deselectObject: EventEmitter> = new EventEmitter(); /** * Send an event to select an object from the list */ - @Output() selectObject: EventEmitter = new EventEmitter(); + @Output() selectObject: EventEmitter> = new EventEmitter(); /** * Search results @@ -214,7 +219,7 @@ export class DsDynamicLookupRelationSearchTabComponent implements OnInit, OnDest this.selection$ .pipe(take(1)) .subscribe((selection: SearchResult[]) => { - const filteredPage = page.filter((pageItem) => selection.findIndex((selected) => selected.equals(pageItem)) < 0); + const filteredPage: SearchResult[] = page.filter((pageItem: SearchResult) => selection.findIndex((selected: SearchResult) => selected.equals(pageItem)) < 0); this.selectObject.emit(...filteredPage); }); this.selectableListService.select(this.listId, page); diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/themed-dynamic-lookup-relation-search-tab.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/themed-dynamic-lookup-relation-search-tab.component.ts index cc82967080a..6a4cca6bc60 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/themed-dynamic-lookup-relation-search-tab.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/themed-dynamic-lookup-relation-search-tab.component.ts @@ -18,14 +18,15 @@ import { RelationshipOptions } from '../../../models/relationship-options.model' import { DsDynamicLookupRelationSearchTabComponent } from './dynamic-lookup-relation-search-tab.component'; @Component({ - selector: 'ds-themed-dynamic-lookup-relation-search-tab', + selector: 'ds-dynamic-lookup-relation-search-tab', styleUrls: [], templateUrl: '../../../../../theme-support/themed.component.html', standalone: true, + imports: [DsDynamicLookupRelationSearchTabComponent], }) export class ThemedDynamicLookupRelationSearchTabComponent extends ThemedComponent { protected inAndOutputNames: (keyof DsDynamicLookupRelationSearchTabComponent & keyof this)[] = ['relationship', 'listId', - 'query', 'repeatable', 'selection$', 'context', 'relationshipType', 'item', 'isLeft', 'toRemove', 'isEditRelationship', + 'query', 'hiddenQuery', 'repeatable', 'selection$', 'context', 'relationshipType', 'item', 'isLeft', 'toRemove', 'isEditRelationship', 'deselectObject', 'selectObject', 'resultFound']; @Input() relationship: RelationshipOptions; @@ -34,6 +35,8 @@ export class ThemedDynamicLookupRelationSearchTabComponent extends ThemedCompone @Input() query: string; + @Input() hiddenQuery: string; + @Input() repeatable: boolean; @Input() selection$: Observable; @@ -50,9 +53,9 @@ export class ThemedDynamicLookupRelationSearchTabComponent extends ThemedCompone @Input() isEditRelationship: boolean; - @Output() deselectObject: EventEmitter = new EventEmitter(); + @Output() deselectObject: EventEmitter> = new EventEmitter(); - @Output() selectObject: EventEmitter = new EventEmitter(); + @Output() selectObject: EventEmitter> = new EventEmitter(); @Output() resultFound: EventEmitter> = new EventEmitter(); diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/selection-tab/dynamic-lookup-relation-selection-tab.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/selection-tab/dynamic-lookup-relation-selection-tab.component.ts index d1e516513b1..ace5e9bdc7d 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/selection-tab/dynamic-lookup-relation-selection-tab.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/selection-tab/dynamic-lookup-relation-selection-tab.component.ts @@ -6,6 +6,7 @@ import { Component, EventEmitter, Input, + OnInit, Output, } from '@angular/core'; import { Router } from '@angular/router'; @@ -24,6 +25,7 @@ import { import { RemoteData } from '../../../../../../core/data/remote-data'; import { PaginationService } from '../../../../../../core/pagination/pagination.service'; import { Context } from '../../../../../../core/shared/context.model'; +import { DSpaceObject } from '../../../../../../core/shared/dspace-object.model'; import { PageInfo } from '../../../../../../core/shared/page-info.model'; import { SearchConfigurationService } from '../../../../../../core/shared/search/search-configuration.service'; import { SEARCH_CONFIG_SERVICE } from '../../../../../../my-dspace-page/my-dspace-configuration.service'; @@ -33,6 +35,7 @@ import { PageSizeSelectorComponent } from '../../../../../page-size-selector/pag import { PaginationComponentOptions } from '../../../../../pagination/pagination-component-options.model'; import { createSuccessfulRemoteDataObject } from '../../../../../remote-data.utils'; import { PaginatedSearchOptions } from '../../../../../search/models/paginated-search-options.model'; +import { SearchResult } from '../../../../../search/models/search-result.model'; @Component({ selector: 'ds-dynamic-lookup-relation-selection-tab', @@ -57,7 +60,7 @@ import { PaginatedSearchOptions } from '../../../../../search/models/paginated-s /** * Tab for inside the lookup model that represents the currently selected relationships */ -export class DsDynamicLookupRelationSelectionTabComponent { +export class DsDynamicLookupRelationSelectionTabComponent implements OnInit { /** * A string that describes the type of relationship */ @@ -91,12 +94,12 @@ export class DsDynamicLookupRelationSelectionTabComponent { /** * Send an event to deselect an object from the list */ - @Output() deselectObject: EventEmitter = new EventEmitter(); + @Output() deselectObject: EventEmitter> = new EventEmitter(); /** * Send an event to select an object from the list */ - @Output() selectObject: EventEmitter = new EventEmitter(); + @Output() selectObject: EventEmitter> = new EventEmitter(); /** * The initial pagination to use @@ -120,7 +123,7 @@ export class DsDynamicLookupRelationSelectionTabComponent { /** * Set up the selection and pagination on load */ - ngOnInit() { + ngOnInit(): void { this.resetRoute(); this.selectionRD$ = this.searchConfigService.paginatedSearchOptions .pipe( diff --git a/src/app/shared/form/builder/form-builder.service.spec.ts b/src/app/shared/form/builder/form-builder.service.spec.ts index 6cf3f709ab3..1e481391915 100644 --- a/src/app/shared/form/builder/form-builder.service.spec.ts +++ b/src/app/shared/form/builder/form-builder.service.spec.ts @@ -31,12 +31,14 @@ import { DynamicTextAreaModel, DynamicTimePickerModel, } from '@ng-dynamic-forms/core'; +import { TranslateService } from '@ngx-translate/core'; import { FormRowModel } from '../../../core/config/models/config-submission-form.model'; import { SubmissionFormsModel } from '../../../core/config/models/config-submission-forms.model'; import { ConfigurationDataService } from '../../../core/data/configuration-data.service'; import { ConfigurationProperty } from '../../../core/shared/configuration-property.model'; import { VocabularyOptions } from '../../../core/submission/vocabularies/models/vocabulary-options.model'; +import { getMockTranslateService } from '../../mocks/translate.service.mock'; import { createSuccessfulRemoteDataObject$ } from '../../remote-data.utils'; import { DynamicDsDatePickerModel } from './ds-dynamic-form-ui/models/date-picker/date-picker.model'; import { DynamicConcatModel } from './ds-dynamic-form-ui/models/ds-dynamic-concat.model'; @@ -85,6 +87,7 @@ describe('FormBuilderService test suite', () => { beforeEach(() => { configSpy = createConfigSuccessSpy(typeFieldTestValue); + let translateService = getMockTranslateService(); TestBed.configureTestingModule({ imports: [ReactiveFormsModule], providers: [ @@ -93,6 +96,7 @@ describe('FormBuilderService test suite', () => { { provide: NG_VALIDATORS, useValue: testValidator, multi: true }, { provide: NG_ASYNC_VALIDATORS, useValue: testAsyncValidator, multi: true }, { provide: ConfigurationDataService, useValue: configSpy }, + { provide: TranslateService, useValue: translateService }, ], }); diff --git a/src/app/shared/form/builder/models/form-field.model.ts b/src/app/shared/form/builder/models/form-field.model.ts index f81c955fab9..a89801007f9 100644 --- a/src/app/shared/form/builder/models/form-field.model.ts +++ b/src/app/shared/form/builder/models/form-field.model.ts @@ -39,43 +39,43 @@ export class FormFieldModel { * The hints for this metadata field to display on form */ @autoserialize - hints: string; + hints: string; /** * The label for this metadata field to display on form */ @autoserialize - label: string; + label: string; /** * The languages available for this metadata field to display on form */ @autoserialize - languageCodes: LanguageCode[]; + languageCodes: LanguageCode[]; /** * The error message for this metadata field to display on form in case of field is required */ @autoserialize - mandatoryMessage: string; + mandatoryMessage: string; /** * Representing if this metadata field is mandatory or not */ @autoserialize - mandatory: string; + mandatory: string; /** * Representing if this metadata field is repeatable or not */ @autoserialize - repeatable: boolean; + repeatable: boolean; /** * Containing additional properties for this metadata field */ @autoserialize - input: { + input: { /** * Representing the type for this metadata field */ @@ -91,41 +91,41 @@ export class FormFieldModel { * Representing additional vocabulary configuration for this metadata field */ @autoserialize - selectableMetadata: SelectableMetadata[]; + selectableMetadata: SelectableMetadata[]; /** * Representing additional relationship configuration for this metadata field */ @autoserialize - selectableRelationship: RelationshipOptions; + selectableRelationship: RelationshipOptions; @autoserialize - rows: FormRowModel[]; + rows: FormRowModel[]; /** * Representing the scope for this metadata field */ @autoserialize - scope: string; + scope: string; /** * Containing additional css classes for this metadata field to use on form */ @autoserialize - style: string; + style: string; /** * Containing types to bind for this field */ @autoserialize - typeBind: string[]; + typeBind: string[]; /** * Containing the value for this metadata field */ @autoserialize - value: any; + value: any; @autoserialize - visibility: SectionVisibility; + visibility: SectionVisibility; } diff --git a/src/app/shared/form/builder/parsers/concat-field-parser.ts b/src/app/shared/form/builder/parsers/concat-field-parser.ts index 3b29d0eb482..40711c188d1 100644 --- a/src/app/shared/form/builder/parsers/concat-field-parser.ts +++ b/src/app/shared/form/builder/parsers/concat-field-parser.ts @@ -1,4 +1,5 @@ import { Inject } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; import { hasNoValue, @@ -34,17 +35,18 @@ export class ConcatFieldParser extends FieldParser { @Inject(CONFIG_DATA) configData: FormFieldModel, @Inject(INIT_FORM_VALUES) initFormValues, @Inject(PARSER_OPTIONS) parserOptions: ParserOptions, + translate: TranslateService, protected separator: string, protected firstPlaceholder: string = null, protected secondPlaceholder: string = null) { - super(submissionId, configData, initFormValues, parserOptions); + super(submissionId, configData, initFormValues, parserOptions, translate); this.separator = separator; this.firstPlaceholder = firstPlaceholder; this.secondPlaceholder = secondPlaceholder; } - public modelFactory(fieldValue?: FormFieldMetadataValueObject | any, label?: boolean): any { + public modelFactory(fieldValue?: FormFieldMetadataValueObject, label?: boolean): any { const id: string = this.configData.selectableMetadata[0].metadata; const clsInput = { diff --git a/src/app/shared/form/builder/parsers/date-field-parser.spec.ts b/src/app/shared/form/builder/parsers/date-field-parser.spec.ts index d1912a7cad9..ec2172523fe 100644 --- a/src/app/shared/form/builder/parsers/date-field-parser.spec.ts +++ b/src/app/shared/form/builder/parsers/date-field-parser.spec.ts @@ -1,12 +1,17 @@ +import { getMockTranslateService } from 'src/app/shared/mocks/translate.service.mock'; + import { DynamicDsDatePickerModel } from '../ds-dynamic-form-ui/models/date-picker/date-picker.model'; import { FormFieldModel } from '../models/form-field.model'; import { FormFieldMetadataValueObject } from '../models/form-field-metadata-value.model'; import { DateFieldParser } from './date-field-parser'; import { ParserOptions } from './parser-options'; + + describe('DateFieldParser test suite', () => { let field: FormFieldModel; let initFormValues: any = {}; + let translateService = getMockTranslateService(); const submissionId = '1234'; const parserOptions: ParserOptions = { @@ -37,13 +42,13 @@ describe('DateFieldParser test suite', () => { }); it('should init parser properly', () => { - const parser = new DateFieldParser(submissionId, field, initFormValues, parserOptions); + const parser = new DateFieldParser(submissionId, field, initFormValues, parserOptions, translateService); expect(parser instanceof DateFieldParser).toBe(true); }); it('should return a DynamicDsDatePickerModel object when repeatable option is false', () => { - const parser = new DateFieldParser(submissionId, field, initFormValues, parserOptions); + const parser = new DateFieldParser(submissionId, field, initFormValues, parserOptions, translateService); const fieldModel = parser.parse(); @@ -56,7 +61,7 @@ describe('DateFieldParser test suite', () => { }; const expectedValue = '1983-11-18'; - const parser = new DateFieldParser(submissionId, field, initFormValues, parserOptions); + const parser = new DateFieldParser(submissionId, field, initFormValues, parserOptions, translateService); const fieldModel = parser.parse(); diff --git a/src/app/shared/form/builder/parsers/date-field-parser.ts b/src/app/shared/form/builder/parsers/date-field-parser.ts index ea9f193e334..8104568b1aa 100644 --- a/src/app/shared/form/builder/parsers/date-field-parser.ts +++ b/src/app/shared/form/builder/parsers/date-field-parser.ts @@ -9,7 +9,7 @@ import { FieldParser } from './field-parser'; export class DateFieldParser extends FieldParser { - public modelFactory(fieldValue?: FormFieldMetadataValueObject | any, label?: boolean): any { + public modelFactory(fieldValue?: FormFieldMetadataValueObject, label?: boolean): any { let malformedDate = false; const inputDateModelConfig: DynamicDsDateControlModelConfig = this.initModel(null, false, true); inputDateModelConfig.legend = this.configData.label; @@ -18,6 +18,8 @@ export class DateFieldParser extends FieldParser { this.setValues(inputDateModelConfig as any, fieldValue); // Init Data and validity check if (isNotEmpty(inputDateModelConfig.value)) { + // todo: model value could be object or Date according to its type annotation + // eslint-disable-next-line @typescript-eslint/no-base-to-string const value = inputDateModelConfig.value.toString(); if (value.length >= 4) { const valuesArray = value.split(DS_DATE_PICKER_SEPARATOR); diff --git a/src/app/shared/form/builder/parsers/disabled-field-parser.spec.ts b/src/app/shared/form/builder/parsers/disabled-field-parser.spec.ts index fd6908ed3b8..759f357f282 100644 --- a/src/app/shared/form/builder/parsers/disabled-field-parser.spec.ts +++ b/src/app/shared/form/builder/parsers/disabled-field-parser.spec.ts @@ -1,3 +1,5 @@ +import { getMockTranslateService } from 'src/app/shared/mocks/translate.service.mock'; + import { DynamicDisabledModel } from '../ds-dynamic-form-ui/models/disabled/dynamic-disabled.model'; import { FormFieldModel } from '../models/form-field.model'; import { DisabledFieldParser } from './disabled-field-parser'; @@ -6,6 +8,7 @@ import { ParserOptions } from './parser-options'; describe('DisabledFieldParser test suite', () => { let field: FormFieldModel; let initFormValues: any = {}; + let translateService = getMockTranslateService(); const submissionId = '1234'; const parserOptions: ParserOptions = { @@ -35,13 +38,13 @@ describe('DisabledFieldParser test suite', () => { }); it('should init parser properly', () => { - const parser = new DisabledFieldParser(submissionId, field, initFormValues, parserOptions); + const parser = new DisabledFieldParser(submissionId, field, initFormValues, parserOptions, translateService); expect(parser instanceof DisabledFieldParser).toBe(true); }); it('should return a DynamicDisabledModel object when repeatable option is false', () => { - const parser = new DisabledFieldParser(submissionId, field, initFormValues, parserOptions); + const parser = new DisabledFieldParser(submissionId, field, initFormValues, parserOptions, translateService); const fieldModel = parser.parse(); @@ -56,7 +59,7 @@ describe('DisabledFieldParser test suite', () => { }; const expectedValue = 'test description'; - const parser = new DisabledFieldParser(submissionId, field, initFormValues, parserOptions); + const parser = new DisabledFieldParser(submissionId, field, initFormValues, parserOptions, translateService); const fieldModel = parser.parse(); expect(fieldModel.value.value).toEqual(expectedValue); diff --git a/src/app/shared/form/builder/parsers/disabled-field-parser.ts b/src/app/shared/form/builder/parsers/disabled-field-parser.ts index e9b97ba7d93..ac977de40a7 100644 --- a/src/app/shared/form/builder/parsers/disabled-field-parser.ts +++ b/src/app/shared/form/builder/parsers/disabled-field-parser.ts @@ -10,7 +10,7 @@ import { FieldParser } from './field-parser'; */ export class DisabledFieldParser extends FieldParser { - public modelFactory(fieldValue?: FormFieldMetadataValueObject | any, label?: boolean): any { + public modelFactory(fieldValue?: FormFieldMetadataValueObject, label?: boolean): any { const emptyModelConfig: DsDynamicDisabledModelConfig = this.initModel(null, label); this.setValues(emptyModelConfig, fieldValue, true); return new DynamicDisabledModel(emptyModelConfig); diff --git a/src/app/shared/form/builder/parsers/dropdown-field-parser.spec.ts b/src/app/shared/form/builder/parsers/dropdown-field-parser.spec.ts index 0ab376d1aaa..c0c3daa3044 100644 --- a/src/app/shared/form/builder/parsers/dropdown-field-parser.spec.ts +++ b/src/app/shared/form/builder/parsers/dropdown-field-parser.spec.ts @@ -1,3 +1,5 @@ +import { getMockTranslateService } from 'src/app/shared/mocks/translate.service.mock'; + import { DynamicScrollableDropdownModel } from '../ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.model'; import { FormFieldModel } from '../models/form-field.model'; import { DropdownFieldParser } from './dropdown-field-parser'; @@ -5,6 +7,7 @@ import { ParserOptions } from './parser-options'; describe('DropdownFieldParser test suite', () => { let field: FormFieldModel; + let translateService = getMockTranslateService(); const submissionId = '1234'; const initFormValues = {}; @@ -37,13 +40,13 @@ describe('DropdownFieldParser test suite', () => { }); it('should init parser properly', () => { - const parser = new DropdownFieldParser(submissionId, field, initFormValues, parserOptions); + const parser = new DropdownFieldParser(submissionId, field, initFormValues, parserOptions, translateService); expect(parser instanceof DropdownFieldParser).toBe(true); }); it('should return a DynamicScrollableDropdownModel object when repeatable option is false', () => { - const parser = new DropdownFieldParser(submissionId, field, initFormValues, parserOptions); + const parser = new DropdownFieldParser(submissionId, field, initFormValues, parserOptions, translateService); const fieldModel = parser.parse(); @@ -52,7 +55,7 @@ describe('DropdownFieldParser test suite', () => { it('should throw when authority is not passed', () => { field.selectableMetadata[0].controlledVocabulary = null; - const parser = new DropdownFieldParser(submissionId, field, initFormValues, parserOptions); + const parser = new DropdownFieldParser(submissionId, field, initFormValues, parserOptions, translateService); expect(() => parser.parse()) .toThrow(); diff --git a/src/app/shared/form/builder/parsers/dropdown-field-parser.ts b/src/app/shared/form/builder/parsers/dropdown-field-parser.ts index 1fc28d1274b..5a21f794f81 100644 --- a/src/app/shared/form/builder/parsers/dropdown-field-parser.ts +++ b/src/app/shared/form/builder/parsers/dropdown-field-parser.ts @@ -1,5 +1,6 @@ import { Inject } from '@angular/core'; import { DynamicFormControlLayout } from '@ng-dynamic-forms/core'; +import { TranslateService } from '@ngx-translate/core'; import { isNotEmpty } from '../../../empty.util'; import { @@ -24,11 +25,12 @@ export class DropdownFieldParser extends FieldParser { @Inject(CONFIG_DATA) configData: FormFieldModel, @Inject(INIT_FORM_VALUES) initFormValues, @Inject(PARSER_OPTIONS) parserOptions: ParserOptions, + translate: TranslateService, ) { - super(submissionId, configData, initFormValues, parserOptions); + super(submissionId, configData, initFormValues, parserOptions, translate); } - public modelFactory(fieldValue?: FormFieldMetadataValueObject | any, label?: boolean): any { + public modelFactory(fieldValue?: FormFieldMetadataValueObject, label?: boolean): any { const dropdownModelConfig: DynamicScrollableDropdownModelConfig = this.initModel(null, label); let layout: DynamicFormControlLayout; diff --git a/src/app/shared/form/builder/parsers/field-parser.ts b/src/app/shared/form/builder/parsers/field-parser.ts index c77b244d6a3..590d5f564e9 100644 --- a/src/app/shared/form/builder/parsers/field-parser.ts +++ b/src/app/shared/form/builder/parsers/field-parser.ts @@ -8,6 +8,7 @@ import { MATCH_VISIBLE, OR_OPERATOR, } from '@ng-dynamic-forms/core'; +import { TranslateService } from '@ngx-translate/core'; import uniqueId from 'lodash/uniqueId'; import { SubmissionScopeType } from '../../../../core/submission/submission-scope-type'; @@ -61,6 +62,7 @@ export abstract class FieldParser { @Inject(CONFIG_DATA) protected configData: FormFieldModel, @Inject(INIT_FORM_VALUES) protected initFormValues: any, @Inject(PARSER_OPTIONS) protected parserOptions: ParserOptions, + protected translate: TranslateService, ) { } @@ -68,8 +70,8 @@ export abstract class FieldParser { public parse() { if (((this.getInitValueCount() > 1 && !this.configData.repeatable) || (this.configData.repeatable)) - && (this.configData.input.type !== ParserType.List) - && (this.configData.input.type !== ParserType.Tag) + && (this.configData.input.type !== ParserType.List.valueOf()) + && (this.configData.input.type !== ParserType.Tag.valueOf()) ) { let arrayCounter = 0; let fieldArrayCounter = 0; @@ -81,7 +83,7 @@ export abstract class FieldParser { } let isDraggable = true; - if (this.configData.input.type === ParserType.Onebox && this.configData?.selectableMetadata?.length > 1) { + if (this.configData.input.type === ParserType.Onebox.valueOf() && this.configData?.selectableMetadata?.length > 1) { isDraggable = false; } const config = { @@ -344,12 +346,12 @@ export abstract class FieldParser { && isNotEmpty(fieldScope) && isNotEmpty(visibility) && (( - submissionScope === SubmissionScopeType.WorkspaceItem + submissionScope === SubmissionScopeType.WorkspaceItem.valueOf() && visibility.main === VisibilityType.READONLY ) || (visibility.other === VisibilityType.READONLY - && submissionScope === SubmissionScopeType.WorkflowItem + && submissionScope === SubmissionScopeType.WorkflowItem.valueOf() ) ); } @@ -405,11 +407,14 @@ export abstract class FieldParser { } else { regex = new RegExp(this.configData.input.regex); } + const baseTranslationKey = 'error.validation.pattern'; + const fieldranslationKey = `${baseTranslationKey}.${controlModel.id}`; + const fieldTranslationExists = this.translate.instant(fieldranslationKey) !== fieldranslationKey; controlModel.validators = Object.assign({}, controlModel.validators, { pattern: regex }); controlModel.errorMessages = Object.assign( {}, controlModel.errorMessages, - { pattern: 'error.validation.pattern' }); + { pattern: fieldTranslationExists ? fieldranslationKey : baseTranslationKey }); } protected markAsRequired(controlModel) { diff --git a/src/app/shared/form/builder/parsers/list-field-parser.spec.ts b/src/app/shared/form/builder/parsers/list-field-parser.spec.ts index f0a446c0cef..646fee9dec0 100644 --- a/src/app/shared/form/builder/parsers/list-field-parser.spec.ts +++ b/src/app/shared/form/builder/parsers/list-field-parser.spec.ts @@ -1,3 +1,5 @@ +import { getMockTranslateService } from 'src/app/shared/mocks/translate.service.mock'; + import { DynamicListCheckboxGroupModel } from '../ds-dynamic-form-ui/models/list/dynamic-list-checkbox-group.model'; import { DynamicListRadioGroupModel } from '../ds-dynamic-form-ui/models/list/dynamic-list-radio-group.model'; import { FormFieldModel } from '../models/form-field.model'; @@ -8,6 +10,7 @@ import { ParserOptions } from './parser-options'; describe('ListFieldParser test suite', () => { let field: FormFieldModel; let initFormValues = {}; + let translateService = getMockTranslateService(); const submissionId = '1234'; const parserOptions: ParserOptions = { @@ -39,13 +42,13 @@ describe('ListFieldParser test suite', () => { }); it('should init parser properly', () => { - const parser = new ListFieldParser(submissionId, field, initFormValues, parserOptions); + const parser = new ListFieldParser(submissionId, field, initFormValues, parserOptions, translateService); expect(parser instanceof ListFieldParser).toBe(true); }); it('should return a DynamicListCheckboxGroupModel object when repeatable option is true', () => { - const parser = new ListFieldParser(submissionId, field, initFormValues, parserOptions); + const parser = new ListFieldParser(submissionId, field, initFormValues, parserOptions, translateService); const fieldModel = parser.parse(); @@ -54,7 +57,7 @@ describe('ListFieldParser test suite', () => { it('should return a DynamicListRadioGroupModel object when repeatable option is false', () => { field.repeatable = false; - const parser = new ListFieldParser(submissionId, field, initFormValues, parserOptions); + const parser = new ListFieldParser(submissionId, field, initFormValues, parserOptions, translateService); const fieldModel = parser.parse(); @@ -67,7 +70,7 @@ describe('ListFieldParser test suite', () => { }; const expectedValue = [new FormFieldMetadataValueObject('test type')]; - const parser = new ListFieldParser(submissionId, field, initFormValues, parserOptions); + const parser = new ListFieldParser(submissionId, field, initFormValues, parserOptions, translateService); const fieldModel = parser.parse(); diff --git a/src/app/shared/form/builder/parsers/list-field-parser.ts b/src/app/shared/form/builder/parsers/list-field-parser.ts index f786e888ceb..dd619872478 100644 --- a/src/app/shared/form/builder/parsers/list-field-parser.ts +++ b/src/app/shared/form/builder/parsers/list-field-parser.ts @@ -6,7 +6,7 @@ import { FieldParser } from './field-parser'; export class ListFieldParser extends FieldParser { - public modelFactory(fieldValue?: FormFieldMetadataValueObject | any, label?: boolean): any { + public modelFactory(fieldValue?: FormFieldMetadataValueObject, label?: boolean): any { const listModelConfig = this.initModel(null, label); listModelConfig.repeatable = this.configData.repeatable; diff --git a/src/app/shared/form/builder/parsers/lookup-field-parser.spec.ts b/src/app/shared/form/builder/parsers/lookup-field-parser.spec.ts index 6d57e686432..2ed45803edc 100644 --- a/src/app/shared/form/builder/parsers/lookup-field-parser.spec.ts +++ b/src/app/shared/form/builder/parsers/lookup-field-parser.spec.ts @@ -1,3 +1,5 @@ +import { getMockTranslateService } from 'src/app/shared/mocks/translate.service.mock'; + import { DynamicLookupModel } from '../ds-dynamic-form-ui/models/lookup/dynamic-lookup.model'; import { FormFieldModel } from '../models/form-field.model'; import { FormFieldMetadataValueObject } from '../models/form-field-metadata-value.model'; @@ -7,6 +9,7 @@ import { ParserOptions } from './parser-options'; describe('LookupFieldParser test suite', () => { let field: FormFieldModel; let initFormValues = {}; + let translateService = getMockTranslateService(); const submissionId = '1234'; const parserOptions: ParserOptions = { @@ -38,13 +41,13 @@ describe('LookupFieldParser test suite', () => { }); it('should init parser properly', () => { - const parser = new LookupFieldParser(submissionId, field, initFormValues, parserOptions); + const parser = new LookupFieldParser(submissionId, field, initFormValues, parserOptions, translateService); expect(parser instanceof LookupFieldParser).toBe(true); }); it('should return a DynamicLookupModel object when repeatable option is false', () => { - const parser = new LookupFieldParser(submissionId, field, initFormValues, parserOptions); + const parser = new LookupFieldParser(submissionId, field, initFormValues, parserOptions, translateService); const fieldModel = parser.parse(); @@ -57,7 +60,7 @@ describe('LookupFieldParser test suite', () => { }; const expectedValue = new FormFieldMetadataValueObject('test journal'); - const parser = new LookupFieldParser(submissionId, field, initFormValues, parserOptions); + const parser = new LookupFieldParser(submissionId, field, initFormValues, parserOptions, translateService); const fieldModel = parser.parse(); diff --git a/src/app/shared/form/builder/parsers/lookup-field-parser.ts b/src/app/shared/form/builder/parsers/lookup-field-parser.ts index de7ba04e76d..cb75022cfff 100644 --- a/src/app/shared/form/builder/parsers/lookup-field-parser.ts +++ b/src/app/shared/form/builder/parsers/lookup-field-parser.ts @@ -7,7 +7,7 @@ import { FieldParser } from './field-parser'; export class LookupFieldParser extends FieldParser { - public modelFactory(fieldValue?: FormFieldMetadataValueObject | any, label?: boolean): any { + public modelFactory(fieldValue?: FormFieldMetadataValueObject, label?: boolean): any { if (this.configData.selectableMetadata[0].controlledVocabulary) { const lookupModelConfig: DynamicLookupModelConfig = this.initModel(null, label); diff --git a/src/app/shared/form/builder/parsers/lookup-name-field-parser.spec.ts b/src/app/shared/form/builder/parsers/lookup-name-field-parser.spec.ts index c6e8df8985d..3384071f6ee 100644 --- a/src/app/shared/form/builder/parsers/lookup-name-field-parser.spec.ts +++ b/src/app/shared/form/builder/parsers/lookup-name-field-parser.spec.ts @@ -1,3 +1,5 @@ +import { getMockTranslateService } from 'src/app/shared/mocks/translate.service.mock'; + import { DynamicLookupNameModel } from '../ds-dynamic-form-ui/models/lookup/dynamic-lookup-name.model'; import { FormFieldModel } from '../models/form-field.model'; import { FormFieldMetadataValueObject } from '../models/form-field-metadata-value.model'; @@ -7,6 +9,7 @@ import { ParserOptions } from './parser-options'; describe('LookupNameFieldParser test suite', () => { let field: FormFieldModel; let initFormValues = {}; + let translateService = getMockTranslateService(); const submissionId = '1234'; const parserOptions: ParserOptions = { @@ -38,13 +41,13 @@ describe('LookupNameFieldParser test suite', () => { }); it('should init parser properly', () => { - const parser = new LookupNameFieldParser(submissionId, field, initFormValues, parserOptions); + const parser = new LookupNameFieldParser(submissionId, field, initFormValues, parserOptions, translateService); expect(parser instanceof LookupNameFieldParser).toBe(true); }); it('should return a DynamicLookupNameModel object when repeatable option is false', () => { - const parser = new LookupNameFieldParser(submissionId, field, initFormValues, parserOptions); + const parser = new LookupNameFieldParser(submissionId, field, initFormValues, parserOptions, translateService); const fieldModel = parser.parse(); @@ -57,7 +60,7 @@ describe('LookupNameFieldParser test suite', () => { }; const expectedValue = new FormFieldMetadataValueObject('test author'); - const parser = new LookupNameFieldParser(submissionId, field, initFormValues, parserOptions); + const parser = new LookupNameFieldParser(submissionId, field, initFormValues, parserOptions, translateService); const fieldModel = parser.parse(); diff --git a/src/app/shared/form/builder/parsers/lookup-name-field-parser.ts b/src/app/shared/form/builder/parsers/lookup-name-field-parser.ts index 0701d7cb7d4..ba7f2f0235b 100644 --- a/src/app/shared/form/builder/parsers/lookup-name-field-parser.ts +++ b/src/app/shared/form/builder/parsers/lookup-name-field-parser.ts @@ -7,7 +7,7 @@ import { FieldParser } from './field-parser'; export class LookupNameFieldParser extends FieldParser { - public modelFactory(fieldValue?: FormFieldMetadataValueObject | any, label?: boolean): any { + public modelFactory(fieldValue?: FormFieldMetadataValueObject, label?: boolean): any { if (this.configData.selectableMetadata[0].controlledVocabulary) { const lookupModelConfig: DynamicLookupNameModelConfig = this.initModel(null, label); diff --git a/src/app/shared/form/builder/parsers/name-field-parser.spec.ts b/src/app/shared/form/builder/parsers/name-field-parser.spec.ts index 7184e67e96f..80b3f9ee83a 100644 --- a/src/app/shared/form/builder/parsers/name-field-parser.spec.ts +++ b/src/app/shared/form/builder/parsers/name-field-parser.spec.ts @@ -1,3 +1,5 @@ +import { getMockTranslateService } from 'src/app/shared/mocks/translate.service.mock'; + import { DynamicConcatModel } from '../ds-dynamic-form-ui/models/ds-dynamic-concat.model'; import { FormFieldModel } from '../models/form-field.model'; import { FormFieldMetadataValueObject } from '../models/form-field-metadata-value.model'; @@ -9,6 +11,7 @@ describe('NameFieldParser test suite', () => { let field2: FormFieldModel; let field3: FormFieldModel; let initFormValues: any = {}; + let translateService = getMockTranslateService(); const submissionId = '1234'; const parserOptions: ParserOptions = { @@ -71,13 +74,13 @@ describe('NameFieldParser test suite', () => { }); it('should init parser properly', () => { - const parser = new NameFieldParser(submissionId, field1, initFormValues, parserOptions); + const parser = new NameFieldParser(submissionId, field1, initFormValues, parserOptions, translateService); expect(parser instanceof NameFieldParser).toBe(true); }); it('should return a DynamicConcatModel object when repeatable option is false', () => { - const parser = new NameFieldParser(submissionId, field2, initFormValues, parserOptions); + const parser = new NameFieldParser(submissionId, field2, initFormValues, parserOptions, translateService); const fieldModel = parser.parse(); @@ -85,7 +88,7 @@ describe('NameFieldParser test suite', () => { }); it('should return a DynamicConcatModel object with the correct separator', () => { - const parser = new NameFieldParser(submissionId, field2, initFormValues, parserOptions); + const parser = new NameFieldParser(submissionId, field2, initFormValues, parserOptions, translateService); const fieldModel = parser.parse(); @@ -98,7 +101,7 @@ describe('NameFieldParser test suite', () => { }; const expectedValue = new FormFieldMetadataValueObject('test, name', undefined, undefined, 'test'); - const parser = new NameFieldParser(submissionId, field1, initFormValues, parserOptions); + const parser = new NameFieldParser(submissionId, field1, initFormValues, parserOptions, translateService); const fieldModel = parser.parse(); diff --git a/src/app/shared/form/builder/parsers/name-field-parser.ts b/src/app/shared/form/builder/parsers/name-field-parser.ts index 1c0fbd6d01e..4bb36ad4d03 100644 --- a/src/app/shared/form/builder/parsers/name-field-parser.ts +++ b/src/app/shared/form/builder/parsers/name-field-parser.ts @@ -1,4 +1,5 @@ import { Inject } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; import { FormFieldModel } from '../models/form-field.model'; import { ConcatFieldParser } from './concat-field-parser'; @@ -17,7 +18,8 @@ export class NameFieldParser extends ConcatFieldParser { @Inject(CONFIG_DATA) configData: FormFieldModel, @Inject(INIT_FORM_VALUES) initFormValues, @Inject(PARSER_OPTIONS) parserOptions: ParserOptions, + translate: TranslateService, ) { - super(submissionId, configData, initFormValues, parserOptions, ',', 'form.last-name', 'form.first-name'); + super(submissionId, configData, initFormValues, parserOptions, translate, ',', 'form.last-name', 'form.first-name'); } } diff --git a/src/app/shared/form/builder/parsers/onebox-field-parser.spec.ts b/src/app/shared/form/builder/parsers/onebox-field-parser.spec.ts index 467431d963c..b2a50395e1a 100644 --- a/src/app/shared/form/builder/parsers/onebox-field-parser.spec.ts +++ b/src/app/shared/form/builder/parsers/onebox-field-parser.spec.ts @@ -1,3 +1,5 @@ +import { getMockTranslateService } from 'src/app/shared/mocks/translate.service.mock'; + import { DsDynamicInputModel } from '../ds-dynamic-form-ui/models/ds-dynamic-input.model'; import { DynamicQualdropModel } from '../ds-dynamic-form-ui/models/ds-dynamic-qualdrop.model'; import { DynamicOneboxModel } from '../ds-dynamic-form-ui/models/onebox/dynamic-onebox.model'; @@ -10,6 +12,7 @@ describe('OneboxFieldParser test suite', () => { let field1: FormFieldModel; let field2: FormFieldModel; let field3: FormFieldModel; + let translateService = getMockTranslateService(); const submissionId = '1234'; const initFormValues = {}; @@ -73,13 +76,13 @@ describe('OneboxFieldParser test suite', () => { }); it('should init parser properly', () => { - const parser = new OneboxFieldParser(submissionId, field1, initFormValues, parserOptions); + const parser = new OneboxFieldParser(submissionId, field1, initFormValues, parserOptions, translateService); expect(parser instanceof OneboxFieldParser).toBe(true); }); it('should return a DynamicQualdropModel object when selectableMetadata is multiple', () => { - const parser = new OneboxFieldParser(submissionId, field2, initFormValues, parserOptions); + const parser = new OneboxFieldParser(submissionId, field2, initFormValues, parserOptions, translateService); const fieldModel = parser.parse(); @@ -87,7 +90,7 @@ describe('OneboxFieldParser test suite', () => { }); it('should return a DsDynamicInputModel object when selectableMetadata is not multiple', () => { - const parser = new OneboxFieldParser(submissionId, field3, initFormValues, parserOptions); + const parser = new OneboxFieldParser(submissionId, field3, initFormValues, parserOptions, translateService); const fieldModel = parser.parse(); @@ -95,7 +98,7 @@ describe('OneboxFieldParser test suite', () => { }); it('should return a DynamicOneboxModel object when selectableMetadata has authority', () => { - const parser = new OneboxFieldParser(submissionId, field1, initFormValues, parserOptions); + const parser = new OneboxFieldParser(submissionId, field1, initFormValues, parserOptions, translateService); const fieldModel = parser.parse(); @@ -124,7 +127,7 @@ describe('OneboxFieldParser test suite', () => { languageCodes: [], } as FormFieldModel; - parser = new OneboxFieldParser(submissionId, regexField, initFormValues, parserOptions); + parser = new OneboxFieldParser(submissionId, regexField, initFormValues, parserOptions, translateService); fieldModel = parser.parse(); }); diff --git a/src/app/shared/form/builder/parsers/onebox-field-parser.ts b/src/app/shared/form/builder/parsers/onebox-field-parser.ts index 2b994146c72..9c5ef7423e5 100644 --- a/src/app/shared/form/builder/parsers/onebox-field-parser.ts +++ b/src/app/shared/form/builder/parsers/onebox-field-parser.ts @@ -24,7 +24,7 @@ import { FieldParser } from './field-parser'; export class OneboxFieldParser extends FieldParser { - public modelFactory(fieldValue?: FormFieldMetadataValueObject | any, label?: boolean): any { + public modelFactory(fieldValue?: FormFieldMetadataValueObject, label?: boolean): any { if (this.configData.selectableMetadata.length > 1) { // Case Qualdrop Model const clsGroup = { diff --git a/src/app/shared/form/builder/parsers/parser-factory.ts b/src/app/shared/form/builder/parsers/parser-factory.ts index 439baf4d4b1..a56a27d77f7 100644 --- a/src/app/shared/form/builder/parsers/parser-factory.ts +++ b/src/app/shared/form/builder/parsers/parser-factory.ts @@ -1,4 +1,5 @@ import { StaticProvider } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; import { DateFieldParser } from './date-field-parser'; import { DisabledFieldParser } from './disabled-field-parser'; @@ -26,6 +27,7 @@ const fieldParserDeps = [ CONFIG_DATA, INIT_FORM_VALUES, PARSER_OPTIONS, + TranslateService, ]; /** diff --git a/src/app/shared/form/builder/parsers/relation-group-field-parser.spec.ts b/src/app/shared/form/builder/parsers/relation-group-field-parser.spec.ts index ef4f0f709a5..9eeeeea35c3 100644 --- a/src/app/shared/form/builder/parsers/relation-group-field-parser.spec.ts +++ b/src/app/shared/form/builder/parsers/relation-group-field-parser.spec.ts @@ -1,3 +1,5 @@ +import { getMockTranslateService } from 'src/app/shared/mocks/translate.service.mock'; + import { DynamicRelationGroupModel } from '../ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.model'; import { FormFieldModel } from '../models/form-field.model'; import { FormFieldMetadataValueObject } from '../models/form-field-metadata-value.model'; @@ -7,6 +9,7 @@ import { RelationGroupFieldParser } from './relation-group-field-parser'; describe('RelationGroupFieldParser test suite', () => { let field: FormFieldModel; let initFormValues = {}; + let translateService = getMockTranslateService(); const submissionId = '1234'; const parserOptions: ParserOptions = { @@ -73,13 +76,13 @@ describe('RelationGroupFieldParser test suite', () => { }); it('should init parser properly', () => { - const parser = new RelationGroupFieldParser(submissionId, field, initFormValues, parserOptions); + const parser = new RelationGroupFieldParser(submissionId, field, initFormValues, parserOptions, translateService); expect(parser instanceof RelationGroupFieldParser).toBe(true); }); it('should return a DynamicRelationGroupModel object', () => { - const parser = new RelationGroupFieldParser(submissionId, field, initFormValues, parserOptions); + const parser = new RelationGroupFieldParser(submissionId, field, initFormValues, parserOptions, translateService); const fieldModel = parser.parse(); @@ -88,7 +91,7 @@ describe('RelationGroupFieldParser test suite', () => { it('should throw when rows configuration is empty', () => { field.rows = null; - const parser = new RelationGroupFieldParser(submissionId, field, initFormValues, parserOptions); + const parser = new RelationGroupFieldParser(submissionId, field, initFormValues, parserOptions, translateService); expect(() => parser.parse()) .toThrow(); @@ -99,7 +102,7 @@ describe('RelationGroupFieldParser test suite', () => { author: [new FormFieldMetadataValueObject('test author')], affiliation: [new FormFieldMetadataValueObject('test affiliation')], }; - const parser = new RelationGroupFieldParser(submissionId, field, initFormValues, parserOptions); + const parser = new RelationGroupFieldParser(submissionId, field, initFormValues, parserOptions, translateService); const fieldModel = parser.parse(); const expectedValue = [{ diff --git a/src/app/shared/form/builder/parsers/relation-group-field-parser.ts b/src/app/shared/form/builder/parsers/relation-group-field-parser.ts index 112bfdaf243..30401b20efc 100644 --- a/src/app/shared/form/builder/parsers/relation-group-field-parser.ts +++ b/src/app/shared/form/builder/parsers/relation-group-field-parser.ts @@ -11,7 +11,7 @@ import { FieldParser } from './field-parser'; export class RelationGroupFieldParser extends FieldParser { - public modelFactory(fieldValue?: FormFieldMetadataValueObject | any, label?: boolean) { + public modelFactory(fieldValue?: FormFieldMetadataValueObject, label?: boolean) { const modelConfiguration: DynamicRelationGroupModelConfig = this.initModel(null, label); modelConfiguration.submissionId = this.submissionId; diff --git a/src/app/shared/form/builder/parsers/row-parser.spec.ts b/src/app/shared/form/builder/parsers/row-parser.spec.ts index 9fde7cc8392..d87931a4880 100644 --- a/src/app/shared/form/builder/parsers/row-parser.spec.ts +++ b/src/app/shared/form/builder/parsers/row-parser.spec.ts @@ -1,3 +1,7 @@ +import { Injector } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; +import { getMockTranslateService } from 'src/app/shared/mocks/translate.service.mock'; + import { FormRowModel } from '../../../../core/config/models/config-submission-form.model'; import { DynamicRowArrayModel } from '../ds-dynamic-form-ui/models/ds-dynamic-row-array-model'; import { DynamicRowGroupModel } from '../ds-dynamic-form-ui/models/ds-dynamic-row-group-model'; @@ -16,6 +20,7 @@ describe('RowParser test suite', () => { let row8: FormRowModel; let row9: FormRowModel; let row10: FormRowModel; + let injector: Injector; const submissionId = '1234'; const scopeUUID = 'testScopeUUID'; @@ -25,6 +30,12 @@ describe('RowParser test suite', () => { const typeField = 'dc_type'; beforeEach(() => { + let translateService = getMockTranslateService(); + injector = Injector.create({ + providers: [ + { provide: TranslateService, useValue: translateService }, + ], + }); row1 = { fields: [ { @@ -330,14 +341,14 @@ describe('RowParser test suite', () => { }); it('should init parser properly', () => { - const parser = new RowParser(undefined); + const parser = new RowParser(injector); expect(parser instanceof RowParser).toBe(true); }); describe('parse', () => { it('should return a DynamicRowGroupModel object', () => { - const parser = new RowParser(undefined); + const parser = new RowParser(injector); const rowModel = parser.parse(submissionId, row1, scopeUUID, initFormValues, submissionScope, readOnly, typeField); @@ -345,7 +356,7 @@ describe('RowParser test suite', () => { }); it('should return a row with three fields', () => { - const parser = new RowParser(undefined); + const parser = new RowParser(injector); const rowModel = parser.parse(submissionId, row1, scopeUUID, initFormValues, submissionScope, readOnly, typeField); @@ -353,7 +364,7 @@ describe('RowParser test suite', () => { }); it('should return a DynamicRowArrayModel object', () => { - const parser = new RowParser(undefined); + const parser = new RowParser(injector); const rowModel = parser.parse(submissionId, row2, scopeUUID, initFormValues, submissionScope, readOnly, typeField); @@ -361,7 +372,7 @@ describe('RowParser test suite', () => { }); it('should return a row that contains only scoped fields', () => { - const parser = new RowParser(undefined); + const parser = new RowParser(injector); const rowModel = parser.parse(submissionId, row3, scopeUUID, initFormValues, submissionScope, readOnly, typeField); @@ -369,7 +380,7 @@ describe('RowParser test suite', () => { }); it('should be able to parse a dropdown combo field', () => { - const parser = new RowParser(undefined); + const parser = new RowParser(injector); const rowModel = parser.parse(submissionId, row4, scopeUUID, initFormValues, submissionScope, readOnly, typeField); @@ -377,7 +388,7 @@ describe('RowParser test suite', () => { }); it('should be able to parse a lookup-name field', () => { - const parser = new RowParser(undefined); + const parser = new RowParser(injector); const rowModel = parser.parse(submissionId, row5, scopeUUID, initFormValues, submissionScope, readOnly, typeField); @@ -385,7 +396,7 @@ describe('RowParser test suite', () => { }); it('should be able to parse a list field', () => { - const parser = new RowParser(undefined); + const parser = new RowParser(injector); const rowModel = parser.parse(submissionId, row6, scopeUUID, initFormValues, submissionScope, readOnly, typeField); @@ -393,7 +404,7 @@ describe('RowParser test suite', () => { }); it('should be able to parse a date field', () => { - const parser = new RowParser(undefined); + const parser = new RowParser(injector); const rowModel = parser.parse(submissionId, row7, scopeUUID, initFormValues, submissionScope, readOnly, typeField); @@ -401,7 +412,7 @@ describe('RowParser test suite', () => { }); it('should be able to parse a tag field', () => { - const parser = new RowParser(undefined); + const parser = new RowParser(injector); const rowModel = parser.parse(submissionId, row8, scopeUUID, initFormValues, submissionScope, readOnly, typeField); @@ -409,7 +420,7 @@ describe('RowParser test suite', () => { }); it('should be able to parse a textarea field', () => { - const parser = new RowParser(undefined); + const parser = new RowParser(injector); const rowModel = parser.parse(submissionId, row9, scopeUUID, initFormValues, submissionScope, readOnly, typeField); @@ -417,7 +428,7 @@ describe('RowParser test suite', () => { }); it('should be able to parse a group field', () => { - const parser = new RowParser(undefined); + const parser = new RowParser(injector); const rowModel = parser.parse(submissionId, row10, scopeUUID, initFormValues, submissionScope, readOnly, typeField); diff --git a/src/app/shared/form/builder/parsers/row-parser.ts b/src/app/shared/form/builder/parsers/row-parser.ts index 97c9cb82576..b4febfc47ff 100644 --- a/src/app/shared/form/builder/parsers/row-parser.ts +++ b/src/app/shared/form/builder/parsers/row-parser.ts @@ -154,9 +154,9 @@ export class RowParser { && ( isEmpty(visibility) && ( - submissionScope === SubmissionScopeType.WorkspaceItem && scope !== SubmissionFieldScopeType.WorkspaceItem + submissionScope === SubmissionScopeType.WorkspaceItem.valueOf() && scope !== SubmissionFieldScopeType.WorkspaceItem.valueOf() || - submissionScope === SubmissionScopeType.WorkflowItem && scope !== SubmissionFieldScopeType.WorkflowItem + submissionScope === SubmissionScopeType.WorkflowItem.valueOf() && scope !== SubmissionFieldScopeType.WorkflowItem.valueOf() ) ); } diff --git a/src/app/shared/form/builder/parsers/series-field-parser.spec.ts b/src/app/shared/form/builder/parsers/series-field-parser.spec.ts index d4b37e5d7f4..8141bf23e06 100644 --- a/src/app/shared/form/builder/parsers/series-field-parser.spec.ts +++ b/src/app/shared/form/builder/parsers/series-field-parser.spec.ts @@ -1,3 +1,5 @@ +import { getMockTranslateService } from 'src/app/shared/mocks/translate.service.mock'; + import { DynamicConcatModel } from '../ds-dynamic-form-ui/models/ds-dynamic-concat.model'; import { FormFieldModel } from '../models/form-field.model'; import { FormFieldMetadataValueObject } from '../models/form-field-metadata-value.model'; @@ -7,6 +9,7 @@ import { SeriesFieldParser } from './series-field-parser'; describe('SeriesFieldParser test suite', () => { let field: FormFieldModel; let initFormValues: any = {}; + let translateService = getMockTranslateService(); const submissionId = '1234'; const parserOptions: ParserOptions = { @@ -34,13 +37,13 @@ describe('SeriesFieldParser test suite', () => { }); it('should init parser properly', () => { - const parser = new SeriesFieldParser(submissionId, field, initFormValues, parserOptions); + const parser = new SeriesFieldParser(submissionId, field, initFormValues, parserOptions, translateService); expect(parser instanceof SeriesFieldParser).toBe(true); }); it('should return a DynamicConcatModel object when repeatable option is false', () => { - const parser = new SeriesFieldParser(submissionId, field, initFormValues, parserOptions); + const parser = new SeriesFieldParser(submissionId, field, initFormValues, parserOptions, translateService); const fieldModel = parser.parse(); @@ -48,7 +51,7 @@ describe('SeriesFieldParser test suite', () => { }); it('should return a DynamicConcatModel object with the correct separator', () => { - const parser = new SeriesFieldParser(submissionId, field, initFormValues, parserOptions); + const parser = new SeriesFieldParser(submissionId, field, initFormValues, parserOptions, translateService); const fieldModel = parser.parse(); @@ -61,7 +64,7 @@ describe('SeriesFieldParser test suite', () => { }; const expectedValue = new FormFieldMetadataValueObject('test; series', undefined, undefined, 'test'); - const parser = new SeriesFieldParser(submissionId, field, initFormValues, parserOptions); + const parser = new SeriesFieldParser(submissionId, field, initFormValues, parserOptions, translateService); const fieldModel = parser.parse(); diff --git a/src/app/shared/form/builder/parsers/series-field-parser.ts b/src/app/shared/form/builder/parsers/series-field-parser.ts index ce85d07d298..196718ab87c 100644 --- a/src/app/shared/form/builder/parsers/series-field-parser.ts +++ b/src/app/shared/form/builder/parsers/series-field-parser.ts @@ -1,4 +1,5 @@ import { Inject } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; import { FormFieldModel } from '../models/form-field.model'; import { ConcatFieldParser } from './concat-field-parser'; @@ -17,7 +18,8 @@ export class SeriesFieldParser extends ConcatFieldParser { @Inject(CONFIG_DATA) configData: FormFieldModel, @Inject(INIT_FORM_VALUES) initFormValues, @Inject(PARSER_OPTIONS) parserOptions: ParserOptions, + translate: TranslateService, ) { - super(submissionId, configData, initFormValues, parserOptions, ';'); + super(submissionId, configData, initFormValues, parserOptions, translate, ';'); } } diff --git a/src/app/shared/form/builder/parsers/tag-field-parser.spec.ts b/src/app/shared/form/builder/parsers/tag-field-parser.spec.ts index 571d06d983a..303d63cad5d 100644 --- a/src/app/shared/form/builder/parsers/tag-field-parser.spec.ts +++ b/src/app/shared/form/builder/parsers/tag-field-parser.spec.ts @@ -1,3 +1,5 @@ +import { getMockTranslateService } from 'src/app/shared/mocks/translate.service.mock'; + import { DynamicTagModel } from '../ds-dynamic-form-ui/models/tag/dynamic-tag.model'; import { FormFieldModel } from '../models/form-field.model'; import { FormFieldMetadataValueObject } from '../models/form-field-metadata-value.model'; @@ -7,6 +9,7 @@ import { TagFieldParser } from './tag-field-parser'; describe('TagFieldParser test suite', () => { let field: FormFieldModel; let initFormValues: any = {}; + let translateService = getMockTranslateService(); const submissionId = '1234'; const parserOptions: ParserOptions = { @@ -38,13 +41,13 @@ describe('TagFieldParser test suite', () => { }); it('should init parser properly', () => { - const parser = new TagFieldParser(submissionId, field, initFormValues, parserOptions); + const parser = new TagFieldParser(submissionId, field, initFormValues, parserOptions, translateService); expect(parser instanceof TagFieldParser).toBe(true); }); it('should return a DynamicTagModel object when repeatable option is false', () => { - const parser = new TagFieldParser(submissionId, field, initFormValues, parserOptions); + const parser = new TagFieldParser(submissionId, field, initFormValues, parserOptions, translateService); const fieldModel = parser.parse(); @@ -59,7 +62,7 @@ describe('TagFieldParser test suite', () => { ], }; - const parser = new TagFieldParser(submissionId, field, initFormValues, parserOptions); + const parser = new TagFieldParser(submissionId, field, initFormValues, parserOptions, translateService); const fieldModel = parser.parse(); diff --git a/src/app/shared/form/builder/parsers/tag-field-parser.ts b/src/app/shared/form/builder/parsers/tag-field-parser.ts index 5c3f2e60bc3..6534b9dc5fa 100644 --- a/src/app/shared/form/builder/parsers/tag-field-parser.ts +++ b/src/app/shared/form/builder/parsers/tag-field-parser.ts @@ -7,7 +7,7 @@ import { FieldParser } from './field-parser'; export class TagFieldParser extends FieldParser { - public modelFactory(fieldValue?: FormFieldMetadataValueObject | any, label?: boolean): any { + public modelFactory(fieldValue?: FormFieldMetadataValueObject, label?: boolean): any { const tagModelConfig: DynamicTagModelConfig = this.initModel(null, label); if (this.configData.selectableMetadata[0].controlledVocabulary && this.configData.selectableMetadata[0].controlledVocabulary.length > 0) { diff --git a/src/app/shared/form/builder/parsers/textarea-field-parser.spec.ts b/src/app/shared/form/builder/parsers/textarea-field-parser.spec.ts index 95fef2103c7..cfab7c36e31 100644 --- a/src/app/shared/form/builder/parsers/textarea-field-parser.spec.ts +++ b/src/app/shared/form/builder/parsers/textarea-field-parser.spec.ts @@ -1,3 +1,5 @@ +import { getMockTranslateService } from 'src/app/shared/mocks/translate.service.mock'; + import { DsDynamicTextAreaModel } from '../ds-dynamic-form-ui/models/ds-dynamic-textarea.model'; import { FormFieldModel } from '../models/form-field.model'; import { FormFieldMetadataValueObject } from '../models/form-field-metadata-value.model'; @@ -7,6 +9,7 @@ import { TextareaFieldParser } from './textarea-field-parser'; describe('TextareaFieldParser test suite', () => { let field: FormFieldModel; let initFormValues: any = {}; + let translateService = getMockTranslateService(); const submissionId = '1234'; const parserOptions: ParserOptions = { @@ -36,13 +39,13 @@ describe('TextareaFieldParser test suite', () => { }); it('should init parser properly', () => { - const parser = new TextareaFieldParser(submissionId, field, initFormValues, parserOptions); + const parser = new TextareaFieldParser(submissionId, field, initFormValues, parserOptions, translateService); expect(parser instanceof TextareaFieldParser).toBe(true); }); it('should return a DsDynamicTextAreaModel object when repeatable option is false', () => { - const parser = new TextareaFieldParser(submissionId, field, initFormValues, parserOptions); + const parser = new TextareaFieldParser(submissionId, field, initFormValues, parserOptions, translateService); const fieldModel = parser.parse(); @@ -57,7 +60,7 @@ describe('TextareaFieldParser test suite', () => { }; const expectedValue = 'test description'; - const parser = new TextareaFieldParser(submissionId, field, initFormValues, parserOptions); + const parser = new TextareaFieldParser(submissionId, field, initFormValues, parserOptions, translateService); const fieldModel = parser.parse(); diff --git a/src/app/shared/form/builder/parsers/textarea-field-parser.ts b/src/app/shared/form/builder/parsers/textarea-field-parser.ts index c253899b3d7..a19fa0ccd27 100644 --- a/src/app/shared/form/builder/parsers/textarea-field-parser.ts +++ b/src/app/shared/form/builder/parsers/textarea-field-parser.ts @@ -8,7 +8,7 @@ import { FieldParser } from './field-parser'; export class TextareaFieldParser extends FieldParser { - public modelFactory(fieldValue?: FormFieldMetadataValueObject | any, label?: boolean): any { + public modelFactory(fieldValue?: FormFieldMetadataValueObject, label?: boolean): any { const textAreaModelConfig: DsDynamicTextAreaModelConfig = this.initModel(null, label); const layout = { diff --git a/src/app/shared/form/number-picker/number-picker.component.ts b/src/app/shared/form/number-picker/number-picker.component.ts index e2689191010..b0972366591 100644 --- a/src/app/shared/form/number-picker/number-picker.component.ts +++ b/src/app/shared/form/number-picker/number-picker.component.ts @@ -4,6 +4,7 @@ import { Component, EventEmitter, Input, + OnChanges, OnInit, Output, SimpleChanges, @@ -36,7 +37,7 @@ import { isEmpty } from '../../empty.util'; standalone: true, }) -export class NumberPickerComponent implements OnInit, ControlValueAccessor { +export class NumberPickerComponent implements OnChanges, OnInit, ControlValueAccessor { @Input() id: string; @Input() step: number; @Input() min: number; diff --git a/src/app/shared/form/vocabulary-treeview-modal/vocabulary-treeview-modal.component.html b/src/app/shared/form/vocabulary-treeview-modal/vocabulary-treeview-modal.component.html index 71eb8e14765..851ccaf880d 100644 --- a/src/app/shared/form/vocabulary-treeview-modal/vocabulary-treeview-modal.component.html +++ b/src/app/shared/form/vocabulary-treeview-modal/vocabulary-treeview-modal.component.html @@ -7,9 +7,11 @@

    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 0630a28a76b..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 @@ -16,6 +16,7 @@ import { import { Metadata } from '../../../../../core/shared/metadata.utils'; import { WorkspaceitemSectionUploadFileObject } from '../../../../../core/submission/models/workspaceitem-section-upload-file.model'; import { isNotEmpty } from '../../../../../shared/empty.util'; +import { FileSizePipe } from '../../../../../shared/utils/file-size-pipe'; import { TruncatePipe } from '../../../../../shared/utils/truncate.pipe'; import { SubmissionSectionUploadAccessConditionsComponent } from '../../accessConditions/submission-section-upload-access-conditions.component'; @@ -31,6 +32,7 @@ import { SubmissionSectionUploadAccessConditionsComponent } from '../../accessCo TruncatePipe, NgIf, NgForOf, + FileSizePipe, ], standalone: true, }) diff --git a/src/app/submission/sections/upload/section-upload.component.html b/src/app/submission/sections/upload/section-upload.component.html index 41e912e613a..9d916a4f982 100644 --- a/src/app/submission/sections/upload/section-upload.component.html +++ b/src/app/submission/sections/upload/section-upload.component.html @@ -29,7 +29,7 @@
    - + [submissionId]="submissionId">

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

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

    +
    {{'submission.sections.upload.no-file-uploaded' | translate}}
    diff --git a/src/app/submission/sections/upload/section-upload.component.ts b/src/app/submission/sections/upload/section-upload.component.ts index 5e7388902e3..58008c9dfb6 100644 --- a/src/app/submission/sections/upload/section-upload.component.ts +++ b/src/app/submission/sections/upload/section-upload.component.ts @@ -227,20 +227,21 @@ export class SubmissionSectionUploadComponent extends SectionModelComponent { this.changeDetectorRef.detectChanges(); }), - // retrieve submission's bitstream data from state - combineLatest([this.configMetadataForm$, - this.bitstreamService.getUploadedFilesData(this.submissionId, this.sectionData.id)]).pipe( - filter(([configMetadataForm, { files }]: [SubmissionFormsModel, WorkspaceitemSectionUploadObject]) => { - return isNotEmpty(configMetadataForm) && isNotEmpty(files); + combineLatest([ + this.configMetadataForm$, + this.bitstreamService.getUploadedFilesData(this.submissionId, this.sectionData.id), + ]).pipe( + filter(([configMetadataForm, sectionUploadObject]: [SubmissionFormsModel, WorkspaceitemSectionUploadObject]) => { + return isNotEmpty(configMetadataForm) && isNotEmpty(sectionUploadObject); }), - distinctUntilChanged()) - .subscribe(([configMetadataForm, { primary, files }]: [SubmissionFormsModel, WorkspaceitemSectionUploadObject]) => { - this.primaryBitstreamUUID = primary; - this.fileList = files; - this.fileNames = Array.from(files, file => this.getFileName(configMetadataForm, file)); - }, - ), + distinctUntilChanged(), + ).subscribe(([configMetadataForm, { primary, files }]: [SubmissionFormsModel, WorkspaceitemSectionUploadObject]) => { + this.primaryBitstreamUUID = primary; + this.fileList = files; + this.fileNames = Array.from(files, file => this.getFileName(configMetadataForm, file)); + this.changeDetectorRef.detectChanges(); + }), ); } diff --git a/src/app/submission/submission.service.spec.ts b/src/app/submission/submission.service.spec.ts index c38a8f39d0c..d97ed7e939b 100644 --- a/src/app/submission/submission.service.spec.ts +++ b/src/app/submission/submission.service.spec.ts @@ -52,6 +52,7 @@ import { NotificationsService } from '../shared/notifications/notifications.serv import { createFailedRemoteDataObject } from '../shared/remote-data.utils'; import { SubmissionJsonPatchOperationsServiceStub } from '../shared/testing/submission-json-patch-operations-service.stub'; import { SubmissionRestServiceStub } from '../shared/testing/submission-rest-service.stub'; +import { SectionScope } from './objects/section-visibility.model'; import { CancelSubmissionFormAction, ChangeSubmissionCollectionAction, @@ -82,6 +83,7 @@ describe('SubmissionService test suite', () => { extraction: { config: '', mandatory: true, + scope: SectionScope.Submission, sectionType: 'utils', visibility: { main: 'HIDDEN', @@ -98,6 +100,7 @@ describe('SubmissionService test suite', () => { collection: { config: '', mandatory: true, + scope: SectionScope.Submission, sectionType: 'collection', visibility: { main: 'HIDDEN', @@ -237,6 +240,7 @@ describe('SubmissionService test suite', () => { extraction: { config: '', mandatory: true, + scope: SectionScope.Submission, sectionType: 'utils', visibility: { main: 'HIDDEN', @@ -253,6 +257,7 @@ describe('SubmissionService test suite', () => { collection: { config: '', mandatory: true, + scope: SectionScope.Submission, sectionType: 'collection', visibility: { main: 'HIDDEN', @@ -590,6 +595,7 @@ describe('SubmissionService test suite', () => { describe('getSubmissionSections', () => { it('should return submission form sections', () => { + spyOn(service, 'getSubmissionScope').and.returnValue(SubmissionScopeType.WorkspaceItem); spyOn((service as any).store, 'select').and.returnValue(hot('a|', { a: subState.objects[826], })); @@ -767,6 +773,7 @@ describe('SubmissionService test suite', () => { describe('getSubmissionStatus', () => { it('should return properly submission status', () => { + spyOn(service, 'getSubmissionScope').and.returnValue(SubmissionScopeType.WorkspaceItem); spyOn((service as any).store, 'select').and.returnValue(hot('-a-b', { a: subState, b: validSubState, @@ -826,41 +833,207 @@ describe('SubmissionService test suite', () => { }); describe('isSectionHidden', () => { - it('should return true/false when section is hidden/visible', () => { - let section: any = { - config: '', - header: '', - mandatory: true, - sectionType: 'collection' as any, - visibility: { - main: 'HIDDEN', - other: 'HIDDEN', - }, - collapsed: false, - enabled: true, - data: {}, - errorsToShow: [], - serverValidationErrors: [], - isLoading: false, - isValid: false, - }; - expect(service.isSectionHidden(section)).toBeTruthy(); - - section = { - header: 'submit.progressbar.describe.keyinformation', - config: 'https://rest.api/dspace-spring-rest/api/config/submissionforms/keyinformation', - mandatory: true, - sectionType: 'submission-form', - collapsed: false, - enabled: true, - data: {}, - errorsToShow: [], - serverValidationErrors: [], - isLoading: false, - isValid: false, - }; - expect(service.isSectionHidden(section)).toBeFalsy(); + describe('when submission scope is workspace', () => { + beforeEach(() => { + spyOn(service, 'getSubmissionScope').and.returnValue(SubmissionScopeType.WorkspaceItem); + }); + + describe('and section scope is workspace', () => { + it('should return true when visibility main is HIDDEN and visibility other is null', () => { + let section: any = { + scope: SectionScope.Submission, + visibility: { + main: 'HIDDEN', + other: null, + }, + }; + expect(service.isSectionHidden(section)).toBeTrue(); + }); + it('should return true when both visibility main and other are HIDDEN', () => { + let section: any = { + scope: SectionScope.Submission, + visibility: { + main: 'HIDDEN', + other: 'HIDDEN', + }, + }; + expect(service.isSectionHidden(section)).toBeTrue(); + }); + it('should return false when visibility main is null and visibility other is HIDDEN', () => { + let section: any = { + scope: SectionScope.Submission, + visibility: { + main: null, + other: 'HIDDEN', + }, + }; + expect(service.isSectionHidden(section)).toBeFalse(); + }); + it('should return false when visibility is null', () => { + let section: any = { + scope: SectionScope.Submission, + visibility: null, + }; + expect(service.isSectionHidden(section)).toBeFalse(); + }); + }); + + describe('and section scope is workflow', () => { + it('should return false when visibility main is HIDDEN and visibility other is null', () => { + let section: any = { + scope: SectionScope.Workflow, + visibility: { + main: 'HIDDEN', + other: null, + }, + }; + expect(service.isSectionHidden(section)).toBeFalse(); + }); + it('should return true when both visibility main and other are HIDDEN', () => { + let section: any = { + scope: SectionScope.Workflow, + visibility: { + main: 'HIDDEN', + other: 'HIDDEN', + }, + }; + expect(service.isSectionHidden(section)).toBeTrue(); + }); + it('should return true when visibility main is null and visibility other is HIDDEN', () => { + let section: any = { + scope: SectionScope.Workflow, + visibility: { + main: null, + other: 'HIDDEN', + }, + }; + expect(service.isSectionHidden(section)).toBeTrue(); + }); + it('should return false when visibility is null', () => { + let section: any = { + scope: SectionScope.Workflow, + visibility: null, + }; + expect(service.isSectionHidden(section)).toBeFalse(); + }); + }); + + describe('and section scope is null', () => { + it('should return false', () => { + let section: any = { + scope: null, + visibility: { + main: 'HIDDEN', + other: null, + }, + }; + expect(service.isSectionHidden(section)).toBeFalse(); + }); + }); + }); + + describe('when submission scope is workflow', () => { + beforeEach(() => { + spyOn(service, 'getSubmissionScope').and.returnValue(SubmissionScopeType.WorkflowItem); + }); + + describe('and section scope is workspace', () => { + it('should return false when visibility main is HIDDEN and visibility other is null', () => { + let section: any = { + scope: SectionScope.Submission, + visibility: { + main: 'HIDDEN', + other: null, + }, + }; + expect(service.isSectionHidden(section)).toBeFalse(); + }); + it('should return true when both visibility main and other are HIDDEN', () => { + let section: any = { + scope: SectionScope.Submission, + visibility: { + main: 'HIDDEN', + other: 'HIDDEN', + }, + }; + expect(service.isSectionHidden(section)).toBeTrue(); + }); + it('should return true when visibility main is null and visibility other is HIDDEN', () => { + let section: any = { + scope: SectionScope.Submission, + visibility: { + main: null, + other: 'HIDDEN', + }, + }; + expect(service.isSectionHidden(section)).toBeTrue(); + }); + it('should return false when visibility is null', () => { + let section: any = { + scope: SectionScope.Submission, + visibility: null, + }; + expect(service.isSectionHidden(section)).toBeFalse(); + }); + }); + + describe('and section scope is workflow', () => { + it('should return true when visibility main is HIDDEN and visibility other is null', () => { + let section: any = { + scope: SectionScope.Workflow, + visibility: { + main: 'HIDDEN', + other: null, + }, + }; + expect(service.isSectionHidden(section)).toBeTrue(); + }); + it('should return true when both visibility main and other are HIDDEN', () => { + let section: any = { + scope: SectionScope.Workflow, + visibility: { + main: 'HIDDEN', + other: 'HIDDEN', + }, + }; + expect(service.isSectionHidden(section)).toBeTrue(); + }); + it('should return false when visibility main is null and visibility other is HIDDEN', () => { + let section: any = { + scope: SectionScope.Workflow, + visibility: { + main: null, + other: 'HIDDEN', + }, + }; + expect(service.isSectionHidden(section)).toBeFalse(); + }); + it('should return false when visibility is null', () => { + let section: any = { + scope: SectionScope.Workflow, + visibility: null, + }; + expect(service.isSectionHidden(section)).toBeFalse(); + }); + }); + + describe('and section scope is null', () => { + it('should return false', () => { + let section: any = { + scope: null, + visibility: { + main: 'HIDDEN', + other: null, + }, + }; + expect(service.isSectionHidden(section)).toBeFalse(); + }); + }); + + }); + + }); describe('isSubmissionLoading', () => { diff --git a/src/app/submission/submission.service.ts b/src/app/submission/submission.service.ts index 2f06a319b9e..185d0b459f1 100644 --- a/src/app/submission/submission.service.ts +++ b/src/app/submission/submission.service.ts @@ -50,6 +50,7 @@ import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject, } from '../shared/remote-data.utils'; +import { SectionScope } from './objects/section-visibility.model'; import { SubmissionError } from './objects/submission-error.model'; import { CancelSubmissionFormAction, @@ -505,9 +506,15 @@ export class SubmissionService { * true if section is hidden, false otherwise */ isSectionHidden(sectionData: SubmissionSectionObject): boolean { - return (isNotUndefined(sectionData.visibility) - && sectionData.visibility.main === 'HIDDEN' - && sectionData.visibility.other === 'HIDDEN'); + const submissionScope: SubmissionScopeType = this.getSubmissionScope(); + if (isEmpty(submissionScope) || isEmpty(sectionData.visibility) || isEmpty(sectionData.scope)) { + return false; + } + const convertedSubmissionScope: SectionScope = submissionScope.valueOf() === SubmissionScopeType.WorkspaceItem.valueOf() ? + SectionScope.Submission : SectionScope.Workflow; + const visibility = convertedSubmissionScope.valueOf() === sectionData.scope.valueOf() ? + sectionData.visibility.main : sectionData.visibility.other; + return visibility === 'HIDDEN'; } /** diff --git a/src/app/submission/submit/submission-submit.component.ts b/src/app/submission/submit/submission-submit.component.ts index 84e12321474..c9e087f7d81 100644 --- a/src/app/submission/submit/submission-submit.component.ts +++ b/src/app/submission/submit/submission-submit.component.ts @@ -39,7 +39,7 @@ import { SubmissionService } from '../submission.service'; * This component allows to submit a new workspaceitem. */ @Component({ - selector: 'ds-submission-submit', + selector: 'ds-base-submission-submit', styleUrls: ['./submission-submit.component.scss'], templateUrl: './submission-submit.component.html', standalone: true, diff --git a/src/app/submission/submit/themed-submission-submit.component.ts b/src/app/submission/submit/themed-submission-submit.component.ts index 15faf2c4205..ee5ee74746e 100644 --- a/src/app/submission/submit/themed-submission-submit.component.ts +++ b/src/app/submission/submit/themed-submission-submit.component.ts @@ -7,10 +7,11 @@ import { SubmissionSubmitComponent } from './submission-submit.component'; * Themed wrapper for SubmissionSubmitComponent */ @Component({ - selector: 'ds-themed-submission-submit', + selector: 'ds-submission-submit', styleUrls: [], templateUrl: './../../shared/theme-support/themed.component.html', standalone: true, + imports: [SubmissionSubmitComponent], }) export class ThemedSubmissionSubmitComponent extends ThemedComponent { protected getComponentName(): string { diff --git a/src/app/subscriptions-page/subscriptions-page.component.html b/src/app/subscriptions-page/subscriptions-page.component.html index fcdaaaeaced..e8dfc8fc6ac 100644 --- a/src/app/subscriptions-page/subscriptions-page.component.html +++ b/src/app/subscriptions-page/subscriptions-page.component.html @@ -4,7 +4,7 @@

    {{'subscriptions.title' | translate}}

    - + ('suggestionNotifications'); - -// Reciter Suggestion Targets -// ---------------------------------------------------------------------------- - -/** - * Returns the Suggestion Targets State. - * @function suggestionTargetStateSelector - * @return {SuggestionNotificationsState} - */ -export function suggestionTargetStateSelector(): MemoizedSelector { - return subStateSelector(suggestionNotificationsSelector, 'suggestionTarget'); -} - -/** - * Returns the Suggestion Targets list. - * @function suggestionTargetObjectSelector - * @return {SuggestionTarget[]} - */ -export function suggestionTargetObjectSelector(): MemoizedSelector { - return subStateSelector(suggestionTargetStateSelector(), 'targets'); -} - -/** - * Returns true if the Suggestion Targets are loaded. - * @function isSuggestionTargetLoadedSelector - * @return {boolean} - */ -export const isSuggestionTargetLoadedSelector = createSelector(_getSuggestionTargetState, - (state: SuggestionNotificationsState) => state.suggestionTarget.loaded, -); - -/** - * Returns true if the deduplication sets are processing. - * @function isDeduplicationSetsProcessingSelector - * @return {boolean} - */ -export const isReciterSuggestionTargetProcessingSelector = createSelector(_getSuggestionTargetState, - (state: SuggestionNotificationsState) => state.suggestionTarget.processing, -); - -/** - * Returns the total available pages of Reciter Suggestion Targets. - * @function getSuggestionTargetTotalPagesSelector - * @return {number} - */ -export const getSuggestionTargetTotalPagesSelector = createSelector(_getSuggestionTargetState, - (state: SuggestionNotificationsState) => state.suggestionTarget.totalPages, -); - -/** - * Returns the current page of Suggestion Targets. - * @function getSuggestionTargetCurrentPageSelector - * @return {number} - */ -export const getSuggestionTargetCurrentPageSelector = createSelector(_getSuggestionTargetState, - (state: SuggestionNotificationsState) => state.suggestionTarget.currentPage, -); - -/** - * Returns the total number of Suggestion Targets. - * @function getSuggestionTargetTotalsSelector - * @return {number} - */ -export const getSuggestionTargetTotalsSelector = createSelector(_getSuggestionTargetState, - (state: SuggestionNotificationsState) => state.suggestionTarget.totalElements, -); - -/** - * Returns Suggestion Targets for the current user. - * @function getCurrentUserSuggestionTargetSelector - * @return {SuggestionTarget[]} - */ -export const getCurrentUserSuggestionTargetsSelector = createSelector(_getSuggestionTargetState, - (state: SuggestionNotificationsState) => state.suggestionTarget.currentUserTargets, -); - -/** - * Returns whether or not the user has consulted their suggestions - * @function getCurrentUserSuggestionTargetSelector - * @return {boolean} - */ -export const getCurrentUserSuggestionTargetsVisitedSelector = createSelector(_getSuggestionTargetState, - (state: SuggestionNotificationsState) => state.suggestionTarget.currentUserTargetsVisited, -); diff --git a/src/app/suggestions-page/suggestions-page.component.html b/src/app/suggestions-page/suggestions-page.component.html index 75ba0315f3b..45bdff32bf5 100644 --- a/src/app/suggestions-page/suggestions-page.component.html +++ b/src/app/suggestions-page/suggestions-page.component.html @@ -2,7 +2,8 @@
    -
    + +

    {{'suggestion.suggestionFor' | translate}} @@ -21,7 +22,6 @@

    (ignoreSuggestionClicked)="ignoreSuggestionAllSelected()">

    -
    -
    {{ 'suggestion.count.missing' | translate }}
    + + {{'suggestion.count.missing' | translate}} +
    diff --git a/src/app/suggestions-page/suggestions-page.component.spec.ts b/src/app/suggestions-page/suggestions-page.component.spec.ts index 8da48d740fb..6c19405e943 100644 --- a/src/app/suggestions-page/suggestions-page.component.spec.ts +++ b/src/app/suggestions-page/suggestions-page.component.spec.ts @@ -1,11 +1,10 @@ import { CommonModule } from '@angular/common'; import { NO_ERRORS_SCHEMA } from '@angular/core'; import { - async, ComponentFixture, fakeAsync, TestBed, - tick, + waitForAsync, } from '@angular/core/testing'; import { BrowserModule } from '@angular/platform-browser'; import { @@ -70,7 +69,7 @@ describe('SuggestionPageComponent', () => { }); const paginationService = new PaginationServiceStub(); - beforeEach(async(() => { + beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ imports: [ BrowserModule, @@ -106,7 +105,7 @@ describe('SuggestionPageComponent', () => { }); it('should create', () => { - spyOn(component, 'updatePage').and.stub(); + spyOn(component, 'updatePage').and.callThrough(); scheduler.schedule(() => fixture.detectChanges()); scheduler.flush(); @@ -118,70 +117,72 @@ describe('SuggestionPageComponent', () => { }); it('should update page on pagination change', () => { - spyOn(component, 'updatePage').and.stub(); + spyOn(component, 'updatePage').and.callThrough(); + component.targetId$ = observableOf('testid'); - scheduler.schedule(() => fixture.detectChanges()); + scheduler.schedule(() => component.onPaginationChange()); scheduler.flush(); - component.onPaginationChange(); + expect(component.updatePage).toHaveBeenCalled(); }); - it('should update suggestion on page update', (done) => { + it('should update suggestion on page update', () => { spyOn(component.processing$, 'next'); spyOn(component.suggestionsRD$, 'next'); - scheduler.schedule(() => fixture.detectChanges()); + component.targetId$ = observableOf('testid'); + scheduler.schedule(() => component.updatePage().subscribe()); scheduler.flush(); - paginationService.getFindListOptions().subscribe(() => { - expect(component.processing$.next).toHaveBeenCalled(); - expect(mockSuggestionsService.getSuggestions).toHaveBeenCalled(); - expect(component.suggestionsRD$.next).toHaveBeenCalled(); - expect(mockSuggestionsService.clearSuggestionRequests).toHaveBeenCalled(); - done(); - }); - component.updatePage(); + + expect(component.processing$.next).toHaveBeenCalledTimes(2); + expect(mockSuggestionsService.getSuggestions).toHaveBeenCalled(); + expect(component.suggestionsRD$.next).toHaveBeenCalled(); + expect(mockSuggestionsService.clearSuggestionRequests).toHaveBeenCalled(); }); it('should flag suggestion for deletion', fakeAsync(() => { - spyOn(component, 'updatePage').and.stub(); + spyOn(component, 'updatePage').and.callThrough(); + component.targetId$ = observableOf('testid'); - scheduler.schedule(() => fixture.detectChanges()); + scheduler.schedule(() => component.ignoreSuggestion('1')); scheduler.flush(); - component.ignoreSuggestion('1'); + expect(mockSuggestionsService.ignoreSuggestion).toHaveBeenCalledWith('1'); expect(mockSuggestionsTargetStateService.dispatchRefreshUserSuggestionsAction).toHaveBeenCalled(); - tick(201); expect(component.updatePage).toHaveBeenCalled(); })); it('should flag all suggestion for deletion', () => { - spyOn(component, 'updatePage').and.stub(); + spyOn(component, 'updatePage').and.callThrough(); + component.targetId$ = observableOf('testid'); - scheduler.schedule(() => fixture.detectChanges()); + scheduler.schedule(() => component.ignoreSuggestionAllSelected()); scheduler.flush(); - component.ignoreSuggestionAllSelected(); + expect(mockSuggestionsService.ignoreSuggestionMultiple).toHaveBeenCalled(); expect(mockSuggestionsTargetStateService.dispatchRefreshUserSuggestionsAction).toHaveBeenCalled(); expect(component.updatePage).toHaveBeenCalled(); }); it('should approve and import', () => { - spyOn(component, 'updatePage').and.stub(); + spyOn(component, 'updatePage').and.callThrough(); + component.targetId$ = observableOf('testid'); - scheduler.schedule(() => fixture.detectChanges()); + scheduler.schedule(() => component.approveAndImport({ collectionId: '1234' } as unknown as SuggestionApproveAndImport)); scheduler.flush(); - component.approveAndImport({ collectionId: '1234' } as unknown as SuggestionApproveAndImport); + expect(mockSuggestionsService.approveAndImport).toHaveBeenCalled(); expect(mockSuggestionsTargetStateService.dispatchRefreshUserSuggestionsAction).toHaveBeenCalled(); expect(component.updatePage).toHaveBeenCalled(); }); it('should approve and import multiple suggestions', () => { - spyOn(component, 'updatePage').and.stub(); + spyOn(component, 'updatePage').and.callThrough(); + component.targetId$ = observableOf('testid'); - scheduler.schedule(() => fixture.detectChanges()); + scheduler.schedule(() => component.approveAndImportAllSelected({ collectionId: '1234' } as unknown as SuggestionApproveAndImport)); scheduler.flush(); - component.approveAndImportAllSelected({ collectionId: '1234' } as unknown as SuggestionApproveAndImport); + expect(mockSuggestionsService.approveAndImportMultiple).toHaveBeenCalled(); expect(mockSuggestionsTargetStateService.dispatchRefreshUserSuggestionsAction).toHaveBeenCalled(); expect(component.updatePage).toHaveBeenCalled(); diff --git a/src/app/suggestions-page/suggestions-page.component.ts b/src/app/suggestions-page/suggestions-page.component.ts index f7aa9ca024a..0fc790a125d 100644 --- a/src/app/suggestions-page/suggestions-page.component.ts +++ b/src/app/suggestions-page/suggestions-page.component.ts @@ -26,7 +26,7 @@ import { distinctUntilChanged, map, switchMap, - take, + tap, } from 'rxjs/operators'; import { AuthService } from '../core/auth/auth.service'; @@ -37,11 +37,14 @@ import { import { FindListOptions } from '../core/data/find-list-options.model'; import { PaginatedList } from '../core/data/paginated-list.model'; import { RemoteData } from '../core/data/remote-data'; -import { Suggestion } from '../core/notifications/models/suggestion.model'; -import { SuggestionTarget } from '../core/notifications/models/suggestion-target.model'; +import { Suggestion } from '../core/notifications/suggestions/models/suggestion.model'; +import { SuggestionTarget } from '../core/notifications/suggestions/models/suggestion-target.model'; import { PaginationService } from '../core/pagination/pagination.service'; import { redirectOn4xx } from '../core/shared/authorized.operators'; -import { getFirstSucceededRemoteDataPayload } from '../core/shared/operators'; +import { + getFirstCompletedRemoteData, + getFirstSucceededRemoteDataPayload, +} from '../core/shared/operators'; import { WorkspaceItem } from '../core/submission/models/workspaceitem.model'; import { WorkspaceitemDataService } from '../core/submission/workspaceitem-data.service'; import { SuggestionActionsComponent } from '../notifications/suggestion-actions/suggestion-actions.component'; @@ -52,7 +55,8 @@ import { SuggestionBulkResult, SuggestionsService, } from '../notifications/suggestions.service'; -import { LoadingComponent } from '../shared/loading/loading.component'; +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'; @@ -70,10 +74,11 @@ import { getWorkspaceItemEditRoute } from '../workflowitems-edit-page/workflowit RouterLink, TranslateModule, SuggestionActionsComponent, - LoadingComponent, + ThemedLoadingComponent, PaginationComponent, SuggestionListElementComponent, NgForOf, + AlertComponent, ], standalone: true, }) @@ -149,14 +154,15 @@ export class SuggestionsPageComponent implements OnInit { ); this.targetRD$.pipe( getFirstSucceededRemoteDataPayload(), - ).subscribe((suggestionTarget: SuggestionTarget) => { - this.suggestionTarget = suggestionTarget; - this.suggestionId = suggestionTarget.id; - this.researcherName = suggestionTarget.display; - this.suggestionSource = suggestionTarget.source; - this.researcherUuid = this.suggestionService.getTargetUuid(suggestionTarget); - this.updatePage(); - }); + tap((suggestionTarget: SuggestionTarget) => { + this.suggestionTarget = suggestionTarget; + this.suggestionId = suggestionTarget.id; + this.researcherName = suggestionTarget.display; + this.suggestionSource = suggestionTarget.source; + this.researcherUuid = this.suggestionService.getTargetUuid(suggestionTarget); + }), + switchMap(() => this.updatePage()), + ).subscribe(); this.suggestionTargetsStateService.dispatchMarkUserSuggestionsAsVisitedAction(); } @@ -165,13 +171,13 @@ export class SuggestionsPageComponent implements OnInit { * Called when one of the pagination settings is changed */ onPaginationChange() { - this.updatePage(); + this.updatePage().subscribe(); } /** * Update the list of suggestions */ - updatePage() { + updatePage(): Observable>> { this.processing$.next(true); const pageConfig$: Observable = this.paginationService.getFindListOptions( this.paginationOptions.id, @@ -179,7 +185,8 @@ export class SuggestionsPageComponent implements OnInit { ).pipe( distinctUntilChanged(), ); - combineLatest([this.targetId$, pageConfig$]).pipe( + + return combineLatest([this.targetId$, pageConfig$]).pipe( switchMap(([targetId, config]: [string, FindListOptions]) => { return this.suggestionService.getSuggestions( targetId, @@ -188,12 +195,18 @@ export class SuggestionsPageComponent implements OnInit { config.sort, ); }), - take(1), - ).subscribe((results: PaginatedList) => { - this.processing$.next(false); - this.suggestionsRD$.next(results); - this.suggestionService.clearSuggestionRequests(); - }); + getFirstCompletedRemoteData(), + tap((resultsRD: RemoteData>) => { + this.processing$.next(false); + if (resultsRD.hasSucceeded) { + this.suggestionsRD$.next(resultsRD.payload); + } else { + this.suggestionsRD$.next(null); + } + + this.suggestionService.clearSuggestionRequests(); + }), + ); } /** @@ -201,11 +214,10 @@ export class SuggestionsPageComponent implements OnInit { * @suggestionId */ ignoreSuggestion(suggestionId) { - this.suggestionService.ignoreSuggestion(suggestionId).subscribe(() => { - this.suggestionTargetsStateService.dispatchRefreshUserSuggestionsAction(); - //We add a little delay in the page refresh so that we ensure the deletion has been propagated - setTimeout(() => this.updatePage(), 200); - }); + this.suggestionService.ignoreSuggestion(suggestionId).pipe( + tap(() => this.suggestionTargetsStateService.dispatchRefreshUserSuggestionsAction()), + switchMap(() => this.updatePage()), + ).subscribe(); } /** @@ -213,11 +225,9 @@ export class SuggestionsPageComponent implements OnInit { */ ignoreSuggestionAllSelected() { this.isBulkOperationPending = true; - this.suggestionService - .ignoreSuggestionMultiple(Object.values(this.selectedSuggestions)) - .subscribe((results: SuggestionBulkResult) => { + this.suggestionService.ignoreSuggestionMultiple(Object.values(this.selectedSuggestions)).pipe( + tap((results: SuggestionBulkResult) => { this.suggestionTargetsStateService.dispatchRefreshUserSuggestionsAction(); - this.updatePage(); this.isBulkOperationPending = false; this.selectedSuggestions = {}; if (results.success > 0) { @@ -230,7 +240,9 @@ export class SuggestionsPageComponent implements OnInit { this.translateService.get('suggestion.ignoreSuggestion.bulk.error', { count: results.fails })); } - }); + }), + switchMap(() => this.updatePage()), + ).subscribe(); } /** @@ -238,13 +250,14 @@ export class SuggestionsPageComponent implements OnInit { * @param event contains the suggestion and the target collection */ approveAndImport(event: SuggestionApproveAndImport) { - this.suggestionService.approveAndImport(this.workspaceItemService, event.suggestion, event.collectionId) - .subscribe((workspaceitem: WorkspaceItem) => { + this.suggestionService.approveAndImport(this.workspaceItemService, event.suggestion, event.collectionId).pipe( + tap((workspaceitem: WorkspaceItem) => { const content = this.translateService.instant('suggestion.approveAndImport.success', { url: getWorkspaceItemEditRoute(workspaceitem.id) }); this.notificationService.success('', content, { timeOut:0 }, true); this.suggestionTargetsStateService.dispatchRefreshUserSuggestionsAction(); - this.updatePage(); - }); + }), + switchMap(() => this.updatePage()), + ).subscribe(); } /** @@ -253,11 +266,9 @@ export class SuggestionsPageComponent implements OnInit { */ approveAndImportAllSelected(event: SuggestionApproveAndImport) { this.isBulkOperationPending = true; - this.suggestionService - .approveAndImportMultiple(this.workspaceItemService, Object.values(this.selectedSuggestions), event.collectionId) - .subscribe((results: SuggestionBulkResult) => { + this.suggestionService.approveAndImportMultiple(this.workspaceItemService, Object.values(this.selectedSuggestions), event.collectionId).pipe( + tap((results: SuggestionBulkResult) => { this.suggestionTargetsStateService.dispatchRefreshUserSuggestionsAction(); - this.updatePage(); this.isBulkOperationPending = false; this.selectedSuggestions = {}; if (results.success > 0) { @@ -270,7 +281,9 @@ export class SuggestionsPageComponent implements OnInit { this.translateService.get('suggestion.approveAndImport.bulk.error', { count: results.fails })); } - }); + }), + switchMap(() => this.updatePage()), + ).subscribe(); } /** diff --git a/src/app/suggestions-page/suggestions-page.resolver.ts b/src/app/suggestions-page/suggestions-page.resolver.ts index 48c5a40f315..1314846b6e6 100644 --- a/src/app/suggestions-page/suggestions-page.resolver.ts +++ b/src/app/suggestions-page/suggestions-page.resolver.ts @@ -5,12 +5,11 @@ import { RouterStateSnapshot, } from '@angular/router'; import { Observable } from 'rxjs'; -import { find } from 'rxjs/operators'; import { RemoteData } from '../core/data/remote-data'; -import { SuggestionTarget } from '../core/notifications/models/suggestion-target.model'; -import { SuggestionTargetDataService } from '../core/notifications/target/suggestion-target-data.service'; -import { hasValue } from '../shared/empty.util'; +import { SuggestionTarget } from '../core/notifications/suggestions/models/suggestion-target.model'; +import { SuggestionTargetDataService } from '../core/notifications/suggestions/target/suggestion-target-data.service'; +import { getFirstCompletedRemoteData } from '../core/shared/operators'; /** * Method for resolving a suggestion target based on the parameters in the current route @@ -26,6 +25,6 @@ export const suggestionsPageResolver: ResolveFn> = suggestionsDataService: SuggestionTargetDataService = inject(SuggestionTargetDataService), ): Observable> => { return suggestionsDataService.getTargetById(route.params.targetId).pipe( - find((RD) => hasValue(RD.hasFailed) || RD.hasSucceeded), + 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 d97d3d5d702..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 @@ -2,26 +2,20 @@
    - - {{'system-wide-alert-banner.countdown.prefix' | translate }} - - - {{'system-wide-alert-banner.countdown.days' | translate: { - days: countDownDays|async - } }} - - - {{'system-wide-alert-banner.countdown.hours' | translate: { - hours: countDownHours| async - } }} - - - {{'system-wide-alert-banner.countdown.minutes' | translate: { - minutes: countDownMinutes|async - } }} - + + {{ 'system-wide-alert-banner.countdown.prefix' | translate }} + + + {{ 'system-wide-alert-banner.countdown.days' | translate: { days: countDownDays|async } }} + + + {{ 'system-wide-alert-banner.countdown.hours' | translate: { hours: countDownHours| async } }} + + + {{ 'system-wide-alert-banner.countdown.minutes' | translate: { minutes: countDownMinutes|async } }} + - {{(systemWideAlert$ |async)?.message}} +
    -
    +
    \ No newline at end of file diff --git a/src/app/system-wide-alert/alert-form/system-wide-alert-form.component.html b/src/app/system-wide-alert/alert-form/system-wide-alert-form.component.html index 95c7a694489..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 @@ -5,23 +5,22 @@

    {{'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,10 +28,8 @@

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

    - + {{ 'system-wide-alert.form.label.countdownTo.enable' | translate }}
    @@ -40,18 +37,10 @@

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

    - +
    @@ -68,49 +57,38 @@

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

    - {{'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 2616b2accf9..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 @@ -170,7 +170,7 @@ describe('SystemWideAlertFormComponent', () => { }); describe('save', () => { - it('should update the exising alert with the form values and show a success notification on success and navigate back', () => { + it('should update the existing alert with the form values and show a success notification on success and navigate back', () => { spyOn(comp, 'back'); comp.currentAlert = systemWideAlert; @@ -193,7 +193,7 @@ describe('SystemWideAlertFormComponent', () => { expect(requestService.setStaleByHrefSubstring).toHaveBeenCalledWith('systemwidealerts'); expect(comp.back).toHaveBeenCalled(); }); - it('should update the exising alert with the form values and show a success notification on success and not navigate back when false is provided to the save method', () => { + it('should update the existing alert with the form values and show a success notification on success and not navigate back when false is provided to the save method', () => { spyOn(comp, 'back'); comp.currentAlert = systemWideAlert; @@ -216,7 +216,7 @@ describe('SystemWideAlertFormComponent', () => { expect(requestService.setStaleByHrefSubstring).toHaveBeenCalledWith('systemwidealerts'); expect(comp.back).not.toHaveBeenCalled(); }); - it('should update the exising alert with the form values but add an empty countdown date when disabled and show a success notification on success', () => { + it('should update the existing alert with the form values but add an empty countdown date when disabled and show a success notification on success', () => { spyOn(comp, 'back'); comp.currentAlert = systemWideAlert; @@ -239,7 +239,7 @@ describe('SystemWideAlertFormComponent', () => { expect(requestService.setStaleByHrefSubstring).toHaveBeenCalledWith('systemwidealerts'); expect(comp.back).toHaveBeenCalled(); }); - it('should update the exising alert with the form values and show a error notification on error', () => { + it('should update the existing alert with the form values and show a error notification on error', () => { spyOn(comp, 'back'); (systemWideAlertDataService.put as jasmine.Spy).and.returnValue(createFailedRemoteDataObject$()); comp.currentAlert = systemWideAlert; diff --git a/src/app/system-wide-alert/system-wide-alert-routes.ts b/src/app/system-wide-alert/system-wide-alert-routes.ts index 2f3596825bf..a71007b6b3c 100644 --- a/src/app/system-wide-alert/system-wide-alert-routes.ts +++ b/src/app/system-wide-alert/system-wide-alert-routes.ts @@ -1,15 +1,12 @@ -import { - mapToCanActivate, - Route, -} from '@angular/router'; +import { Route } from '@angular/router'; -import { SiteAdministratorGuard } from '../core/data/feature-authorization/feature-authorization-guard/site-administrator.guard'; +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: mapToCanActivate([SiteAdministratorGuard]), + canActivate: [siteAdministratorGuard], component: SystemWideAlertFormComponent, }, 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 3c2b524c1fa..73bd7610a9d 100644 --- a/src/app/system-wide-alert/system-wide-alert.model.ts +++ b/src/app/system-wide-alert/system-wide-alert.model.ts @@ -22,38 +22,38 @@ export class SystemWideAlert implements CacheableObject { */ @excludeFromEquals @autoserialize - type: ResourceType; + type: ResourceType; /** * The identifier for this system-wide alert */ @autoserialize - alertId: string; + alertId: string; /** * The message for this system-wide alert */ @autoserialize - message: string; + message: string; /** * A string representation of the date to which this system-wide alert will count down when active */ @autoserialize - countdownTo: string; + countdownTo: string; /** * Whether the system-wide alert is active */ @autoserialize - active: boolean; + active: boolean; /** * The {@link HALLink}s for this system-wide alert */ @deserialize - _links: { + _links: { self: HALLink, }; } diff --git a/src/app/thumbnail/themed-thumbnail.component.ts b/src/app/thumbnail/themed-thumbnail.component.ts index a9a324aa415..6d14378d3a5 100644 --- a/src/app/thumbnail/themed-thumbnail.component.ts +++ b/src/app/thumbnail/themed-thumbnail.component.ts @@ -9,10 +9,11 @@ import { ThemedComponent } from '../shared/theme-support/themed.component'; import { ThumbnailComponent } from './thumbnail.component'; @Component({ - selector: 'ds-themed-thumbnail', + selector: 'ds-thumbnail', styleUrls: [], templateUrl: '../shared/theme-support/themed.component.html', standalone: true, + imports: [ThumbnailComponent], }) export class ThemedThumbnailComponent extends ThemedComponent { diff --git a/src/app/thumbnail/thumbnail.component.html b/src/app/thumbnail/thumbnail.component.html index 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 78afc420517..10e461112bb 100644 --- a/src/app/thumbnail/thumbnail.component.spec.ts +++ b/src/app/thumbnail/thumbnail.component.spec.ts @@ -96,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(); }); @@ -129,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(); @@ -140,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(); }); @@ -243,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(); @@ -262,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; @@ -357,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 96d3965a4e7..cc583c3998f 100644 --- a/src/app/thumbnail/thumbnail.component.ts +++ b/src/app/thumbnail/thumbnail.component.ts @@ -6,10 +6,7 @@ import { SimpleChanges, } from '@angular/core'; import { TranslateModule } from '@ngx-translate/core'; -import { - BehaviorSubject, - of as observableOf, -} from 'rxjs'; +import { of as observableOf } from 'rxjs'; import { switchMap } from 'rxjs/operators'; import { AuthService } from '../core/auth/auth.service'; @@ -32,7 +29,7 @@ import { VarDirective } from '../shared/utils/var.directive'; * 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, @@ -53,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; @@ -76,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, @@ -129,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; @@ -181,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; } } @@ -191,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-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 8dee58795dc..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 @@ -224,6 +224,38 @@ describe('ReviewersListComponent', () => { })).not.toBeTruthy(); }); }); + + it('should replace the value when a new member is added when multipleReviewers is false', () => { + spyOn(component.selectedReviewersUpdated, 'emit'); + component.multipleReviewers = false; + component.selectedReviewers = [EPersonMock]; + + component.addMemberToGroup(EPersonMock2); + + expect(component.selectedReviewers).toEqual([EPersonMock2]); + expect(component.selectedReviewersUpdated.emit).toHaveBeenCalledWith([EPersonMock2]); + }); + + it('should add the value when a new member is added when multipleReviewers is true', () => { + spyOn(component.selectedReviewersUpdated, 'emit'); + component.multipleReviewers = true; + component.selectedReviewers = [EPersonMock]; + + component.addMemberToGroup(EPersonMock2); + + expect(component.selectedReviewers).toEqual([EPersonMock, EPersonMock2]); + expect(component.selectedReviewersUpdated.emit).toHaveBeenCalledWith([EPersonMock, EPersonMock2]); + }); + + it('should delete the member when present', () => { + spyOn(component.selectedReviewersUpdated, 'emit'); + component.selectedReviewers = [EPersonMock]; + + component.deleteMemberFromGroup(EPersonMock); + + expect(component.selectedReviewers).toEqual([]); + expect(component.selectedReviewersUpdated.emit).toHaveBeenCalledWith([]); + }); }); describe('when a group is selected', () => { @@ -245,37 +277,4 @@ describe('ReviewersListComponent', () => { }); }); - - it('should replace the value when a new member is added when multipleReviewers is false', () => { - spyOn(component.selectedReviewersUpdated, 'emit'); - component.multipleReviewers = false; - component.selectedReviewers = [EPersonMock]; - - component.addMemberToGroup(EPersonMock2); - - expect(component.selectedReviewers).toEqual([EPersonMock2]); - expect(component.selectedReviewersUpdated.emit).toHaveBeenCalledWith([EPersonMock2]); - }); - - it('should add the value when a new member is added when multipleReviewers is true', () => { - spyOn(component.selectedReviewersUpdated, 'emit'); - component.multipleReviewers = true; - component.selectedReviewers = [EPersonMock]; - - component.addMemberToGroup(EPersonMock2); - - expect(component.selectedReviewers).toEqual([EPersonMock, EPersonMock2]); - expect(component.selectedReviewersUpdated.emit).toHaveBeenCalledWith([EPersonMock, EPersonMock2]); - }); - - it('should delete the member when present', () => { - spyOn(component.selectedReviewersUpdated, 'emit'); - component.selectedReviewers = [EPersonMock]; - - component.deleteMemberFromGroup(EPersonMock); - - expect(component.selectedReviewers).toEqual([]); - expect(component.selectedReviewersUpdated.emit).toHaveBeenCalledWith([]); - }); - }); diff --git a/src/app/workflowitems-edit-page/advanced-workflow-action/advanced-workflow-action-select-reviewer/reviewers-list/reviewers-list.component.ts b/src/app/workflowitems-edit-page/advanced-workflow-action/advanced-workflow-action-select-reviewer/reviewers-list/reviewers-list.component.ts index 60ab557432e..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 @@ -26,6 +26,10 @@ import { TranslateModule, TranslateService, } from '@ngx-translate/core'; +import { + Observable, + of as observableOf, +} from 'rxjs'; import { EPersonListActionConfig, @@ -36,10 +40,12 @@ import { PaginatedList } from '../../../../core/data/paginated-list.model'; import { EPersonDataService } from '../../../../core/eperson/eperson-data.service'; import { GroupDataService } from '../../../../core/eperson/group-data.service'; import { EPerson } from '../../../../core/eperson/models/eperson.model'; +import { EpersonDtoModel } from '../../../../core/eperson/models/eperson-dto.model'; import { Group } from '../../../../core/eperson/models/group.model'; import { PaginationService } from '../../../../core/pagination/pagination.service'; import { getFirstSucceededRemoteDataPayload } from '../../../../core/shared/operators'; import { ContextHelpDirective } from '../../../../shared/context-help.directive'; +import { hasValue } from '../../../../shared/empty.util'; import { NotificationsService } from '../../../../shared/notifications/notifications.service'; import { PaginationComponent } from '../../../../shared/pagination/pagination.component'; @@ -75,16 +81,16 @@ enum SubKey { export class ReviewersListComponent extends MembersListComponent implements OnInit, OnChanges, OnDestroy { @Input() - groupId: string | null; + groupId: string | null; @Input() - actionConfig: EPersonListActionConfig; + actionConfig: EPersonListActionConfig; @Input() - multipleReviewers: boolean; + multipleReviewers: boolean; @Output() - selectedReviewersUpdated: EventEmitter = new EventEmitter(); + selectedReviewersUpdated: EventEmitter = new EventEmitter(); selectedReviewers: EPerson[] = []; @@ -101,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: '', @@ -114,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) => { @@ -136,25 +143,37 @@ export class ReviewersListComponent extends MembersListComponent implements OnIn retrieveMembers(page: number): void { this.config.currentPage = page; if (this.groupId === null) { - this.unsubFrom(SubKey.Members); - const paginatedListOfEPersons: PaginatedList = new PaginatedList(); - paginatedListOfEPersons.page = this.selectedReviewers; + const paginatedListOfEPersons: PaginatedList = new PaginatedList(); + paginatedListOfEPersons.page = this.selectedReviewers.map((ePerson: EPerson) => Object.assign(new EpersonDtoModel(), { + eperson: ePerson, + ableToDelete: this.isMemberOfGroup(ePerson), + })); this.ePeopleMembersOfGroup.next(paginatedListOfEPersons); } else { super.retrieveMembers(page); } } + /** + * Checks whether the given {@link possibleMember} is part of the {@link selectedReviewers}. + * + * @param possibleMember The {@link EPerson} that needs to be checked + */ + isMemberOfGroup(possibleMember: EPerson): Observable { + return observableOf(hasValue(this.selectedReviewers.find((reviewer: EPerson) => reviewer.id === possibleMember.id))); + } + /** * Removes the {@link eperson} from the {@link selectedReviewers} * * @param eperson The {@link EPerson} to remove */ deleteMemberFromGroup(eperson: EPerson) { - const index = this.selectedReviewers.indexOf(eperson); + const index = this.selectedReviewers.findIndex((reviewer: EPerson) => reviewer.id === eperson.id); if (index !== -1) { this.selectedReviewers.splice(index, 1); } + this.retrieveMembers(this.config.currentPage); this.selectedReviewersUpdated.emit(this.selectedReviewers); } @@ -169,6 +188,7 @@ export class ReviewersListComponent extends MembersListComponent implements OnIn this.selectedReviewers = []; } this.selectedReviewers.push(eperson); + this.retrieveMembers(this.config.currentPage); this.selectedReviewersUpdated.emit(this.selectedReviewers); } diff --git a/src/app/workflowitems-edit-page/workflow-item-action-page.component.ts b/src/app/workflowitems-edit-page/workflow-item-action-page.component.ts index 6e1a9b3a31f..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,6 +1,7 @@ import { Location } from '@angular/common'; import { Directive, + Input, OnInit, } from '@angular/core'; import { @@ -42,7 +43,9 @@ import { NotificationsService } from '../shared/notifications/notifications.serv standalone: true, }) export abstract class WorkflowItemActionPageDirective implements OnInit { - public type; + + @Input() type: string; + public wfi$: Observable; public item$: Observable; protected previousQueryParameters?: 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 4be1b65741a..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 @@ -8,10 +8,11 @@ import { WorkflowItemDeleteComponent } from './workflow-item-delete.component'; */ @Component({ - selector: 'ds-themed-workflow-item-delete', + selector: 'ds-workflow-item-delete', styleUrls: [], templateUrl: './../../shared/theme-support/themed.component.html', standalone: true, + imports: [WorkflowItemDeleteComponent], }) export class ThemedWorkflowItemDeleteComponent extends ThemedComponent { protected getComponentName(): string { diff --git a/src/app/workflowitems-edit-page/workflow-item-delete/workflow-item-delete.component.ts b/src/app/workflowitems-edit-page/workflow-item-delete/workflow-item-delete.component.ts index 3d5fae9fe65..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 @@ -26,7 +26,7 @@ import { VarDirective } from '../../shared/utils/var.directive'; import { WorkflowItemActionPageDirective } from '../workflow-item-action-page.component'; @Component({ - selector: 'ds-workflow-item-delete', + selector: 'ds-base-workflow-item-delete', templateUrl: '../workflow-item-action-page.component.html', standalone: true, imports: [VarDirective, TranslateModule, CommonModule, ModifyItemOverviewComponent], 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 ecf7c965b64..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 @@ -8,10 +8,11 @@ import { WorkflowItemSendBackComponent } from './workflow-item-send-back.compone */ @Component({ - selector: 'ds-themed-workflow-item-send-back', + selector: 'ds-workflow-item-send-back', styleUrls: [], templateUrl: './../../shared/theme-support/themed.component.html', standalone: true, + imports: [WorkflowItemSendBackComponent], }) export class ThemedWorkflowItemSendBackComponent extends ThemedComponent { protected getComponentName(): string { diff --git a/src/app/workflowitems-edit-page/workflow-item-send-back/workflow-item-send-back.component.ts b/src/app/workflowitems-edit-page/workflow-item-send-back/workflow-item-send-back.component.ts index 797146e1f2b..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 @@ -22,7 +22,7 @@ import { VarDirective } from '../../shared/utils/var.directive'; import { WorkflowItemActionPageDirective } from '../workflow-item-action-page.component'; @Component({ - selector: 'ds-workflow-item-send-back', + selector: 'ds-base-workflow-item-send-back', templateUrl: '../workflow-item-action-page.component.html', standalone: true, imports: [VarDirective, TranslateModule, CommonModule, ModifyItemOverviewComponent], 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 b335c1dbd00..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,25 +1,17 @@ -import { CommonModule } from '@angular/common'; import { Component } from '@angular/core'; -import { TranslateModule } from '@ngx-translate/core'; -import { ModifyItemOverviewComponent } from '../../item-page/edit-item-page/modify-item-overview/modify-item-overview.component'; import { ThemedComponent } from '../../shared/theme-support/themed.component'; import { WorkspaceItemsDeletePageComponent } from './workspaceitems-delete-page.component'; /** - * Themed wrapper for WorkspaceItemsDeletePageComponent + * Themed wrapper for {@link WorkspaceItemsDeletePageComponent} */ - @Component({ - selector: 'ds-themed-workspace-items-delete', + selector: 'ds-workspace-items-delete', styleUrls: [], templateUrl: './../../shared/theme-support/themed.component.html', standalone: true, - imports: [ - ModifyItemOverviewComponent, - TranslateModule, - CommonModule, - ], + imports: [WorkspaceItemsDeletePageComponent], }) export class ThemedWorkspaceItemsDeletePageComponent extends ThemedComponent { protected getComponentName(): string { @@ -27,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 f33a4a67069..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,5 +1,5 @@
    -

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

    +

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

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

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

    - +
    diff --git a/src/themes/dspace/app/header/header.component.ts b/src/themes/dspace/app/header/header.component.ts index 1a8144ed968..19318389231 100644 --- a/src/themes/dspace/app/header/header.component.ts +++ b/src/themes/dspace/app/header/header.component.ts @@ -18,17 +18,16 @@ import { ThemedNavbarComponent } from '../../../../app/navbar/themed-navbar.comp 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'; -import { LangSwitchComponent } from '../../../../app/shared/lang-switch/lang-switch.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, LangSwitchComponent, ContextHelpToggleComponent, ThemedAuthNavMenuComponent, ImpersonateNavbarComponent, ThemedNavbarComponent, TranslateModule, AsyncPipe, NgIf], + imports: [NgbDropdownModule, ThemedLangSwitchComponent, RouterLink, ThemedSearchNavbarComponent, ContextHelpToggleComponent, ThemedAuthNavMenuComponent, ImpersonateNavbarComponent, ThemedNavbarComponent, TranslateModule, AsyncPipe, NgIf], }) export class HeaderComponent extends BaseComponent implements OnInit { public isNavBarCollapsed$: Observable; diff --git a/src/themes/dspace/app/home-page/home-news/home-news.component.html b/src/themes/dspace/app/home-page/home-news/home-news.component.html index ca686a9933f..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 @@ -3,7 +3,7 @@
    -

    DSpace 8

    +

    DSpace 9

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

    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 820d6ceaf65..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 @@ -3,7 +3,7 @@ import { Component } from '@angular/core'; import { HomeNewsComponent as BaseComponent } from '../../../../../app/home-page/home-news/home-news.component'; @Component({ - selector: 'ds-home-news', + selector: 'ds-themed-home-news', styleUrls: ['./home-news.component.scss'], templateUrl: './home-news.component.html', standalone: true, diff --git a/src/themes/dspace/app/navbar/navbar.component.html b/src/themes/dspace/app/navbar/navbar.component.html index f1c8c668477..d828206e7ae 100644 --- a/src/themes/dspace/app/navbar/navbar.component.html +++ b/src/themes/dspace/app/navbar/navbar.component.html @@ -1,5 +1,5 @@ - +