From a8e16eabfe91c01745ebf94a40224f5dbd569c1d Mon Sep 17 00:00:00 2001 From: Danilo Hoffmann Date: Sun, 16 Oct 2022 08:15:14 +0000 Subject: [PATCH 01/39] chore: upgrade github actions --- .github/workflows/automated-upgrade.yml | 6 +++--- .github/workflows/dead-code.yml | 4 ++-- .github/workflows/demo-server-up.yml | 2 +- .github/workflows/development.yml | 20 ++++++++++---------- .github/workflows/docs.yml | 4 ++-- .github/workflows/integration.yml | 24 ++++++++++++------------ .github/workflows/reports.yml | 2 +- .github/workflows/updates.yml | 4 ++-- .github/workflows/windows.yml | 16 ++++++++-------- 9 files changed, 41 insertions(+), 41 deletions(-) diff --git a/.github/workflows/automated-upgrade.yml b/.github/workflows/automated-upgrade.yml index b511c6b8be..03a5eaa369 100644 --- a/.github/workflows/automated-upgrade.yml +++ b/.github/workflows/automated-upgrade.yml @@ -29,18 +29,18 @@ jobs: continue-on-error: true steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 if: github.ref != 'refs/heads/develop' with: ref: ${{ github.event.pull_request.head.sha }} - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 if: github.ref == 'refs/heads/develop' with: ref: develop - name: Use Node.js 16 - uses: actions/setup-node@v2 + uses: actions/setup-node@v3 with: node-version: ${{ env.NODE_VERSION }} cache: npm diff --git a/.github/workflows/dead-code.yml b/.github/workflows/dead-code.yml index 759c54b2f0..82408414f7 100644 --- a/.github/workflows/dead-code.yml +++ b/.github/workflows/dead-code.yml @@ -14,12 +14,12 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 with: fetch-depth: 0 - name: Use Node.js 16 - uses: actions/setup-node@v2 + uses: actions/setup-node@v3 with: node-version: ${{ env.NODE_VERSION }} cache: npm diff --git a/.github/workflows/demo-server-up.yml b/.github/workflows/demo-server-up.yml index 850b0e7edb..fac154b503 100644 --- a/.github/workflows/demo-server-up.yml +++ b/.github/workflows/demo-server-up.yml @@ -28,7 +28,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Publish SSR image to Registry id: ssr uses: elgohr/Publish-Docker-Github-Action@v4 diff --git a/.github/workflows/development.yml b/.github/workflows/development.yml index 624b4e9cde..8b3b3d4539 100644 --- a/.github/workflows/development.yml +++ b/.github/workflows/development.yml @@ -32,9 +32,9 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Use Node.js 16 - uses: actions/setup-node@v2 + uses: actions/setup-node@v3 with: node-version: ${{ env.NODE_VERSION }} cache: npm @@ -79,9 +79,9 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Use Node.js 16 - uses: actions/setup-node@v2 + uses: actions/setup-node@v3 with: node-version: ${{ env.NODE_VERSION }} cache: npm @@ -106,12 +106,12 @@ jobs: if: "!startsWith(github.ref, 'refs/tags/') && github.ref != 'refs/heads/master' && github.ref != 'refs/heads/develop'" steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 with: fetch-depth: 0 - name: Use Node.js 16 - uses: actions/setup-node@v2 + uses: actions/setup-node@v3 with: node-version: ${{ env.NODE_VERSION }} cache: npm @@ -137,9 +137,9 @@ jobs: test: ['normal', 'customization'] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Use Node.js 16 - uses: actions/setup-node@v2 + uses: actions/setup-node@v3 with: node-version: ${{ env.NODE_VERSION }} cache: npm @@ -157,7 +157,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Production Build PWA Image run: docker-compose build pwa @@ -167,7 +167,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Build Nginx Image run: docker-compose build nginx diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index cea065a526..7d1af906a5 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -15,12 +15,12 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 with: fetch-depth: 0 - name: Use Node.js 16 - uses: actions/setup-node@v2 + uses: actions/setup-node@v3 with: node-version: ${{ env.NODE_VERSION }} cache: npm diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index d56d58c9d0..c297aa4571 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -35,9 +35,9 @@ jobs: TESTING: true steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Use Node.js 16 - uses: actions/setup-node@v2 + uses: actions/setup-node@v3 with: node-version: ${{ env.NODE_VERSION }} cache: npm @@ -52,7 +52,7 @@ jobs: run: npm run build:multi - name: Upload Build Output - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: dist path: dist @@ -66,15 +66,15 @@ jobs: test: ['b2c', 'b2b'] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Use Node.js 16 - uses: actions/setup-node@v2 + uses: actions/setup-node@v3 with: node-version: ${{ env.NODE_VERSION }} cache: npm - name: Download Build Output - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v3 with: name: dist path: dist @@ -109,13 +109,13 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Install e2e dependencies run: cd e2e && npm i - name: Download Build Output - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v3 with: name: dist path: dist @@ -141,7 +141,7 @@ jobs: node dist/$THEME/run-standalone & - name: Cypress - uses: cypress-io/github-action@v2 + uses: cypress-io/github-action@v4 with: install: false wait-on: 'http://localhost:4200' @@ -150,14 +150,14 @@ jobs: command: npx ts-node cypress-ci-e2e **/*${{ matrix.test }}*.e2e-spec.ts - name: Upload Screenshots - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 if: failure() with: name: screenshots path: e2e/cypress/screenshots - name: Upload Videos - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 if: failure() with: name: videos @@ -169,7 +169,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Execute shellspec tests run: | docker run --rm -v "${{ github.workspace }}/nginx/docker-entrypoint.d:/src" shellspec/shellspec diff --git a/.github/workflows/reports.yml b/.github/workflows/reports.yml index 34941c0de1..aab1ec0f5c 100644 --- a/.github/workflows/reports.yml +++ b/.github/workflows/reports.yml @@ -24,7 +24,7 @@ jobs: url: https://pwa-gh-review-reports.azurewebsites.net steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Reset .dockerignore run: echo node_modules > .dockerignore diff --git a/.github/workflows/updates.yml b/.github/workflows/updates.yml index 9a9d33ab87..980928c6ff 100644 --- a/.github/workflows/updates.yml +++ b/.github/workflows/updates.yml @@ -18,9 +18,9 @@ jobs: continue-on-error: true steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Use Node.js 16 - uses: actions/setup-node@v2 + uses: actions/setup-node@v3 with: node-version: ${{ env.NODE_VERSION }} cache: npm diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 37e23b7e04..546eb64cef 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -41,9 +41,9 @@ jobs: runs-on: windows-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Use Node.js 16 - uses: actions/setup-node@v2 + uses: actions/setup-node@v3 with: node-version: ${{ env.NODE_VERSION }} cache: npm @@ -61,9 +61,9 @@ jobs: runs-on: windows-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Use Node.js 16 - uses: actions/setup-node@v2 + uses: actions/setup-node@v3 with: node-version: ${{ env.NODE_VERSION }} cache: npm @@ -91,9 +91,9 @@ jobs: runs-on: windows-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Use Node.js 16 - uses: actions/setup-node@v2 + uses: actions/setup-node@v3 with: node-version: ${{ env.NODE_VERSION }} cache: npm @@ -107,9 +107,9 @@ jobs: runs-on: windows-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Use Node.js 16 - uses: actions/setup-node@v2 + uses: actions/setup-node@v3 with: node-version: ${{ env.NODE_VERSION }} cache: npm From a4e6a0297750fd519ee11e080f8249da0c777f1a Mon Sep 17 00:00:00 2001 From: Johannes Metzner Date: Mon, 17 Oct 2022 09:34:27 +0200 Subject: [PATCH 02/39] chore: bump nginx and nginx prometheus exporter --- nginx/Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nginx/Dockerfile b/nginx/Dockerfile index 9c15c1c14e..c0e6fdba46 100644 --- a/nginx/Dockerfile +++ b/nginx/Dockerfile @@ -1,4 +1,4 @@ -FROM nginx:1.20 +FROM nginx:1.22 RUN apt-get update \ && apt-get install --no-install-recommends --no-install-suggests -y apache2-utils libnss3-tools COPY nginx.conf /etc/nginx/nginx.conf @@ -11,7 +11,7 @@ ADD https://github.com/hairyhenderson/gomplate/releases/download/v3.8.0/gomplate RUN chmod 700 /gomplate ADD https://github.com/FiloSottile/mkcert/releases/download/v1.4.4/mkcert-v1.4.4-linux-amd64 /usr/bin/mkcert RUN chmod 700 /usr/bin/mkcert -COPY --from=nginx/nginx-prometheus-exporter:0.10.0 /usr/bin/nginx-prometheus-exporter /nginx-prometheus-exporter +COPY --from=nginx/nginx-prometheus-exporter:0.11.0 /usr/bin/nginx-prometheus-exporter /nginx-prometheus-exporter ENV CACHE=on ENV COMPRESSION=on ENV DEVICE_DETECTION=on From 1cae0d89c5764653722e14a29e9d9a2ef807d19b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Silke=20Gr=C3=BCber?= <57660644+SGrueber@users.noreply.github.com> Date: Fri, 21 Oct 2022 14:32:08 +0200 Subject: [PATCH 03/39] fix: add matrix parameter spgid to promotion REST calls (#1310) fix: add matrix parameter spgid to promotion REST calls to avoid 404 response status --- .../core/services/promotions/promotions.service.spec.ts | 7 ++++--- src/app/core/services/promotions/promotions.service.ts | 4 +++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/app/core/services/promotions/promotions.service.spec.ts b/src/app/core/services/promotions/promotions.service.spec.ts index f82a8b2da7..066ed12789 100644 --- a/src/app/core/services/promotions/promotions.service.spec.ts +++ b/src/app/core/services/promotions/promotions.service.spec.ts @@ -1,6 +1,6 @@ import { TestBed } from '@angular/core/testing'; import { of } from 'rxjs'; -import { instance, mock, verify, when } from 'ts-mockito'; +import { anything, instance, mock, verify, when } from 'ts-mockito'; import { Promotion } from 'ish-core/models/promotion/promotion.model'; import { ApiService } from 'ish-core/services/api/api.service'; @@ -39,10 +39,11 @@ describe('Promotions Service', () => { }); it("should get Promotion data when 'getPromotion' is called", done => { - when(apiServiceMock.get(`promotions/PROMO_UUID`)).thenReturn(of(promotionMockData)); + when(apiServiceMock.get(anything(), anything())).thenReturn(of(promotionMockData)); + promotionsService.getPromotion('PROMO_UUID').subscribe(data => { expect(data.id).toEqual('PROMO_UUID'); - verify(apiServiceMock.get(`promotions/PROMO_UUID`)).once(); + verify(apiServiceMock.get(`promotions/PROMO_UUID`, anything())).once(); done(); }); }); diff --git a/src/app/core/services/promotions/promotions.service.ts b/src/app/core/services/promotions/promotions.service.ts index 40bff51c4d..9a20aa0c1f 100644 --- a/src/app/core/services/promotions/promotions.service.ts +++ b/src/app/core/services/promotions/promotions.service.ts @@ -19,6 +19,8 @@ export class PromotionsService { return throwError(() => new Error('getPromotion() called without a id')); } - return this.apiService.get(`promotions/${id}`); + return this.apiService.get(`promotions/${id}`, { + sendSPGID: true, + }); } } From 025818ca1c890648e92dbc4a503a329d3b1157ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Silke=20Gr=C3=BCber?= <57660644+SGrueber@users.noreply.github.com> Date: Tue, 1 Nov 2022 10:53:18 +0100 Subject: [PATCH 04/39] fix: display login modal after authentication token has expired (#1280, #1311) Closes: #1280 --- src/app/core/guards/login.guard.ts | 1 + .../login/login-modal/login-modal.component.html | 1 + .../components/login/login-modal/login-modal.component.ts | 8 +++++++- 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/app/core/guards/login.guard.ts b/src/app/core/guards/login.guard.ts index 1e6a3d31f7..fcdd2ec073 100644 --- a/src/app/core/guards/login.guard.ts +++ b/src/app/core/guards/login.guard.ts @@ -38,6 +38,7 @@ export class LoginGuard implements CanActivate { const loginModalComponent = this.currentDialog.componentInstance as LoginModalComponent; loginModalComponent.loginMessageKey = route.queryParamMap.get('messageKey'); + loginModalComponent.detectChanges(); // dialog closed loginModalComponent.closeModal.pipe(first()).subscribe(() => { diff --git a/src/app/shared/components/login/login-modal/login-modal.component.html b/src/app/shared/components/login/login-modal/login-modal.component.html index 28116b1a05..5755e4bae4 100644 --- a/src/app/shared/components/login/login-modal/login-modal.component.html +++ b/src/app/shared/components/login/login-modal/login-modal.component.html @@ -7,6 +7,7 @@ [title]="'dialog.close.text' | translate" [attr.aria-label]="'dialog.close.text' | translate" (click)="hide()" + ngbAutofocus > diff --git a/src/app/shared/components/login/login-modal/login-modal.component.ts b/src/app/shared/components/login/login-modal/login-modal.component.ts index a0030c2e1a..0f9cd4bb53 100644 --- a/src/app/shared/components/login/login-modal/login-modal.component.ts +++ b/src/app/shared/components/login/login-modal/login-modal.component.ts @@ -1,4 +1,4 @@ -import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, Output } from '@angular/core'; @Component({ selector: 'ish-login-modal', @@ -9,6 +9,12 @@ export class LoginModalComponent { @Input() loginMessageKey: string; @Output() closeModal = new EventEmitter(); + constructor(private cdRef: ChangeDetectorRef) {} + + detectChanges() { + this.cdRef.detectChanges(); + } + hide() { this.closeModal.emit(); } From d4db9d62ab14fc6b385cc0dac00fd8e764fb765e Mon Sep 17 00:00:00 2001 From: Stefan Hauke Date: Tue, 1 Nov 2022 14:30:59 +0100 Subject: [PATCH 05/39] docs: extend PurgeCSS integration documentation (#1313) * fix "check sentence newline" script to accept "i.e." and "e.g." as being not sentence ends --- docs/check-sentence-newline.js | 2 +- docs/concepts/styling-behavior.md | 5 ++++ docs/guides/customizations.md | 13 +++++++---- docs/guides/optimizations.md | 38 ++++++++++++++++++++++++++++++- 4 files changed, 52 insertions(+), 6 deletions(-) diff --git a/docs/check-sentence-newline.js b/docs/check-sentence-newline.js index 20d9b62e6f..110271a3bd 100644 --- a/docs/check-sentence-newline.js +++ b/docs/check-sentence-newline.js @@ -15,7 +15,7 @@ files.forEach(file => { if (/^(>|#|\||\s*-|\s*[0-9]+\.)/.test(line)) { return line; } else { - return line.replace(/((?!i\.e)\.) ([A-Z0-9])/g, '$1\n$2'); + return line.replace(/((? **Note:** You should @@ -164,6 +164,7 @@ The lookup starts with the file `style.scss` in the theme specific folder. > - not delete the standard theme folders to prevent merge conflicts when migrating the PWA (changes in standard files but deleted in your project). When styling is done on component level, all styling is encapsulated to exactly this component (default behavior). +On component level, theme-specific overrides for `.scss` files work as expected. You can re-use variables from the global styling on component level by importing only the styling file that defines the theme variables, e.g. @@ -173,10 +174,14 @@ You can re-use variables from the global styling on component level by importing > **Note:** Be aware that Visual Studio Code will not resolve all import references correctly but it works in the build PWA version anyways. +> **Note:** For bundled styles optimization PurgeCSS is used. Please read [the additional documentation](./optimizations.md#purgecss) regarding the usage and configuration of PurgeCSS in the Intershop PWA. + +### Static Assets + To add static assets (images, favicon, manifest file), create a theme specific folder in `src/assets/themes/` and adjust the theme specific references in the `*.scss` files accordingly. -The `index.html` does not support the theme specific overrides, see [Theme Specific Overrides](../guides/customizations.md#theme-specific-overrides). -Therefore, any theme specific references have to be changed directly in this file. +The `index.html` does not support theme specific overrides, see [Theme Specific Overrides](../guides/customizations.md#theme-specific-overrides). +For this file, any theme specific differences are handled via [theme.service.ts](../../src/app/core/utils/theme/theme.service.ts). ### Dependencies diff --git a/docs/guides/optimizations.md b/docs/guides/optimizations.md index 41cdd20924..5a02a56393 100644 --- a/docs/guides/optimizations.md +++ b/docs/guides/optimizations.md @@ -27,7 +27,43 @@ If the PWA is built using `production` configuration. (Either by building with ` - Webpack [SplitChunksPlugin](https://webpack.js.org/plugins/split-chunks-plugin/) is instructed to produce only `main`, `vendor`, `polyfills` and one `common` bundle for the code for optimized compression and download of the application. - All `data-testing` attributes are removed from the HTML templates to reduce output. - [PurgeCSS](https://purgecss.com) is used to remove unused CSS classes from the CSS output. - [Configuration](https://purgecss.com/configuration.html), especially [safelisting](https://purgecss.com/safelisting.html) certain classes, can be done on the plugin configuration or directly in your CSS with a special comment. + +## PurgeCSS + +> PurgeCSS is a tool to remove unused CSS. It can be part of your development workflow. When you are building a website, you might decide to use a CSS framework like TailwindCSS, Bootstrap, MaterializeCSS, Foundation, etc... But you will only use a small set of the framework, and a lot of unused CSS styles will be included. + +> This is where PurgeCSS comes into play. PurgeCSS analyzes your content and your CSS files. Then it matches the selectors used in your files with the one in your content files. It removes unused selectors from your CSS, resulting in smaller CSS files. + +While the described function for deleting unused CSS styles is very helpful, the mechanism for determining which styles are used is not without problems. +PurgeCSS can only analyze the strings in the actual source code of the project for used styles. + +So, styles that get added to the rendered HTML by third-party libraries (e.g. Bootstrap, Swiper) would not be found. +The same applies for styles used in server-loaded content (e.g. CMS, product descriptions). +Also style selectors that are dynamically generated would not be found. + +### Safelisting + +To solve this problem PurgeCSS provides different [options for safelisting](https://purgecss.com/safelisting.html) specific styles. +This can either be done in the plugin configuration or directly in your SCSS/CSS files with special comments. + +The PurgeCSS plugin configuration can be found in the project's [`webpack.custom.ts`](https://github.com/intershop/intershop-pwa/blob/3.1.0/templates/webpack/webpack.custom.ts#L231-L246). +This method is used and recommended to include required styles of the third-party libraries used, which would otherwise be purged. +For the different [configuration options](https://purgecss.com/configuration.html), refer to the PurgeCSS documentation. + +To protect styles defined in the Intershop PWA project source code, Intershop recommends safelisting them directly in your SCSS/CSS with [special comments](https://purgecss.com/safelisting.html#in-the-css-directly). +To include nested SCSS definitions, use `/* purgecss start ignore */` and `/* purgecss end ignore */`. + +### Development + +When using the standard way of developing the PWA with `ng s`, PurgeCSS is not activated and styling should work as expected. +This way missing styling issues because of PurgeCSS often first show up in deployed environments. +To test or develop with enabled PurgeCSS, the development server needs to be started with `ng s -c=b2b,production` (or your desired theme instead of `b2b`). + +In this startup process the following line can be read, indicating the usage of PurgeCSS similar to the deployed builds: + +``` +serve@b2b,production: setting up purgecss CSS minification +``` # Further References From 9e77fbad0c40c7a7b1958c1d86c35bf7bcc03923 Mon Sep 17 00:00:00 2001 From: Michal Kechner Date: Thu, 3 Nov 2022 15:31:03 +0100 Subject: [PATCH 06/39] chore: fix commit linting (#1309) * chore: re-enable commitlint experience since `commitlint-config-cz` no longer worked * chore: remove no longer working `commitlint-config-cz` integration * chore: switch to husky recommended commitlint command (see https://typicode.github.io/husky/#/?id=husky_git_params-ie-commitlint-) * chore: commitizen is not actually needed as dependency, it needs to be installed globally (The repository just needs to be Commitizen friendly https://github.com/commitizen/cz-cli#making-your-repo-commitizen-friendly) Co-authored-by: Stefan Hauke --- .husky/commit-msg | 2 +- package-lock.json | 640 ++++------------------------------------------ package.json | 5 +- 3 files changed, 52 insertions(+), 595 deletions(-) diff --git a/.husky/commit-msg b/.husky/commit-msg index 0bd658f496..5a8500090b 100755 --- a/.husky/commit-msg +++ b/.husky/commit-msg @@ -1,4 +1,4 @@ #!/bin/sh . "$(dirname "$0")/_/husky.sh" -npx --no-install commitlint --edit "$1" +npx --no -- commitlint --edit $1 diff --git a/package-lock.json b/package-lock.json index 30aaba2c26..f31b0a03fa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -71,6 +71,7 @@ "@angular/compiler-cli": "14.2.5", "@angular/language-service": "14.2.5", "@commitlint/cli": "^17.1.2", + "@commitlint/config-conventional": "^17.1.0", "@compodoc/compodoc": "^1.1.19", "@cspell/dict-de-de": "1.1.32", "@cspell/dict-fr-fr": "^2.1.1", @@ -90,8 +91,6 @@ "@typescript-eslint/eslint-plugin": "^5.39.0", "@typescript-eslint/parser": "^5.39.0", "comment-json": "^4.2.3", - "commitizen": "^4.2.5", - "commitlint-config-cz": "^0.13.3", "conventional-changelog-cli": "2.2.2", "cspell": "^6.12.0", "cz-customizable": "^7.0.0", @@ -2631,6 +2630,32 @@ "node": ">=v14" } }, + "node_modules/@commitlint/config-conventional": { + "version": "17.1.0", + "resolved": "https://registry.npmjs.org/@commitlint/config-conventional/-/config-conventional-17.1.0.tgz", + "integrity": "sha512-WU2p0c9/jLi8k2q2YrDV96Y8XVswQOceIQ/wyJvQxawJSCasLdRB3kUIYdNjOCJsxkpoUlV/b90ZPxp1MYZDiA==", + "dev": true, + "dependencies": { + "conventional-changelog-conventionalcommits": "^5.0.0" + }, + "engines": { + "node": ">=v14" + } + }, + "node_modules/@commitlint/config-conventional/node_modules/conventional-changelog-conventionalcommits": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/conventional-changelog-conventionalcommits/-/conventional-changelog-conventionalcommits-5.0.0.tgz", + "integrity": "sha512-lCDbA+ZqVFQGUj7h9QBKoIpLhl8iihkO0nCTyRNzuXtcd7ubODpYB04IFy31JloiJgG0Uovu8ot8oxRzn7Nwtw==", + "dev": true, + "dependencies": { + "compare-func": "^2.0.0", + "lodash": "^4.17.15", + "q": "^1.5.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@commitlint/config-validator": { "version": "17.1.0", "resolved": "https://registry.npmjs.org/@commitlint/config-validator/-/config-validator-17.1.0.tgz", @@ -7804,15 +7829,6 @@ "node": ">=8" } }, - "node_modules/app-root-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/app-root-path/-/app-root-path-3.0.0.tgz", - "integrity": "sha512-qMcx+Gy2UZynHjOHOIXPNvpf+9cjvk3cWrBBK7zg4gH9+clobJRb9NGzcT7mQTcV/6Gm/1WelUtqxVXnNlrwcw==", - "dev": true, - "engines": { - "node": ">= 6.0.0" - } - }, "node_modules/aproba": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", @@ -9112,15 +9128,6 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, - "node_modules/cachedir": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/cachedir/-/cachedir-2.3.0.tgz", - "integrity": "sha512-A+Fezp4zxnit6FanDmv9EqXNAi3vt9DWp51/71UEhXukb7QUuvtv9344h91dyAxuTLoSYJFU299qzR3tzwPAhw==", - "dev": true, - "engines": { - "node": ">=6" - } - }, "node_modules/call-bind": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", @@ -9727,103 +9734,6 @@ "node": ">= 12.0.0" } }, - "node_modules/commitizen": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/commitizen/-/commitizen-4.2.5.tgz", - "integrity": "sha512-9sXju8Qrz1B4Tw7kC5KhnvwYQN88qs2zbiB8oyMsnXZyJ24PPGiNM3nHr73d32dnE3i8VJEXddBFIbOgYSEXtQ==", - "dev": true, - "dependencies": { - "cachedir": "2.3.0", - "cz-conventional-changelog": "3.3.0", - "dedent": "0.7.0", - "detect-indent": "6.1.0", - "find-node-modules": "^2.1.2", - "find-root": "1.1.0", - "fs-extra": "9.1.0", - "glob": "7.2.3", - "inquirer": "8.2.4", - "is-utf8": "^0.2.1", - "lodash": "4.17.21", - "minimist": "1.2.6", - "strip-bom": "4.0.0", - "strip-json-comments": "3.1.1" - }, - "bin": { - "commitizen": "bin/commitizen", - "cz": "bin/git-cz", - "git-cz": "bin/git-cz" - }, - "engines": { - "node": ">= 12" - } - }, - "node_modules/commitizen/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/commitizen/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/commitizen/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "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/commitizen/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/commitlint-config-cz": { - "version": "0.13.3", - "resolved": "https://registry.npmjs.org/commitlint-config-cz/-/commitlint-config-cz-0.13.3.tgz", - "integrity": "sha512-6LmCvGiFDTVSmLF0mzVVp1etMM8lAqLmPRlU7Oml1J8J9oOLadf+2g4uMTchdxOvvYLgll99SESFUHgmc6oATA==", - "dev": true, - "dependencies": { - "app-root-path": "~3.0.0", - "lodash.clonedeep": "~4.5.0" - } - }, "node_modules/common-tags": { "version": "1.8.2", "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz", @@ -10276,12 +10186,6 @@ "semver": "bin/semver.js" } }, - "node_modules/conventional-commit-types": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/conventional-commit-types/-/conventional-commit-types-3.0.0.tgz", - "integrity": "sha512-SmmCYnOniSsAa9GqWOeLqc179lfr5TRu5b4QFDkbsrJ5TZjPJx85wtOr3zn+1dbeNiXDKGPbZ72IKbPhLXh/Lg==", - "dev": true - }, "node_modules/conventional-commits-filter": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/conventional-commits-filter/-/conventional-commits-filter-2.0.7.tgz", @@ -11076,26 +10980,6 @@ "integrity": "sha512-yi1x3EAWKjQTreYWeSd98431AV+IEE0qoDyOoaHJ7KJ21gv6HtBXHVLX74opVSGqcR8/AbjJBHAHpcOy2bj5Gg==", "dev": true }, - "node_modules/cz-conventional-changelog": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/cz-conventional-changelog/-/cz-conventional-changelog-3.3.0.tgz", - "integrity": "sha512-U466fIzU5U22eES5lTNiNbZ+d8dfcHcssH4o7QsdWaCcRs/feIPCxKYSWkYBNs5mny7MvEfwpTLWjvbm94hecw==", - "dev": true, - "dependencies": { - "chalk": "^2.4.1", - "commitizen": "^4.0.3", - "conventional-commit-types": "^3.0.0", - "lodash.map": "^4.5.1", - "longest": "^2.0.1", - "word-wrap": "^1.0.3" - }, - "engines": { - "node": ">= 10" - }, - "optionalDependencies": { - "@commitlint/load": ">6.1.1" - } - }, "node_modules/cz-customizable": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/cz-customizable/-/cz-customizable-7.0.0.tgz", @@ -11677,24 +11561,6 @@ "npm": "1.2.8000 || >= 1.4.16" } }, - "node_modules/detect-file": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz", - "integrity": "sha512-DtCOLG98P007x7wiiOmfI0fi3eIKyWiLTGJ2MDnVi/E04lWGbf+JzrRHMm0rgIIZJGtHpKpbVgLWHrv8xXpc3Q==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/detect-indent": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz", - "integrity": "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -13711,18 +13577,6 @@ "node": ">= 0.8.0" } }, - "node_modules/expand-tilde": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", - "integrity": "sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw==", - "dev": true, - "dependencies": { - "homedir-polyfill": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/expect": { "version": "28.1.3", "resolved": "https://registry.npmjs.org/expect/-/expect-28.1.3.tgz", @@ -14188,22 +14042,6 @@ "node": ">= 0.12" } }, - "node_modules/find-node-modules": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/find-node-modules/-/find-node-modules-2.1.3.tgz", - "integrity": "sha512-UC2I2+nx1ZuOBclWVNdcnbDR5dlrOdVb7xNjmT/lHE+LsgztWks3dG7boJ37yTS/venXw84B/mAW9uHVoC5QRg==", - "dev": true, - "dependencies": { - "findup-sync": "^4.0.0", - "merge": "^2.1.1" - } - }, - "node_modules/find-root": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", - "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", - "dev": true - }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -14229,21 +14067,6 @@ "node": ">=0.8.22" } }, - "node_modules/findup-sync": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-4.0.0.tgz", - "integrity": "sha512-6jvvn/12IC4quLBL1KNokxC7wWTvYncaVUYSoxWw7YykPLuRrnv4qdHcSOywOI5RpkOVGeQRtWM8/q+G6W6qfQ==", - "dev": true, - "dependencies": { - "detect-file": "^1.0.0", - "is-glob": "^4.0.0", - "micromatch": "^4.0.2", - "resolve-dir": "^1.0.1" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/flat-cache": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", @@ -14827,54 +14650,6 @@ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", "dev": true }, - "node_modules/global-modules": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz", - "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==", - "dev": true, - "dependencies": { - "global-prefix": "^1.0.1", - "is-windows": "^1.0.1", - "resolve-dir": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/global-prefix": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz", - "integrity": "sha512-5lsx1NUDHtSjfg0eHlmYvZKv8/nVqX4ckFbM+FrGcQ+04KWcWFo9P5MxPZYSzUvyzmdTbI7Eix8Q4IbELDqzKg==", - "dev": true, - "dependencies": { - "expand-tilde": "^2.0.2", - "homedir-polyfill": "^1.0.1", - "ini": "^1.3.4", - "is-windows": "^1.0.1", - "which": "^1.2.14" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/global-prefix/node_modules/ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "dev": true - }, - "node_modules/global-prefix/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/globals": { "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", @@ -15097,18 +14872,6 @@ "integrity": "sha512-7kIufnBqdsBGcSZLPJwqHT3yhk1QTsSlFsVD3kx5ixH/AlgBs9yM1q6DPhXZ8f8gtdqgh7N7/5btRLpQsS2gHw==", "dev": true }, - "node_modules/homedir-polyfill": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", - "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==", - "dev": true, - "dependencies": { - "parse-passwd": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/hosted-git-info": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", @@ -16098,12 +15861,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-utf8": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", - "integrity": "sha512-rMYPYvCzsXywIsldgLaSoPlw5PfoB/ssr7hY4pLfcodrA5M/eArza1a9VmTiNIBNMjOGr1Ow9mTyU2o69U6U9Q==", - "dev": true - }, "node_modules/is-weakref": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", @@ -16122,15 +15879,6 @@ "integrity": "sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA==", "dev": true }, - "node_modules/is-windows": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", - "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/is-wsl": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", @@ -19696,12 +19444,6 @@ "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==" }, - "node_modules/lodash.clonedeep": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", - "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==", - "dev": true - }, "node_modules/lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", @@ -19720,12 +19462,6 @@ "integrity": "sha512-fPMfXjGQEV9Xsq/8MTSgUf255gawYRbjwMyDbcvDhXgV7enSZA0hynz6vMPnpAb5iONEzBHBPsT+0zes5Z301g==", "dev": true }, - "node_modules/lodash.map": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.map/-/lodash.map-4.6.0.tgz", - "integrity": "sha512-worNHGKLDetmcEYDvh2stPCrrQRkP20E4l0iIS7F8EvzMqBBi7ltvFN5m1HvTf1P7Jk1txKhvFcmYsCr8O2F1Q==", - "dev": true - }, "node_modules/lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -19942,15 +19678,6 @@ "integrity": "sha512-WpG9CcFAOjz/FtNht+QJeGpvVl/cdR6P0z6OcXSkr8wFJOsV2GRj2j10JLfjuA4aYkcKCNIEqRGCyTife9R8/g==", "dev": true }, - "node_modules/longest": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/longest/-/longest-2.0.1.tgz", - "integrity": "sha512-Ajzxb8CM6WAnFjgiloPsI3bF+WCxcvhdIG3KNA2KN962+tdBsHcuQ4k4qX/EcS/2CRkcc0iAkR956Nib6aXU/Q==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/lru-cache": { "version": "7.14.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.14.0.tgz", @@ -20315,12 +20042,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/merge": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/merge/-/merge-2.1.1.tgz", - "integrity": "sha512-jz+Cfrg9GWOZbQAnDQ4hlVnQky+341Yk5ru8bZSe6sIDTCIg8n9i/u7hSQGSVOF3C7lH6mGtqjkiT9G4wFLL0w==", - "dev": true - }, "node_modules/merge-descriptors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", @@ -22027,15 +21748,6 @@ "node": ">= 0.10" } }, - "node_modules/parse-passwd": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", - "integrity": "sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/parse5": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz", @@ -24328,19 +24040,6 @@ "node": ">=8" } }, - "node_modules/resolve-dir": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz", - "integrity": "sha512-R7uiTjECzvOsWSfdM0QKFNBVFcK27aHOUwdvK53BcW8zqnGdYp0Fbj82cy54+2A4P2tFM22J5kRfe1R+lM/1yg==", - "dev": true, - "dependencies": { - "expand-tilde": "^2.0.0", - "global-modules": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/resolve-from": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", @@ -30715,6 +30414,28 @@ "yargs": "^17.0.0" } }, + "@commitlint/config-conventional": { + "version": "17.1.0", + "resolved": "https://registry.npmjs.org/@commitlint/config-conventional/-/config-conventional-17.1.0.tgz", + "integrity": "sha512-WU2p0c9/jLi8k2q2YrDV96Y8XVswQOceIQ/wyJvQxawJSCasLdRB3kUIYdNjOCJsxkpoUlV/b90ZPxp1MYZDiA==", + "dev": true, + "requires": { + "conventional-changelog-conventionalcommits": "^5.0.0" + }, + "dependencies": { + "conventional-changelog-conventionalcommits": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/conventional-changelog-conventionalcommits/-/conventional-changelog-conventionalcommits-5.0.0.tgz", + "integrity": "sha512-lCDbA+ZqVFQGUj7h9QBKoIpLhl8iihkO0nCTyRNzuXtcd7ubODpYB04IFy31JloiJgG0Uovu8ot8oxRzn7Nwtw==", + "dev": true, + "requires": { + "compare-func": "^2.0.0", + "lodash": "^4.17.15", + "q": "^1.5.1" + } + } + } + }, "@commitlint/config-validator": { "version": "17.1.0", "resolved": "https://registry.npmjs.org/@commitlint/config-validator/-/config-validator-17.1.0.tgz", @@ -34708,12 +34429,6 @@ "integrity": "sha512-FCAJojipPn0bXjuEpjOOOMN8FZDkxfWWp4JGN9mifU2IhxvKyXZYqpzPHdnTSUpmPDy+tsslB6Z1g+Vg6nVbYA==", "dev": true }, - "app-root-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/app-root-path/-/app-root-path-3.0.0.tgz", - "integrity": "sha512-qMcx+Gy2UZynHjOHOIXPNvpf+9cjvk3cWrBBK7zg4gH9+clobJRb9NGzcT7mQTcV/6Gm/1WelUtqxVXnNlrwcw==", - "dev": true - }, "aproba": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", @@ -35738,12 +35453,6 @@ "unique-filename": "^1.1.1" } }, - "cachedir": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/cachedir/-/cachedir-2.3.0.tgz", - "integrity": "sha512-A+Fezp4zxnit6FanDmv9EqXNAi3vt9DWp51/71UEhXukb7QUuvtv9344h91dyAxuTLoSYJFU299qzR3tzwPAhw==", - "dev": true - }, "call-bind": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", @@ -36192,85 +35901,6 @@ "integrity": "sha512-B52sN2VNghyq5ofvUsqZjmk6YkihBX5vMSChmSK9v4ShjKf3Vk5Xcmgpw4o+iIgtrnM/u5FiMpz9VKb8lpBveA==", "dev": true }, - "commitizen": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/commitizen/-/commitizen-4.2.5.tgz", - "integrity": "sha512-9sXju8Qrz1B4Tw7kC5KhnvwYQN88qs2zbiB8oyMsnXZyJ24PPGiNM3nHr73d32dnE3i8VJEXddBFIbOgYSEXtQ==", - "dev": true, - "requires": { - "cachedir": "2.3.0", - "cz-conventional-changelog": "3.3.0", - "dedent": "0.7.0", - "detect-indent": "6.1.0", - "find-node-modules": "^2.1.2", - "find-root": "1.1.0", - "fs-extra": "9.1.0", - "glob": "7.2.3", - "inquirer": "8.2.4", - "is-utf8": "^0.2.1", - "lodash": "4.17.21", - "minimist": "1.2.6", - "strip-bom": "4.0.0", - "strip-json-comments": "3.1.1" - }, - "dependencies": { - "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, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "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, - "requires": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - } - }, - "glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, - "requires": { - "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" - } - }, - "minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - } - } - }, - "commitlint-config-cz": { - "version": "0.13.3", - "resolved": "https://registry.npmjs.org/commitlint-config-cz/-/commitlint-config-cz-0.13.3.tgz", - "integrity": "sha512-6LmCvGiFDTVSmLF0mzVVp1etMM8lAqLmPRlU7Oml1J8J9oOLadf+2g4uMTchdxOvvYLgll99SESFUHgmc6oATA==", - "dev": true, - "requires": { - "app-root-path": "~3.0.0", - "lodash.clonedeep": "~4.5.0" - } - }, "common-tags": { "version": "1.8.2", "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz", @@ -36638,12 +36268,6 @@ } } }, - "conventional-commit-types": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/conventional-commit-types/-/conventional-commit-types-3.0.0.tgz", - "integrity": "sha512-SmmCYnOniSsAa9GqWOeLqc179lfr5TRu5b4QFDkbsrJ5TZjPJx85wtOr3zn+1dbeNiXDKGPbZ72IKbPhLXh/Lg==", - "dev": true - }, "conventional-commits-filter": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/conventional-commits-filter/-/conventional-commits-filter-2.0.7.tgz", @@ -37217,21 +36841,6 @@ "integrity": "sha512-yi1x3EAWKjQTreYWeSd98431AV+IEE0qoDyOoaHJ7KJ21gv6HtBXHVLX74opVSGqcR8/AbjJBHAHpcOy2bj5Gg==", "dev": true }, - "cz-conventional-changelog": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/cz-conventional-changelog/-/cz-conventional-changelog-3.3.0.tgz", - "integrity": "sha512-U466fIzU5U22eES5lTNiNbZ+d8dfcHcssH4o7QsdWaCcRs/feIPCxKYSWkYBNs5mny7MvEfwpTLWjvbm94hecw==", - "dev": true, - "requires": { - "@commitlint/load": ">6.1.1", - "chalk": "^2.4.1", - "commitizen": "^4.0.3", - "conventional-commit-types": "^3.0.0", - "lodash.map": "^4.5.1", - "longest": "^2.0.1", - "word-wrap": "^1.0.3" - } - }, "cz-customizable": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/cz-customizable/-/cz-customizable-7.0.0.tgz", @@ -37672,18 +37281,6 @@ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==" }, - "detect-file": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz", - "integrity": "sha512-DtCOLG98P007x7wiiOmfI0fi3eIKyWiLTGJ2MDnVi/E04lWGbf+JzrRHMm0rgIIZJGtHpKpbVgLWHrv8xXpc3Q==", - "dev": true - }, - "detect-indent": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz", - "integrity": "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==", - "dev": true - }, "detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -39103,15 +38700,6 @@ "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", "dev": true }, - "expand-tilde": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", - "integrity": "sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw==", - "dev": true, - "requires": { - "homedir-polyfill": "^1.0.1" - } - }, "expect": { "version": "28.1.3", "resolved": "https://registry.npmjs.org/expect/-/expect-28.1.3.tgz", @@ -39498,22 +39086,6 @@ "user-home": "^2.0.0" } }, - "find-node-modules": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/find-node-modules/-/find-node-modules-2.1.3.tgz", - "integrity": "sha512-UC2I2+nx1ZuOBclWVNdcnbDR5dlrOdVb7xNjmT/lHE+LsgztWks3dG7boJ37yTS/venXw84B/mAW9uHVoC5QRg==", - "dev": true, - "requires": { - "findup-sync": "^4.0.0", - "merge": "^2.1.1" - } - }, - "find-root": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", - "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", - "dev": true - }, "find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -39530,18 +39102,6 @@ "integrity": "sha512-lg/Moejf4qXovVutL0Lz4IsaPoNYMuxt4PA0nGqFxnJ1CTTGGlEO2wKgoDpwknhvZ8k4Q2F+eesgkLbG2Mxfog==", "dev": true }, - "findup-sync": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-4.0.0.tgz", - "integrity": "sha512-6jvvn/12IC4quLBL1KNokxC7wWTvYncaVUYSoxWw7YykPLuRrnv4qdHcSOywOI5RpkOVGeQRtWM8/q+G6W6qfQ==", - "dev": true, - "requires": { - "detect-file": "^1.0.0", - "is-glob": "^4.0.0", - "micromatch": "^4.0.2", - "resolve-dir": "^1.0.1" - } - }, "flat-cache": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", @@ -39992,47 +39552,6 @@ } } }, - "global-modules": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz", - "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==", - "dev": true, - "requires": { - "global-prefix": "^1.0.1", - "is-windows": "^1.0.1", - "resolve-dir": "^1.0.0" - } - }, - "global-prefix": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz", - "integrity": "sha512-5lsx1NUDHtSjfg0eHlmYvZKv8/nVqX4ckFbM+FrGcQ+04KWcWFo9P5MxPZYSzUvyzmdTbI7Eix8Q4IbELDqzKg==", - "dev": true, - "requires": { - "expand-tilde": "^2.0.2", - "homedir-polyfill": "^1.0.1", - "ini": "^1.3.4", - "is-windows": "^1.0.1", - "which": "^1.2.14" - }, - "dependencies": { - "ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "dev": true - }, - "which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - } - } - }, "globals": { "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", @@ -40200,15 +39719,6 @@ "integrity": "sha512-7kIufnBqdsBGcSZLPJwqHT3yhk1QTsSlFsVD3kx5ixH/AlgBs9yM1q6DPhXZ8f8gtdqgh7N7/5btRLpQsS2gHw==", "dev": true }, - "homedir-polyfill": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", - "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==", - "dev": true, - "requires": { - "parse-passwd": "^1.0.0" - } - }, "hosted-git-info": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", @@ -40899,12 +40409,6 @@ "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==" }, - "is-utf8": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", - "integrity": "sha512-rMYPYvCzsXywIsldgLaSoPlw5PfoB/ssr7hY4pLfcodrA5M/eArza1a9VmTiNIBNMjOGr1Ow9mTyU2o69U6U9Q==", - "dev": true - }, "is-weakref": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", @@ -40920,12 +40424,6 @@ "integrity": "sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA==", "dev": true }, - "is-windows": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", - "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", - "dev": true - }, "is-wsl": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", @@ -43607,12 +43105,6 @@ "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==" }, - "lodash.clonedeep": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", - "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==", - "dev": true - }, "lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", @@ -43631,12 +43123,6 @@ "integrity": "sha512-fPMfXjGQEV9Xsq/8MTSgUf255gawYRbjwMyDbcvDhXgV7enSZA0hynz6vMPnpAb5iONEzBHBPsT+0zes5Z301g==", "dev": true }, - "lodash.map": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.map/-/lodash.map-4.6.0.tgz", - "integrity": "sha512-worNHGKLDetmcEYDvh2stPCrrQRkP20E4l0iIS7F8EvzMqBBi7ltvFN5m1HvTf1P7Jk1txKhvFcmYsCr8O2F1Q==", - "dev": true - }, "lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -43793,12 +43279,6 @@ "integrity": "sha512-WpG9CcFAOjz/FtNht+QJeGpvVl/cdR6P0z6OcXSkr8wFJOsV2GRj2j10JLfjuA4aYkcKCNIEqRGCyTife9R8/g==", "dev": true }, - "longest": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/longest/-/longest-2.0.1.tgz", - "integrity": "sha512-Ajzxb8CM6WAnFjgiloPsI3bF+WCxcvhdIG3KNA2KN962+tdBsHcuQ4k4qX/EcS/2CRkcc0iAkR956Nib6aXU/Q==", - "dev": true - }, "lru-cache": { "version": "7.14.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.14.0.tgz", @@ -44077,12 +43557,6 @@ } } }, - "merge": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/merge/-/merge-2.1.1.tgz", - "integrity": "sha512-jz+Cfrg9GWOZbQAnDQ4hlVnQky+341Yk5ru8bZSe6sIDTCIg8n9i/u7hSQGSVOF3C7lH6mGtqjkiT9G4wFLL0w==", - "dev": true - }, "merge-descriptors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", @@ -45386,12 +44860,6 @@ "integrity": "sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==", "dev": true }, - "parse-passwd": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", - "integrity": "sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==", - "dev": true - }, "parse5": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz", @@ -47019,16 +46487,6 @@ "resolve-from": "^5.0.0" } }, - "resolve-dir": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz", - "integrity": "sha512-R7uiTjECzvOsWSfdM0QKFNBVFcK27aHOUwdvK53BcW8zqnGdYp0Fbj82cy54+2A4P2tFM22J5kRfe1R+lM/1yg==", - "dev": true, - "requires": { - "expand-tilde": "^2.0.0", - "global-modules": "^1.0.0" - } - }, "resolve-from": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", diff --git a/package.json b/package.json index e1abffabe7..93cc3edfe2 100644 --- a/package.json +++ b/package.json @@ -119,6 +119,7 @@ "@angular/compiler-cli": "14.2.5", "@angular/language-service": "14.2.5", "@commitlint/cli": "^17.1.2", + "@commitlint/config-conventional": "^17.1.0", "@compodoc/compodoc": "^1.1.19", "@cspell/dict-de-de": "1.1.32", "@cspell/dict-fr-fr": "^2.1.1", @@ -138,8 +139,6 @@ "@typescript-eslint/eslint-plugin": "^5.39.0", "@typescript-eslint/parser": "^5.39.0", "comment-json": "^4.2.3", - "commitizen": "^4.2.5", - "commitlint-config-cz": "^0.13.3", "conventional-changelog-cli": "2.2.2", "cspell": "^6.12.0", "cz-customizable": "^7.0.0", @@ -221,7 +220,7 @@ }, "commitlint": { "extends": [ - "cz" + "@commitlint/config-conventional" ] }, "config": { From 33be4cc9d4f45974ffff96a9a21d052a623995d1 Mon Sep 17 00:00:00 2001 From: Stefan Hauke Date: Fri, 4 Nov 2022 15:25:00 +0100 Subject: [PATCH 07/39] fix: 'npm install' not working with PWA release zip file (#1314) * lazy components schematic needs git and a git repository but checked only for the git command not for being in a git repository * if a downloaded PWA release zip file was used to build the PWA it no longer worked and failed at the ' npm install' --- schematics/src/helpers/lazy-components/factory.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/schematics/src/helpers/lazy-components/factory.ts b/schematics/src/helpers/lazy-components/factory.ts index b46e63dd38..a7b31e01d7 100644 --- a/schematics/src/helpers/lazy-components/factory.ts +++ b/schematics/src/helpers/lazy-components/factory.ts @@ -48,10 +48,10 @@ async function deleteOldComponents() { if (process.env.CI !== 'true') { let gitAvailable = false; try { - cp.execSync('git --version'); + cp.execSync('git status'); gitAvailable = true; } catch (error) { - console.warn('Git is not installed. Skipping deletion.'); + console.warn('Git is not installed or it is not a Git repository. Skipping deletion.'); } if (gitAvailable) { From feb16485318f476d55360a403769366d8e1607ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tristan=20Bouli=C3=A8re?= <108473857+tbouliere-datasolution@users.noreply.github.com> Date: Wed, 9 Nov 2022 11:38:30 +0100 Subject: [PATCH 08/39] fix: keep the footer at the bottom of the page (#1318) Due to the asynchronous loading of the pages or less content on a page than the window height can hold, the footer sometimes goes up to the header. A pre-sizing of the main container via styling avoids this unpleasant effect. application shell styling cleanup + migration note Co-authored-by: Stefan Hauke --- docs/guides/migrations.md | 5 +++++ src/app/app.component.html | 6 +++--- .../pages/store-locator/store-locator-page.component.html | 3 ++- src/styles/global/global.scss | 2 ++ 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/docs/guides/migrations.md b/docs/guides/migrations.md index 7a714ed873..6663a2809c 100644 --- a/docs/guides/migrations.md +++ b/docs/guides/migrations.md @@ -7,6 +7,11 @@ kb_sync_latest_only # Migrations +## 3.1 to 3.2 + +A styling adaption was made to the application shell to expand it to the full page height so the footer now always stays at the bottom. +Together with that an inline style of the `main-container` was moved to the global styling definition. + ## 3.0 to 3.1 The SSR environment variable 'ICM_IDENTITY_PROVIDER' will be removed in a future release ( PWA 5.0 ). diff --git a/src/app/app.component.html b/src/app/app.component.html index b98b45fa71..8aa2b4dbb3 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -1,13 +1,13 @@ -
+
-
+
-
+
diff --git a/src/app/extensions/store-locator/pages/store-locator/store-locator-page.component.html b/src/app/extensions/store-locator/pages/store-locator/store-locator-page.component.html index 84347aed49..2527834a82 100644 --- a/src/app/extensions/store-locator/pages/store-locator/store-locator-page.component.html +++ b/src/app/extensions/store-locator/pages/store-locator/store-locator-page.component.html @@ -1,7 +1,8 @@ -

{{ 'store_locator.title' | translate }}

+

{{ 'store_locator.title' | translate }}

{{ 'store_locator.description' | translate }}

{{ 'store_locator.howto' | translate }}

+
diff --git a/src/styles/global/global.scss b/src/styles/global/global.scss index 9b25dbf1b9..c9a7a39d69 100644 --- a/src/styles/global/global.scss +++ b/src/styles/global/global.scss @@ -2,6 +2,8 @@ // LAYOUT // contains layout and presentation classes for the global page structure (header, footer, global navigation, ...) .main-container { + position: relative; + min-height: 200px; background: $color-inverse; } From bb334f7e490fb677b09fec8d081df3ab1ba84745 Mon Sep 17 00:00:00 2001 From: Silke Date: Fri, 7 Oct 2022 09:36:21 +0200 Subject: [PATCH 09/39] deps: update formly to version 6 (#1291) --- .eslintrc.json | 9 ++- docs/guides/migrations.md | 4 ++ package-lock.json | 34 ++++------- package.json | 2 +- .../contact-form.component.spec.ts | 2 +- .../direct-order.component.spec.ts | 6 +- ...korder-add-products-form.component.spec.ts | 6 +- .../account-addresses.component.spec.ts | 10 +++- .../account-profile-company.component.spec.ts | 2 +- .../account-profile-email.component.spec.ts | 2 +- ...account-profile-password.component.spec.ts | 2 +- .../checkout-shipping.component.spec.ts | 2 +- .../registration-page.component.spec.ts | 8 +-- ...et-cost-center-selection.component.spec.ts | 15 ++--- ...-shipping-address-widget.component.spec.ts | 8 +++ .../translate-placeholder.extension.ts | 4 -- .../library-config-replacement.extension.ts | 15 +++-- .../select-field/select-field.component.html | 10 ++-- .../description-wrapper.component.spec.ts | 56 ++++++++----------- 19 files changed, 94 insertions(+), 103 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index e7d83277a4..2f6c7aa831 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -182,7 +182,14 @@ "dot-notation": "off", // disabled in favor of @typescript-eslint/dot-notation "eqeqeq": ["error", "always"], "etc/no-commented-out-code": "warn", - "etc/no-deprecated": "warn", + "etc/no-deprecated": [ + "warn", + { + "ignored": { + "templateOptions|to|FormlyTemplateOptions|expressionProperties": "name" // deprecated formly methods are temporarily ignored + } + } + ], "id-blacklist": [ "error", "any", diff --git a/docs/guides/migrations.md b/docs/guides/migrations.md index 6663a2809c..164170d4d5 100644 --- a/docs/guides/migrations.md +++ b/docs/guides/migrations.md @@ -12,6 +12,10 @@ kb_sync_latest_only A styling adaption was made to the application shell to expand it to the full page height so the footer now always stays at the bottom. Together with that an inline style of the `main-container` was moved to the global styling definition. +Formly has been upgraded from version 5 to 6. +Find more information in the [Formly Upgrade Guide](https://github.com/ngx-formly/ngx-formly/blob/main/UPGRADE-6.0.md). +We still use deprecated form properties like 'templateOptions' and 'expressionProperties' for compatibility reasons but we are going to replace them in the next major release. + ## 3.0 to 3.1 The SSR environment variable 'ICM_IDENTITY_PROVIDER' will be removed in a future release ( PWA 5.0 ). diff --git a/package-lock.json b/package-lock.json index f31b0a03fa..6cc68aea34 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34,7 +34,7 @@ "@ngrx/store": "14.3.2", "@ngrx/store-devtools": "14.3.2", "@nguniversal/express-engine": "14.2.0", - "@ngx-formly/core": "^5.12.7", + "@ngx-formly/core": "^6.0.1", "@ngx-translate/core": "^14.0.0", "@rx-angular/state": "1.7.0", "@sentry/browser": "^7.14.1", @@ -5475,22 +5475,17 @@ } }, "node_modules/@ngx-formly/core": { - "version": "5.12.7", - "resolved": "https://registry.npmjs.org/@ngx-formly/core/-/core-5.12.7.tgz", - "integrity": "sha512-WE4mVbIgWJ6zVf6WdQTUjQ17th/iZTW5pYlF43JdWxhwzmY6IX/NA2Qg7N4gtdlV3+Z+iSytcocBVx8G4kukZA==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@ngx-formly/core/-/core-6.0.1.tgz", + "integrity": "sha512-TkVg7m2SZEIHoL5kIJgH7TYqwT6G6mz1ZKIgvT3W3eBUr70u1HZ1RiDHjXeqMC3sse7saSDFsFOI6P7ybHJ1Zw==", "dependencies": { - "tslib": "^1.7.1" + "tslib": "^2.0.0" }, "peerDependencies": { - "@angular/forms": ">=7.0.0", - "rxjs": ">=6.3.0" + "@angular/forms": ">=13.2.0", + "rxjs": "^6.5.3 || ^7.0.0" } }, - "node_modules/@ngx-formly/core/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" - }, "node_modules/@ngx-translate/core": { "version": "14.0.0", "resolved": "https://registry.npmjs.org/@ngx-translate/core/-/core-14.0.0.tgz", @@ -32583,18 +32578,11 @@ } }, "@ngx-formly/core": { - "version": "5.12.7", - "resolved": "https://registry.npmjs.org/@ngx-formly/core/-/core-5.12.7.tgz", - "integrity": "sha512-WE4mVbIgWJ6zVf6WdQTUjQ17th/iZTW5pYlF43JdWxhwzmY6IX/NA2Qg7N4gtdlV3+Z+iSytcocBVx8G4kukZA==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@ngx-formly/core/-/core-6.0.1.tgz", + "integrity": "sha512-TkVg7m2SZEIHoL5kIJgH7TYqwT6G6mz1ZKIgvT3W3eBUr70u1HZ1RiDHjXeqMC3sse7saSDFsFOI6P7ybHJ1Zw==", "requires": { - "tslib": "^1.7.1" - }, - "dependencies": { - "tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" - } + "tslib": "^2.0.0" } }, "@ngx-translate/core": { diff --git a/package.json b/package.json index 93cc3edfe2..d385731d5e 100644 --- a/package.json +++ b/package.json @@ -82,7 +82,7 @@ "@ngrx/store": "14.3.2", "@ngrx/store-devtools": "14.3.2", "@nguniversal/express-engine": "14.2.0", - "@ngx-formly/core": "^5.12.7", + "@ngx-formly/core": "^6.0.1", "@ngx-translate/core": "^14.0.0", "@rx-angular/state": "1.7.0", "@sentry/browser": "^7.14.1", diff --git a/src/app/extensions/contact-us/pages/contact/contact-form/contact-form.component.spec.ts b/src/app/extensions/contact-us/pages/contact/contact-form/contact-form.component.spec.ts index 6d0a67373a..8cf6758b89 100644 --- a/src/app/extensions/contact-us/pages/contact/contact-form/contact-form.component.spec.ts +++ b/src/app/extensions/contact-us/pages/contact/contact-form/contact-form.component.spec.ts @@ -46,7 +46,7 @@ describe('Contact Form Component', () => { it('should always render formly form', () => { fixture.detectChanges(); - expect(element.querySelectorAll('formly-field')).toHaveLength(7); + expect(element.querySelectorAll('formly-group formly-field')).toHaveLength(7); }); it('should not emit contact request when invalid form is submitted', () => { diff --git a/src/app/extensions/quickorder/shared/direct-order/direct-order.component.spec.ts b/src/app/extensions/quickorder/shared/direct-order/direct-order.component.spec.ts index 1ed87d9f3c..4dd96dbef9 100644 --- a/src/app/extensions/quickorder/shared/direct-order/direct-order.component.spec.ts +++ b/src/app/extensions/quickorder/shared/direct-order/direct-order.component.spec.ts @@ -51,13 +51,13 @@ describe('Direct Order Component', () => { it('should display form with direct order configuration', () => { fixture.detectChanges(); - expect(element.querySelectorAll('formly-field')).toMatchInlineSnapshot(` + expect(element.querySelectorAll('formly-group formly-field')).toMatchInlineSnapshot(` NodeList [ - TextInputFieldComponent: sku ish-text-input-field { "fieldClass": "col-12", "placeholder": "shopping_cart.direct_order.item_placeholder", "attributes": { "autocomplete": "on" }, "label": - "", "focus": false, "disabled": false}, ] diff --git a/src/app/extensions/quickorder/shared/quickorder-add-products-form/quickorder-add-products-form.component.spec.ts b/src/app/extensions/quickorder/shared/quickorder-add-products-form/quickorder-add-products-form.component.spec.ts index 31d8fd673e..0191505920 100644 --- a/src/app/extensions/quickorder/shared/quickorder-add-products-form/quickorder-add-products-form.component.spec.ts +++ b/src/app/extensions/quickorder/shared/quickorder-add-products-form/quickorder-add-products-form.component.spec.ts @@ -48,11 +48,9 @@ describe('Quickorder Add Products Form Component', () => { it('should display form with add products configuration', () => { fixture.detectChanges(); - expect(element.querySelectorAll('formly-field')).toMatchInlineSnapshot(` + expect(element.querySelectorAll('formly-group formly-field')).toMatchInlineSnapshot(` NodeList [ - , + , ] `); }); diff --git a/src/app/pages/account-addresses/account-addresses/account-addresses.component.spec.ts b/src/app/pages/account-addresses/account-addresses/account-addresses.component.spec.ts index 7c22517d66..0f61dec195 100644 --- a/src/app/pages/account-addresses/account-addresses/account-addresses.component.spec.ts +++ b/src/app/pages/account-addresses/account-addresses/account-addresses.component.spec.ts @@ -129,7 +129,7 @@ describe('Account Addresses Component', () => { expect(component.preferredAddressesEqual).toBeTruthy(); expect(element.querySelector('div[data-testing-id=preferred-invoice-and-shipping-address]')).toBeTruthy(); expect( - element.querySelectorAll('div[data-testing-id=preferred-invoice-and-shipping-address] formly-field') + element.querySelectorAll('div[data-testing-id=preferred-invoice-and-shipping-address] formly-group formly-field') ).toHaveLength(2); expect(element.querySelector('div[data-testing-id=preferred-invoice-address]')).toBeFalsy(); expect(element.querySelector('div[data-testing-id=preferred-shipping-address]')).toBeFalsy(); @@ -148,9 +148,13 @@ describe('Account Addresses Component', () => { expect(element.querySelector('div[data-testing-id=preferred-invoice-and-shipping-address]')).toBeFalsy(); expect(element.querySelector('div[data-testing-id=preferred-invoice-address]')).toBeTruthy(); - expect(element.querySelectorAll('div[data-testing-id=preferred-invoice-address] formly-field')).toHaveLength(1); + expect( + element.querySelectorAll('div[data-testing-id=preferred-invoice-address] formly-group formly-field') + ).toHaveLength(1); expect(element.querySelector('div[data-testing-id=preferred-shipping-address]')).toBeTruthy(); - expect(element.querySelectorAll('div[data-testing-id=preferred-shipping-address] formly-field')).toHaveLength(1); + expect( + element.querySelectorAll('div[data-testing-id=preferred-shipping-address] formly-group formly-field') + ).toHaveLength(1); }); it('should not display further addresses if only preferred invoice and shipping addresses are available', () => { diff --git a/src/app/pages/account-profile-company/account-profile-company/account-profile-company.component.spec.ts b/src/app/pages/account-profile-company/account-profile-company/account-profile-company.component.spec.ts index 951eab60e7..8fc470faa7 100644 --- a/src/app/pages/account-profile-company/account-profile-company/account-profile-company.component.spec.ts +++ b/src/app/pages/account-profile-company/account-profile-company/account-profile-company.component.spec.ts @@ -54,7 +54,7 @@ describe('Account Profile Company Component', () => { it('should display 3 input fields for companyName, companyName2 and taxationID', () => { fixture.detectChanges(); - expect(element.querySelectorAll('formly-field')).toHaveLength(3); + expect(element.querySelectorAll('formly-group formly-field')).toHaveLength(3); }); it('should emit updateCompanyProfile event if form is valid', () => { diff --git a/src/app/pages/account-profile-email/account-profile-email/account-profile-email.component.spec.ts b/src/app/pages/account-profile-email/account-profile-email/account-profile-email.component.spec.ts index 28eb48346a..4a920e0682 100644 --- a/src/app/pages/account-profile-email/account-profile-email/account-profile-email.component.spec.ts +++ b/src/app/pages/account-profile-email/account-profile-email/account-profile-email.component.spec.ts @@ -39,7 +39,7 @@ describe('Account Profile Email Component', () => { it('should display 3 input fields for email, emailConfirmation and password', () => { fixture.detectChanges(); - expect(element.querySelectorAll('formly-group formly-field')).toHaveLength(3); + expect(element.querySelectorAll('formly-group formly-group formly-field')).toHaveLength(3); }); it('should emit updateEmail event if form is valid', () => { diff --git a/src/app/pages/account-profile-password/account-profile-password/account-profile-password.component.spec.ts b/src/app/pages/account-profile-password/account-profile-password/account-profile-password.component.spec.ts index 3f1506ed63..98786aaa8c 100644 --- a/src/app/pages/account-profile-password/account-profile-password/account-profile-password.component.spec.ts +++ b/src/app/pages/account-profile-password/account-profile-password/account-profile-password.component.spec.ts @@ -35,7 +35,7 @@ describe('Account Profile Password Component', () => { it('should display 3 input fields for oldPassword, password and passwordConfirmation', () => { fixture.detectChanges(); - expect(element.querySelectorAll('formly-group formly-field')).toHaveLength(3); + expect(element.querySelectorAll('formly-group formly-group formly-field')).toHaveLength(3); }); it('should emit updatePassword event if form is valid', () => { diff --git a/src/app/pages/checkout-shipping/checkout-shipping/checkout-shipping.component.spec.ts b/src/app/pages/checkout-shipping/checkout-shipping/checkout-shipping.component.spec.ts index 15f8a716b4..da042482ab 100644 --- a/src/app/pages/checkout-shipping/checkout-shipping/checkout-shipping.component.spec.ts +++ b/src/app/pages/checkout-shipping/checkout-shipping/checkout-shipping.component.spec.ts @@ -48,7 +48,7 @@ describe('Checkout Shipping Component', () => { it('should render available shipping methods on page', () => { fixture.detectChanges(); - expect(element.querySelectorAll('formly-field')).toHaveLength(1); + expect(element.querySelectorAll('formly-group formly-field')).toHaveLength(1); }); it('should throw updateShippingMethod event when the user changes payment selection', () => { diff --git a/src/app/pages/registration/registration-page.component.spec.ts b/src/app/pages/registration/registration-page.component.spec.ts index acf629b622..78c8dc161d 100644 --- a/src/app/pages/registration/registration-page.component.spec.ts +++ b/src/app/pages/registration/registration-page.component.spec.ts @@ -63,12 +63,12 @@ describe('Registration Page Component', () => { it('should display form with registration configuration', () => { fixture.detectChanges(); - expect(element.querySelectorAll('formly-field')).toMatchInlineSnapshot(` + expect(element.querySelectorAll('formly-group formly-field')).toMatchInlineSnapshot(` NodeList [ - TextInputFieldComponent: test ish-text-input-field { "label": "", "placeholder": "", "focus": - false, "disabled": false}TextInputFieldComponent: test ish-text-input-field { "label": "", "placeholder": "", + "disabled": false}, ] diff --git a/src/app/shared/components/basket/basket-cost-center-selection/basket-cost-center-selection.component.spec.ts b/src/app/shared/components/basket/basket-cost-center-selection/basket-cost-center-selection.component.spec.ts index caabaf17c8..3e69e6d73b 100644 --- a/src/app/shared/components/basket/basket-cost-center-selection/basket-cost-center-selection.component.spec.ts +++ b/src/app/shared/components/basket/basket-cost-center-selection/basket-cost-center-selection.component.spec.ts @@ -71,8 +71,8 @@ describe('Basket Cost Center Selection Component', () => { when(checkoutFacade.eligibleCostCenterSelectOptions$()).thenReturn(of([mockCostCenterOptions[0]])); fixture.detectChanges(); - expect(element.querySelectorAll('formly-field')).toHaveLength(1); - expect(element.querySelector('formly-field').textContent).toMatchInlineSnapshot(` + expect(element.querySelectorAll('formly-group formly-field')).toHaveLength(1); + expect(element.querySelector('formly-group formly-field').textContent).toMatchInlineSnapshot(` "SelectFieldComponent: costCenter ish-select-field { \\"label\\": \\"checkout.cost_center.select.label\\", \\"required\\": true, @@ -84,7 +84,6 @@ describe('Basket Cost Center Selection Component', () => { } ], \\"placeholder\\": \\"\\", - \\"focus\\": false, \\"disabled\\": false }" `); @@ -93,8 +92,8 @@ describe('Basket Cost Center Selection Component', () => { it('should be rendered with correct options and placeholder for multiple cost center options', () => { when(checkoutFacade.eligibleCostCenterSelectOptions$()).thenReturn(of(mockCostCenterOptions)); fixture.detectChanges(); - expect(element.querySelectorAll('formly-field')).toHaveLength(1); - expect(element.querySelector('formly-field').textContent).toMatchInlineSnapshot(` + expect(element.querySelectorAll('formly-group formly-field')).toHaveLength(1); + expect(element.querySelector('formly-group formly-field').textContent).toMatchInlineSnapshot(` "SelectFieldComponent: costCenter ish-select-field { \\"label\\": \\"checkout.cost_center.select.label\\", \\"required\\": true, @@ -110,7 +109,6 @@ describe('Basket Cost Center Selection Component', () => { } ], \\"placeholder\\": \\"account.option.select.text\\", - \\"focus\\": false, \\"disabled\\": false }" `); @@ -125,8 +123,8 @@ describe('Basket Cost Center Selection Component', () => { subject$.next({ ...BasketMockData.getBasket(), costCenter: '2' }); fixture.detectChanges(); - expect(element.querySelectorAll('formly-field')).toHaveLength(1); - expect(element.querySelector('formly-field').textContent).toMatchInlineSnapshot(` + expect(element.querySelectorAll('formly-group formly-field')).toHaveLength(1); + expect(element.querySelector('formly-group formly-field').textContent).toMatchInlineSnapshot(` "SelectFieldComponent: costCenter ish-select-field { \\"label\\": \\"checkout.cost_center.select.label\\", \\"required\\": true, @@ -142,7 +140,6 @@ describe('Basket Cost Center Selection Component', () => { } ], \\"placeholder\\": \\"\\", - \\"focus\\": false, \\"disabled\\": false }" `); diff --git a/src/app/shared/components/checkout/basket-shipping-address-widget/basket-shipping-address-widget.component.spec.ts b/src/app/shared/components/checkout/basket-shipping-address-widget/basket-shipping-address-widget.component.spec.ts index e25e7ae120..0bd9808109 100644 --- a/src/app/shared/components/checkout/basket-shipping-address-widget/basket-shipping-address-widget.component.spec.ts +++ b/src/app/shared/components/checkout/basket-shipping-address-widget/basket-shipping-address-widget.component.spec.ts @@ -90,6 +90,8 @@ describe('Basket Shipping Address Widget Component', () => { "ish-address", "formly-form", "formly-field", + "formly-group", + "formly-field", "ng-component", "ish-formly-customer-address-form", ] @@ -114,6 +116,8 @@ describe('Basket Shipping Address Widget Component', () => { "ish-address", "formly-form", "formly-field", + "formly-group", + "formly-field", "ng-component", "ish-formly-customer-address-form", ] @@ -141,6 +145,8 @@ describe('Basket Shipping Address Widget Component', () => { Array [ "formly-form", "formly-field", + "formly-group", + "formly-field", "ng-component", "ish-formly-customer-address-form", ] @@ -163,6 +169,8 @@ describe('Basket Shipping Address Widget Component', () => { Array [ "formly-form", "formly-field", + "formly-group", + "formly-field", "ng-component", "ish-formly-customer-address-form", ] diff --git a/src/app/shared/formly/extensions/translate-placeholder.extension.ts b/src/app/shared/formly/extensions/translate-placeholder.extension.ts index c07694db39..14a8c10cee 100644 --- a/src/app/shared/formly/extensions/translate-placeholder.extension.ts +++ b/src/app/shared/formly/extensions/translate-placeholder.extension.ts @@ -23,10 +23,6 @@ class TranslatePlaceholderExtension implements FormlyExtension { of(to.placeholder).pipe(delay(1000)) ).subscribe(translation => { field.templateOptions.placeholder = translation; - // trigger formly change detection - if (field.options) { - field.options.resetModel(); - } }); } } diff --git a/src/app/shared/formly/field-library/library-config-replacement.extension.ts b/src/app/shared/formly/field-library/library-config-replacement.extension.ts index c568f1fc68..e7291f837f 100644 --- a/src/app/shared/formly/field-library/library-config-replacement.extension.ts +++ b/src/app/shared/formly/field-library/library-config-replacement.extension.ts @@ -16,12 +16,15 @@ class LibraryConfigReplacementExtension implements FormlyExtension { private configIds: Set; prePopulate(field: FormlyFieldConfig): void { - const configId = new RegExp(/^#(.+)$/).exec(field.type)?.[1]; - if (this.configIds.has(configId)) { - const override = omit(field, 'type'); - const config = this.fieldLibrary.getConfiguration(configId, override); - // eslint-disable-next-line ban/ban - Object.assign(field, config); + if (typeof field.type === 'string') { + const configId = new RegExp(/^#(.+)$/).exec(field.type)?.[1]; + if (this.configIds.has(configId)) { + const override = omit(field, 'type'); + const config = this.fieldLibrary.getConfiguration(configId, override); + // eslint-disable-next-line ban/ban + Object.assign(field, config); + field.templateOptions = config.templateOptions; + } } } } diff --git a/src/app/shared/formly/types/select-field/select-field.component.html b/src/app/shared/formly/types/select-field/select-field.component.html index aeedfe1e69..8496863315 100644 --- a/src/app/shared/formly/types/select-field/select-field.component.html +++ b/src/app/shared/formly/types/select-field/select-field.component.html @@ -7,12 +7,10 @@ [attr.data-testing-id]="field.key" > - - - - + + diff --git a/src/app/shared/formly/wrappers/description-wrapper/description-wrapper.component.spec.ts b/src/app/shared/formly/wrappers/description-wrapper/description-wrapper.component.spec.ts index 6b848c7b7d..add60f9dc0 100644 --- a/src/app/shared/formly/wrappers/description-wrapper/description-wrapper.component.spec.ts +++ b/src/app/shared/formly/wrappers/description-wrapper/description-wrapper.component.spec.ts @@ -41,15 +41,20 @@ describe('Description Wrapper Component', () => { fixture = TestBed.createComponent(FormlyTestingContainerComponent); component = fixture.componentInstance; element = fixture.nativeElement; + + component.form = new FormGroup({}); + component.model = {}; + component.fields = [ + { + ...fieldBase, + templateOptions: { + customDescription: 'desc', + }, + }, + ]; }); it('should be created', () => { - const testComponentInputs = { - model: {}, - fields: [fieldBase], - form: new FormGroup({}), - }; - component.testComponentInputs = testComponentInputs; fixture.detectChanges(); expect(component).toBeTruthy(); @@ -59,42 +64,25 @@ describe('Description Wrapper Component', () => { }); it('should contain basic string description for no arguments', () => { - const testComponentInputs = { - model: {}, - form: new FormGroup({}), - fields: [ - { - ...fieldBase, - templateOptions: { - customDescription: 'desc', - }, - }, - ], - }; - component.testComponentInputs = testComponentInputs; fixture.detectChanges(); + expect(element.querySelector('small').textContent).toMatchInlineSnapshot(`" value:desc args:{} "`); }); it('should contain complex description when arguments are supplied', () => { - const testComponentInputs = { - model: {}, - form: new FormGroup({}), - fields: [ - { - ...fieldBase, - templateOptions: { - customDescription: { - key: 'description', - args: { - 0: 'argument', - }, + component.fields = [ + { + ...fieldBase, + templateOptions: { + customDescription: { + key: 'description', + args: { + 0: 'argument', }, }, }, - ], - }; - component.testComponentInputs = testComponentInputs; + }, + ]; fixture.detectChanges(); expect(element.querySelector('small').textContent).toMatchInlineSnapshot( `" value:description args:{\\"0\\":\\"argument\\"} "` From 8da52d6b496bc953a47ea2dca93ce7ba17dc0ec2 Mon Sep 17 00:00:00 2001 From: Christian Linde Date: Thu, 10 Nov 2022 10:21:39 +0100 Subject: [PATCH 10/39] fix: add promotion scope to the validation of the first basket checkout step (#1322) --- src/app/core/store/customer/basket/basket-validation.effects.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/core/store/customer/basket/basket-validation.effects.ts b/src/app/core/store/customer/basket/basket-validation.effects.ts index 8cee7683e9..4336fe7345 100644 --- a/src/app/core/store/customer/basket/basket-validation.effects.ts +++ b/src/app/core/store/customer/basket/basket-validation.effects.ts @@ -42,7 +42,7 @@ export class BasketValidationEffects { ) {} private validationSteps: { scopes: BasketValidationScopeType[]; route: string }[] = [ - { scopes: ['Products', 'Value', 'CostCenter'], route: '/basket' }, + { scopes: ['Products', 'Promotion', 'Value', 'CostCenter'], route: '/basket' }, { scopes: ['InvoiceAddress', 'ShippingAddress', 'Addresses'], route: '/checkout/address' }, { scopes: ['Shipping'], route: '/checkout/shipping' }, { scopes: ['Payment'], route: '/checkout/payment' }, From 80d5f9deabd8df2e07fff65061658e33786a40dd Mon Sep 17 00:00:00 2001 From: Marcel Eisentraut Date: Thu, 10 Nov 2022 16:37:33 +0100 Subject: [PATCH 11/39] fix: apiToken cookie should be shared between same host (#1321) * for language switch in multisite configuration * for hybrid approach --- src/app/core/utils/api-token/api-token.service.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/app/core/utils/api-token/api-token.service.ts b/src/app/core/utils/api-token/api-token.service.ts index 2a9be22718..7a6613ee3e 100644 --- a/src/app/core/utils/api-token/api-token.service.ts +++ b/src/app/core/utils/api-token/api-token.service.ts @@ -91,6 +91,7 @@ export class ApiTokenService { expires: new Date(Date.now() + 3600000), secure: true, sameSite: 'Strict', + path: '/', }); } else { cookiesService.remove('apiToken'); From a98d391bf773fdc4573e0cdf14cdf06a204f150d Mon Sep 17 00:00:00 2001 From: DilaraGueler <116000195+DilaraGueler@users.noreply.github.com> Date: Mon, 14 Nov 2022 12:54:48 +0100 Subject: [PATCH 12/39] feat: customer address update in the My Account section (#1315) Co-authored-by: Silke --- .../e2e/pages/account/addresses.page.ts | 87 +++++++++++ .../account/addresses-crud.b2b.e2e-spec.ts | 98 +++++++++++++ src/app/core/facades/account.facade.ts | 5 + .../customer/addresses/addresses.actions.ts | 2 + .../addresses/addresses.effects.spec.ts | 44 ++++++ .../customer/addresses/addresses.effects.ts | 24 ++++ .../customer/addresses/addresses.reducer.ts | 2 + .../account-addresses.component.html | 136 +++++++++++++----- .../account-addresses.component.spec.ts | 8 ++ .../account-addresses.component.ts | 18 ++- .../formly-address-form.component.ts | 3 +- src/assets/i18n/de_DE.json | 1 + src/assets/i18n/en_US.json | 1 + src/assets/i18n/fr_FR.json | 1 + 14 files changed, 392 insertions(+), 38 deletions(-) create mode 100644 e2e/cypress/e2e/pages/account/addresses.page.ts create mode 100644 e2e/cypress/e2e/specs/account/addresses-crud.b2b.e2e-spec.ts diff --git a/e2e/cypress/e2e/pages/account/addresses.page.ts b/e2e/cypress/e2e/pages/account/addresses.page.ts new file mode 100644 index 0000000000..00b1e4a32c --- /dev/null +++ b/e2e/cypress/e2e/pages/account/addresses.page.ts @@ -0,0 +1,87 @@ +import { fillFormField } from '../../framework'; +import { Registration } from '../account/registration.page'; +import { HeaderModule } from '../header.module'; + +export type AddressDetailsTypes = Partial< + Pick +>; +export class AddressesPage { + readonly header = new HeaderModule(); + readonly tag = 'ish-account-addresses'; + + get defaultAddress() { + return cy.get(this.tag).find('[data-testing-id="preferred-invoice-and-shipping-address"]'); + } + + get furtherAddress() { + return cy.get(this.tag).find('[data-testing-id="further-addresses"]'); + } + + get shippingAddress() { + return cy.get(this.tag).find('[data-testing-id="preferred-shipping-address"]'); + } + + get invoiceAddress() { + return cy.get(this.tag).find('[data-testing-id="preferred-invoice-address"]'); + } + + createAddress() { + cy.get('[data-testing-id="create-address-button"]').click(); + } + + fillForm(user: string, password: string) { + cy.get('input[data-testing-id="login"]').clear().type(user).blur(); + cy.get('input[data-testing-id="password"]').clear().type(password).blur(); + return this; + } + + fillCreateAddressForm(address: AddressDetailsTypes) { + Object.keys(address).forEach(key => fillFormField('[data-testing-id="create-address-form"]', key, address[key])); + return this; + } + + fillUpdateAddressForm(address: AddressDetailsTypes) { + Object.keys(address).forEach(key => fillFormField('[data-testing-id="update-address-form"]', key, address[key])); + return this; + } + + cancel() { + cy.get('button').contains('Cancel').click(); + } + + saveAddress() { + cy.get('button').contains('Save Address').click(); + } + + selectShippingAddress() { + cy.get('select[placeholder="Change preferred shipping address"]').select(1); + } + + selectInvoiceAddress() { + cy.get('select[placeholder="Change preferred invoice address"]').select(2); + } + + updateAddress() { + cy.get('[data-testing-id="update-address-button"]').last().click(); + } + + submit() { + cy.intercept('PUT', '**/customers/**').as('customers'); + cy.wait(500); + + cy.get(this.tag).find('button[type="submit"]').click(); + + return cy.wait('@customers'); + } + + deleteAddress() { + cy.get('[data-testing-id="delete-address-icon"]').last().click(); + cy.get('button').contains('Delete').click(); + } + + get successMessage() { + return { + message: cy.get('#toast-container').find('.toast-message'), + }; + } +} diff --git a/e2e/cypress/e2e/specs/account/addresses-crud.b2b.e2e-spec.ts b/e2e/cypress/e2e/specs/account/addresses-crud.b2b.e2e-spec.ts new file mode 100644 index 0000000000..d0d2da8d26 --- /dev/null +++ b/e2e/cypress/e2e/specs/account/addresses-crud.b2b.e2e-spec.ts @@ -0,0 +1,98 @@ +import { at } from '../../framework'; +import { createB2BUserViaREST } from '../../framework/b2b-user'; +import { AddressDetailsTypes, AddressesPage } from '../../pages/account/addresses.page'; +import { LoginPage } from '../../pages/account/login.page'; +import { Registration, sensibleDefaults } from '../../pages/account/registration.page'; + +const _ = { + user: { + login: `testuser${new Date().getTime()}@test.intershop.de`, + ...sensibleDefaults, + companyName1: 'Big Foods', + } as Registration, + address: { + countryCode: 'DE', + companyName1: 'Intershop', + firstName: 'Pablo', + lastName: 'Parked', + addressLine1: 'Marcher Str. 87', + city: 'Stuttgart', + postalCode: '12345', + } as AddressDetailsTypes, + furtherAddress: { + companyName1: 'Samsung', + firstName: 'Daniel', + lastName: 'Circus', + addressLine1: 'Berg Str. 83', + city: 'Heidelberg', + postalCode: '36890', + countryCode: 'DE', + } as AddressDetailsTypes, + newAddress: { + companyName1: 'Samsung DE', + firstName: 'Daniels', + lastName: 'Decorous', + addressLine1: 'HeidelBerg Str. 83', + city: 'Heidelberg', + postalCode: '36890', + countryCode: 'DE', + } as AddressDetailsTypes, +}; + +describe('Addresses Page Functionality', () => { + before(() => { + createB2BUserViaREST(_.user); + LoginPage.navigateTo('/account/addresses'); + at(LoginPage, page => { + page.fillForm(_.user.login, _.user.password); + page.submit().its('response.statusCode').should('equal', 200); + }); + at(AddressesPage, page => { + page.defaultAddress.should('exist'); + }); + }); + + it('should be able to create address and assign as shipping address', () => { + at(AddressesPage, page => { + page.createAddress(); + cy.wait(500); + page.fillCreateAddressForm(_.address); + page.saveAddress(); + page.furtherAddress.should('contain', _.address.addressLine1); + page.selectShippingAddress(); + page.shippingAddress.should('contain', _.address.addressLine1); + }); + }); + + it('should be able to create a further address and see changes', () => { + at(AddressesPage, page => { + page.createAddress(); + page.fillCreateAddressForm(_.furtherAddress); + page.saveAddress(); + page.furtherAddress.should('contain', _.furtherAddress.addressLine1); + }); + }); + + it('should be able to assign the further address as default invoice address', () => { + at(AddressesPage, page => { + page.selectInvoiceAddress(); + page.invoiceAddress.should('contain', _.furtherAddress.addressLine1); + }); + }); + + it('should be able to update address details and see changes', () => { + at(AddressesPage, page => { + page.updateAddress(); + page.fillUpdateAddressForm(_.newAddress).submit().its('response.statusCode').should('equal', 200); + page.successMessage.message.should('contain', 'updated'); + page.furtherAddress.should('contain', `${_.newAddress.firstName} ${_.newAddress.lastName}`); + }); + }); + + it('should be able to delete an address and see changes', () => { + at(AddressesPage, page => { + page.deleteAddress(); + page.successMessage.message.should('contain', 'deleted'); + }); + }); +}); diff --git a/src/app/core/facades/account.facade.ts b/src/app/core/facades/account.facade.ts index 10d682d961..0f886259be 100644 --- a/src/app/core/facades/account.facade.ts +++ b/src/app/core/facades/account.facade.ts @@ -15,6 +15,7 @@ import { MessagesPayloadType } from 'ish-core/store/core/messages'; import { createCustomerAddress, deleteCustomerAddress, + updateCustomerAddress, getAddressesError, getAddressesLoading, getAllAddresses, @@ -240,6 +241,10 @@ export class AccountFacade { this.store.dispatch(deleteCustomerAddress({ addressId })); } + updateCustomerAddress(address: Address) { + this.store.dispatch(updateCustomerAddress({ address })); + } + // DATA REQUESTS dataRequestLoading$ = this.store.pipe(select(getDataRequestLoading)); diff --git a/src/app/core/store/customer/addresses/addresses.actions.ts b/src/app/core/store/customer/addresses/addresses.actions.ts index 2f9ff5aa8f..a72633552d 100644 --- a/src/app/core/store/customer/addresses/addresses.actions.ts +++ b/src/app/core/store/customer/addresses/addresses.actions.ts @@ -21,6 +21,8 @@ export const createCustomerAddressSuccess = createAction( payload<{ address: Address }>() ); +export const updateCustomerAddress = createAction('[Address] Update Customer Address', payload<{ address: Address }>()); + export const updateCustomerAddressFail = createAction('[Address API] Update Customer Address Fail', httpError()); export const updateCustomerAddressSuccess = createAction( diff --git a/src/app/core/store/customer/addresses/addresses.effects.spec.ts b/src/app/core/store/customer/addresses/addresses.effects.spec.ts index aae8de8be1..398b96fb5f 100644 --- a/src/app/core/store/customer/addresses/addresses.effects.spec.ts +++ b/src/app/core/store/customer/addresses/addresses.effects.spec.ts @@ -21,6 +21,9 @@ import { deleteCustomerAddress, deleteCustomerAddressFail, deleteCustomerAddressSuccess, + updateCustomerAddress, + updateCustomerAddressFail, + updateCustomerAddressSuccess, loadAddresses, loadAddressesSuccess, } from './addresses.actions'; @@ -37,6 +40,7 @@ describe('Addresses Effects', () => { when(addressServiceMock.getCustomerAddresses()).thenReturn(of([{ urn: 'test' } as Address])); when(addressServiceMock.createCustomerAddress(anyString(), anything())).thenReturn(of({ urn: 'test' } as Address)); + when(addressServiceMock.updateCustomerAddress(anyString(), anything())).thenReturn(of({ urn: 'test' } as Address)); when(addressServiceMock.deleteCustomerAddress(anyString(), anything())).thenReturn(of('123')); TestBed.configureTestingModule({ @@ -155,4 +159,44 @@ describe('Addresses Effects', () => { expect(effects.deleteCustomerAddress$).toBeObservable(expected$); }); }); + + describe('updateCustomerAddress$', () => { + it('should call the addressService for updateCustomerAddress', done => { + const address = { urn: '123' } as Address; + const action = updateCustomerAddress({ address }); + actions$ = of(action); + + effects.updateCustomerAddress$.subscribe(() => { + verify(addressServiceMock.updateCustomerAddress('-', anything())).once(); + done(); + }); + }); + + it('should map to action of type updateCustomerSuccess', () => { + const address = { urn: '123' } as Address; + const action = updateCustomerAddress({ address }); + const completion = updateCustomerAddressSuccess({ address }); + const completion2 = displaySuccessMessage({ + message: 'account.addresses.address_updated.message', + }); + actions$ = hot('-a-', { a: action }); + const expected$ = cold('-(cd)', { c: completion, d: completion2 }); + + expect(effects.updateCustomerAddress$).toBeObservable(expected$); + }); + + it('should map invalid request to action of type updateCustomerFail', () => { + when(addressServiceMock.updateCustomerAddress('-', anything())).thenReturn( + throwError(() => makeHttpError({ message: 'invalid' })) + ); + const address = { urn: '123' } as Address; + const action = updateCustomerAddress({ address }); + const error = makeHttpError({ message: 'invalid' }); + const completion = updateCustomerAddressFail({ error }); + actions$ = hot('-a-a-a', { a: action }); + const expected$ = cold('-c-c-c', { c: completion }); + + expect(effects.updateCustomerAddress$).toBeObservable(expected$); + }); + }); }); diff --git a/src/app/core/store/customer/addresses/addresses.effects.ts b/src/app/core/store/customer/addresses/addresses.effects.ts index 48b4b50d58..7f5e5add04 100644 --- a/src/app/core/store/customer/addresses/addresses.effects.ts +++ b/src/app/core/store/customer/addresses/addresses.effects.ts @@ -12,6 +12,9 @@ import { createCustomerAddress, createCustomerAddressFail, createCustomerAddressSuccess, + updateCustomerAddress, + updateCustomerAddressFail, + updateCustomerAddressSuccess, deleteCustomerAddress, deleteCustomerAddressFail, deleteCustomerAddressSuccess, @@ -59,6 +62,27 @@ export class AddressesEffects { ) ); + /** + * Updates a customer address. + */ + updateCustomerAddress$ = createEffect(() => + this.actions$.pipe( + ofType(updateCustomerAddress), + mapToPayloadProperty('address'), + withLatestFrom(this.store.pipe(select(getLoggedInCustomer))), + filter(([address, customer]) => !!address || !!customer), + mergeMap(([address]) => + this.addressService.updateCustomerAddress('-', address).pipe( + mergeMap(() => [ + updateCustomerAddressSuccess({ address }), + displaySuccessMessage({ message: 'account.addresses.address_updated.message' }), + ]), + mapErrorToAction(updateCustomerAddressFail) + ) + ) + ) + ); + /** * Deletes a customer address. */ diff --git a/src/app/core/store/customer/addresses/addresses.reducer.ts b/src/app/core/store/customer/addresses/addresses.reducer.ts index e6424c16bb..8bcbc60cbf 100644 --- a/src/app/core/store/customer/addresses/addresses.reducer.ts +++ b/src/app/core/store/customer/addresses/addresses.reducer.ts @@ -21,6 +21,7 @@ import { loadAddresses, loadAddressesFail, loadAddressesSuccess, + updateCustomerAddress, updateCustomerAddressFail, updateCustomerAddressSuccess, } from './addresses.actions'; @@ -43,6 +44,7 @@ export const addressesReducer = createReducer( loadAddresses, createCustomerAddress, createBasketAddress, + updateCustomerAddress, updateBasketAddress, deleteCustomerAddress, deleteBasketShippingAddress diff --git a/src/app/pages/account-addresses/account-addresses/account-addresses.component.html b/src/app/pages/account-addresses/account-addresses/account-addresses.component.html index fa600601e6..7ac61ededb 100644 --- a/src/app/pages/account-addresses/account-addresses/account-addresses.component.html +++ b/src/app/pages/account-addresses/account-addresses/account-addresses.component.html @@ -3,7 +3,7 @@

{{ 'account.addresses.saved_address.heading' | translate }}

{{ 'account.addresses.preferredinvoiceandshipping.heading' | translate }}

- - -
+ +
{{ 'account.addresses.preferredinvoiceandshipping.heading' | translate }}

{{ 'account.addresses.preferredinvoice.heading' | translate }}

- +

{{ 'account.addresses.no_preferred_invoice_address.text' | translate }}

-
+
@@ -74,12 +87,19 @@

{{ 'account.addresses.preferredinvoice.heading' | translate }}

{{ 'account.addresses.preferredshipping.heading' | translate }}

- +

{{ 'account.addresses.no_preferred_shipping_address.text' | translate }}

-
diff --git a/src/app/pages/account-addresses/account-addresses/account-addresses.component.spec.ts b/src/app/pages/account-addresses/account-addresses/account-addresses.component.spec.ts index 0f61dec195..884043e931 100644 --- a/src/app/pages/account-addresses/account-addresses/account-addresses.component.spec.ts +++ b/src/app/pages/account-addresses/account-addresses/account-addresses.component.spec.ts @@ -275,6 +275,14 @@ describe('Account Addresses Component', () => { verify(accountFacade.createCustomerAddress(anything())).once(); }); + it('should emit updateAddress event when updateAddress is triggered', () => { + const address = { id: '123' } as Address; + + component.updateAddress(address); + + verify(accountFacade.updateCustomerAddress(anything())).once(); + }); + it('should emit deleteCustomerAddress event when deleteCustomerAddress is triggered', () => { const address = { id: '123' } as Address; diff --git a/src/app/pages/account-addresses/account-addresses/account-addresses.component.ts b/src/app/pages/account-addresses/account-addresses/account-addresses.component.ts index 306b4f5a8c..a0c152f4bc 100644 --- a/src/app/pages/account-addresses/account-addresses/account-addresses.component.ts +++ b/src/app/pages/account-addresses/account-addresses/account-addresses.component.ts @@ -36,6 +36,7 @@ export class AccountAddressesComponent implements OnInit, OnDestroy { selectShippingConfig: FormlyFieldConfig; isCreateAddressFormCollapsed = true; + updateFormExpandedAddressId: string; preferredAddressForm: FormGroup = new FormGroup({}); furtherAddresses: Address[] = []; @@ -150,15 +151,30 @@ export class AccountAddressesComponent implements OnInit, OnDestroy { showCreateAddressForm() { this.isCreateAddressFormCollapsed = false; } - hideCreateAddressForm() { this.isCreateAddressFormCollapsed = true; } + showUpdateAddressForm(address: Address) { + this.updateFormExpandedAddressId = address.id; + } + hideUpdateAddressForm() { + this.updateFormExpandedAddressId = undefined; + } + + isUpdateAddressFormCollapsed(address: Address) { + return address?.id !== this.updateFormExpandedAddressId; + } + createAddress(address: Address) { this.accountFacade.createCustomerAddress(address); } + updateAddress(address: Address): void { + this.accountFacade.updateCustomerAddress(address); + this.hideUpdateAddressForm(); + } + deleteAddress(address: Address) { this.accountFacade.deleteCustomerAddress(address.id); } diff --git a/src/app/shared/formly-address-forms/components/formly-address-form/formly-address-form.component.ts b/src/app/shared/formly-address-forms/components/formly-address-form/formly-address-form.component.ts index 8c03d9b4f4..49f29032a8 100644 --- a/src/app/shared/formly-address-forms/components/formly-address-form/formly-address-form.component.ts +++ b/src/app/shared/formly-address-forms/components/formly-address-form/formly-address-form.component.ts @@ -125,9 +125,10 @@ export class FormlyAddressFormComponent implements OnInit, OnChanges { } private fillForm(prefilledAddress: Partial
= {}) { - if (Object.keys(prefilledAddress).length === 0) { + if (!this.addressForm || Object.keys(prefilledAddress).length === 0) { return; } + this.addressModel.countryCode = prefilledAddress.countryCode; this.handleCountryChange(this.addressModel); this.addressModel = { diff --git a/src/assets/i18n/de_DE.json b/src/assets/i18n/de_DE.json index d0ebc85f90..5f7a84e579 100644 --- a/src/assets/i18n/de_DE.json +++ b/src/assets/i18n/de_DE.json @@ -31,6 +31,7 @@ "account.address.uk.postalcode.error.regexp": "Geben Sie eine gültige PLZ (ZIP-Code) für UK an (mit Leerzeichen).", "account.address.us.postalcode.error.regexp": "Geben Sie eine gültige PLZ (ZIP-Code) für die USA an.", "account.addresses.add_address.button.text": "Adresse hinzufügen", + "account.addresses.address_updated.message": "Ihre Adresse wurde aktualisiert.", "account.addresses.create_address.button.label": "Adresse speichern", "account.addresses.delete_address.text": "Diese Adresse löschen", "account.addresses.have_no_saved_address.text": "Sie haben derzeit keine gespeicherten Adressen. Fügen Sie Ihre Rechnungs- und Lieferadresse hinzu, um den Bestellvorgang in Zukunft zu beschleunigen.", diff --git a/src/assets/i18n/en_US.json b/src/assets/i18n/en_US.json index a8dda0f66a..9973def8dc 100644 --- a/src/assets/i18n/en_US.json +++ b/src/assets/i18n/en_US.json @@ -31,6 +31,7 @@ "account.address.uk.postalcode.error.regexp": "Please enter a valid UK ZIP code (including the whitespace).", "account.address.us.postalcode.error.regexp": "Please enter a valid US ZIP code.", "account.addresses.add_address.button.text": "Add Address", + "account.addresses.address_updated.message": "Your address has been updated.", "account.addresses.create_address.button.label": "Save Address", "account.addresses.delete_address.text": "Delete this address", "account.addresses.have_no_saved_address.text": "You currently have no saved addresses. Add your invoice and shipping addresses for faster checkout in the future.", diff --git a/src/assets/i18n/fr_FR.json b/src/assets/i18n/fr_FR.json index 25b475687d..c6c0c03031 100644 --- a/src/assets/i18n/fr_FR.json +++ b/src/assets/i18n/fr_FR.json @@ -31,6 +31,7 @@ "account.address.uk.postalcode.error.regexp": "Veuillez entrer un code postal britannique valide (espace inclus).", "account.address.us.postalcode.error.regexp": "Veuillez entrer un code postal des États-Unis valide.", "account.addresses.add_address.button.text": "Ajouter une adresse", + "account.addresses.address_updated.message": "Votre adresse a été mise à jour.", "account.addresses.create_address.button.label": "Enregistrer l’adresse", "account.addresses.delete_address.text": "Supprimer cette adresse", "account.addresses.have_no_saved_address.text": "Actuellement vous n’avez aucune adresse enregistrée. Ajoutez vos adresses de facturation et d’expédition pour un paiement plus rapide à l’avenir.", From 5ba41ecde0dd27b3a28f522188f005d2d22a3b53 Mon Sep 17 00:00:00 2001 From: Silke Date: Wed, 26 Oct 2022 13:33:07 +0200 Subject: [PATCH 13/39] fix: validate email during the checkout process of an anonymous user (#1312) --- ...eckout-address-anonymous.component.spec.ts | 6 +++- .../checkout-address-anonymous.component.ts | 9 ++++-- ...kout-address-anonymous-form.component.html | 4 +-- ...t-address-anonymous-form.component.spec.ts | 2 +- ...eckout-address-anonymous-form.component.ts | 28 +++++++++---------- 5 files changed, 27 insertions(+), 22 deletions(-) diff --git a/src/app/pages/checkout-address/checkout-address-anonymous/checkout-address-anonymous.component.spec.ts b/src/app/pages/checkout-address/checkout-address-anonymous/checkout-address-anonymous.component.spec.ts index d2a81c1cc5..961540f03d 100644 --- a/src/app/pages/checkout-address/checkout-address-anonymous/checkout-address-anonymous.component.spec.ts +++ b/src/app/pages/checkout-address/checkout-address-anonymous/checkout-address-anonymous.component.spec.ts @@ -54,6 +54,8 @@ describe('Checkout Address Anonymous Component', () => { additionalAddressAttributes: fb.group({ email: new FormControl('', Validators.required), taxationID: new FormControl(''), + }), + shipOptions: fb.group({ shipOption: new FormControl('shipToInvoiceAddress'), }), invoiceAddress: fb.group({ @@ -125,9 +127,11 @@ describe('Checkout Address Anonymous Component', () => { it('should create address for valid invoice address form', () => { component.form.get('additionalAddressAttributes').setValue({ taxationID: '', - shipOption: 'shipToInvoiceAddress', email: 'test@intershop.de', }); + component.form.get('shipOptions').setValue({ + shipOption: 'shipToInvoiceAddress', + }); component.submitAddressForm(); fixture.detectChanges(); diff --git a/src/app/pages/checkout-address/checkout-address-anonymous/checkout-address-anonymous.component.ts b/src/app/pages/checkout-address/checkout-address-anonymous/checkout-address-anonymous.component.ts index 43bc07f993..7f1a65927e 100644 --- a/src/app/pages/checkout-address/checkout-address-anonymous/checkout-address-anonymous.component.ts +++ b/src/app/pages/checkout-address/checkout-address-anonymous/checkout-address-anonymous.component.ts @@ -64,10 +64,13 @@ export class CheckoutAddressAnonymousComponent implements OnChanges { this.addressForm.control.get('additionalAddressAttributes').setValue({ taxationID: '', - shipOption: 'shipToInvoiceAddress', email: '', }); + this.addressForm.control.get('shipOptions').setValue({ + shipOption: 'shipToInvoiceAddress', + }); + this.submitted = false; } @@ -88,7 +91,7 @@ export class CheckoutAddressAnonymousComponent implements OnChanges { }; const shippingAddress = - this.form.get('additionalAddressAttributes').value.shipOption === 'shipToInvoiceAddress' + this.form.get('shipOptions').value.shipOption === 'shipToInvoiceAddress' ? undefined : this.form.get('shippingAddress').value.address; @@ -114,6 +117,6 @@ export class CheckoutAddressAnonymousComponent implements OnChanges { } get isShippingAddressFormExpanded() { - return this.form && this.form.get('additionalAddressAttributes').value.shipOption === 'shipToDifferentAddress'; + return this.form && this.form.get('shipOptions').value.shipOption === 'shipToDifferentAddress'; } } diff --git a/src/app/pages/checkout-address/formly/components/checkout-address-anonymous-form/checkout-address-anonymous-form.component.html b/src/app/pages/checkout-address/formly/components/checkout-address-anonymous-form/checkout-address-anonymous-form.component.html index e0aea58658..a0501a6829 100644 --- a/src/app/pages/checkout-address/formly/components/checkout-address-anonymous-form/checkout-address-anonymous-form.component.html +++ b/src/app/pages/checkout-address/formly/components/checkout-address-anonymous-form/checkout-address-anonymous-form.component.html @@ -11,12 +11,12 @@

{{ 'checkout.addresses.billing_address.heading' | transla - +

{{ 'checkout.addresses.shipping_address.heading' | translate }}

- +
diff --git a/src/app/pages/checkout-address/formly/components/checkout-address-anonymous-form/checkout-address-anonymous-form.component.spec.ts b/src/app/pages/checkout-address/formly/components/checkout-address-anonymous-form/checkout-address-anonymous-form.component.spec.ts index aae4c8ac3b..4a4f41e103 100644 --- a/src/app/pages/checkout-address/formly/components/checkout-address-anonymous-form/checkout-address-anonymous-form.component.spec.ts +++ b/src/app/pages/checkout-address/formly/components/checkout-address-anonymous-form/checkout-address-anonymous-form.component.spec.ts @@ -51,7 +51,7 @@ describe('Checkout Address Anonymous Form Component', () => { it('should add shipping address form to parent form, when shipOption is set to shipToDifferentAddress', () => { fixture.detectChanges(); - component.form.get('shipOption').setValue('shipToDifferentAddress'); + component.shipOptionForm.get('shipOption').setValue('shipToDifferentAddress'); fixture.detectChanges(); diff --git a/src/app/pages/checkout-address/formly/components/checkout-address-anonymous-form/checkout-address-anonymous-form.component.ts b/src/app/pages/checkout-address/formly/components/checkout-address-anonymous-form/checkout-address-anonymous-form.component.ts index 61fd2e471c..b103d5cc54 100644 --- a/src/app/pages/checkout-address/formly/components/checkout-address-anonymous-form/checkout-address-anonymous-form.component.ts +++ b/src/app/pages/checkout-address/formly/components/checkout-address-anonymous-form/checkout-address-anonymous-form.component.ts @@ -1,6 +1,6 @@ import { ChangeDetectionStrategy, Component, Input, OnDestroy, OnInit } from '@angular/core'; -import { FormControl, FormGroup } from '@angular/forms'; -import { FormlyFieldConfig, FormlyFormOptions } from '@ngx-formly/core'; +import { FormGroup } from '@angular/forms'; +import { FormlyFieldConfig } from '@ngx-formly/core'; import { Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; @@ -13,15 +13,13 @@ import { FeatureToggleService } from 'ish-core/feature-toggle.module'; }) export class CheckoutAddressAnonymousFormComponent implements OnInit, OnDestroy { @Input() parentForm: FormGroup; - formControl: FormControl; invoiceAddressForm = new FormGroup({}); shippingAddressForm = new FormGroup({}); - form: FormGroup = new FormGroup({}); - - addressFields: FormlyFieldConfig[]; - addressOptions: FormlyFormOptions = {}; + attributesForm: FormGroup = new FormGroup({}); + shipOptionForm: FormGroup = new FormGroup({}); + attributesFields: FormlyFieldConfig[]; shipOptionFields: FormlyFieldConfig[]; isBusinessCustomer = false; @@ -29,13 +27,13 @@ export class CheckoutAddressAnonymousFormComponent implements OnInit, OnDestroy private destroy$ = new Subject(); get isShippingAddressFormExpanded() { - return this.form && this.form.get('shipOption').value === 'shipToDifferentAddress'; + return this.shipOptionForm && this.shipOptionForm.get('shipOption').value === 'shipToDifferentAddress'; } constructor(private featureToggleService: FeatureToggleService) {} ngOnInit() { - this.addressFields = [ + this.attributesFields = [ { type: 'ish-fieldset-field', fieldGroup: [ @@ -45,7 +43,6 @@ export class CheckoutAddressAnonymousFormComponent implements OnInit, OnDestroy templateOptions: { required: true, label: 'checkout.addresses.email.label', - forceRequiredStar: true, customDescription: { key: 'account.address.email.hint', }, @@ -57,7 +54,7 @@ export class CheckoutAddressAnonymousFormComponent implements OnInit, OnDestroy ]; if (this.featureToggleService.enabled('businessCustomerRegistration')) { - this.addressFields = [this.createTaxationIDField(), ...this.addressFields]; + this.attributesFields = [this.createTaxationIDField(), ...this.attributesFields]; this.isBusinessCustomer = true; } @@ -82,14 +79,15 @@ export class CheckoutAddressAnonymousFormComponent implements OnInit, OnDestroy }, ]; this.parentForm.setControl('invoiceAddress', this.invoiceAddressForm); - this.parentForm.setControl('additionalAddressAttributes', this.form); + this.parentForm.setControl('additionalAddressAttributes', this.attributesForm); + this.parentForm.setControl('shipOptions', this.shipOptionForm); // add / remove shipping form if shipTo address option changes this.parentForm - .get('additionalAddressAttributes') + .get('shipOptions') .valueChanges.pipe(takeUntil(this.destroy$)) - .subscribe(attributes => { - attributes.shipOption === 'shipToInvoiceAddress' + .subscribe(options => { + options.shipOption === 'shipToInvoiceAddress' ? this.parentForm.removeControl('shippingAddress') : this.parentForm.setControl('shippingAddress', this.shippingAddressForm); }); From b2c265be8fa609bdb83e845ace3456db28fefb3f Mon Sep 17 00:00:00 2001 From: Silke Date: Wed, 26 Oct 2022 15:16:16 +0200 Subject: [PATCH 14/39] fix: change and display an anonymous user`s email during the checkout process (#1312) --- .../requisition-detail-page.component.html | 2 +- ...kout-address-anonymous-form.component.html | 5 +- ...t-address-anonymous-form.component.spec.ts | 12 ++-- ...eckout-address-anonymous-form.component.ts | 33 ----------- .../checkout-receipt.component.html | 2 +- .../checkout-review.component.html | 2 +- .../basket-address-summary.component.html | 2 +- ...sket-invoice-address-widget.component.html | 5 +- ...t-invoice-address-widget.component.spec.ts | 11 ++-- ...basket-invoice-address-widget.component.ts | 2 + ...ket-shipping-address-widget.component.html | 2 +- ...-shipping-address-widget.component.spec.ts | 16 +++++- ...asket-shipping-address-widget.component.ts | 7 +++ ...rmly-address-extension-form.component.html | 1 + ...y-address-extension-form.component.spec.ts | 53 ++++++++++++++++++ ...formly-address-extension-form.component.ts | 55 +++++++++++++++++++ ...ormly-customer-address-form.component.html | 6 ++ ...ly-customer-address-form.component.spec.ts | 14 ++++- .../formly-customer-address-form.component.ts | 12 ++++ .../formly-address-forms.module.ts | 5 +- 20 files changed, 191 insertions(+), 56 deletions(-) create mode 100644 src/app/shared/formly-address-forms/components/formly-address-extension-form/formly-address-extension-form.component.html create mode 100644 src/app/shared/formly-address-forms/components/formly-address-extension-form/formly-address-extension-form.component.spec.ts create mode 100644 src/app/shared/formly-address-forms/components/formly-address-extension-form/formly-address-extension-form.component.ts diff --git a/projects/requisition-management/src/app/pages/requisition-detail/requisition-detail-page.component.html b/projects/requisition-management/src/app/pages/requisition-detail/requisition-detail-page.component.html index 2f8c02eec0..38e70bde9b 100644 --- a/projects/requisition-management/src/app/pages/requisition-detail/requisition-detail-page.component.html +++ b/projects/requisition-management/src/app/pages/requisition-detail/requisition-detail-page.component.html @@ -46,7 +46,7 @@

{{ 'approval.detailspage.order_details.heading' | translate }}

- + diff --git a/src/app/pages/checkout-address/formly/components/checkout-address-anonymous-form/checkout-address-anonymous-form.component.html b/src/app/pages/checkout-address/formly/components/checkout-address-anonymous-form/checkout-address-anonymous-form.component.html index a0501a6829..f8c9648d3f 100644 --- a/src/app/pages/checkout-address/formly/components/checkout-address-anonymous-form/checkout-address-anonymous-form.component.html +++ b/src/app/pages/checkout-address/formly/components/checkout-address-anonymous-form/checkout-address-anonymous-form.component.html @@ -11,7 +11,10 @@

{{ 'checkout.addresses.billing_address.heading' | transla - +
diff --git a/src/app/pages/checkout-address/formly/components/checkout-address-anonymous-form/checkout-address-anonymous-form.component.spec.ts b/src/app/pages/checkout-address/formly/components/checkout-address-anonymous-form/checkout-address-anonymous-form.component.spec.ts index 4a4f41e103..833db14546 100644 --- a/src/app/pages/checkout-address/formly/components/checkout-address-anonymous-form/checkout-address-anonymous-form.component.spec.ts +++ b/src/app/pages/checkout-address/formly/components/checkout-address-anonymous-form/checkout-address-anonymous-form.component.spec.ts @@ -5,6 +5,7 @@ import { TranslateModule } from '@ngx-translate/core'; import { MockComponent } from 'ng-mocks'; import { FeatureToggleModule } from 'ish-core/feature-toggle.module'; +import { FormlyAddressExtensionFormComponent } from 'ish-shared/formly-address-forms/components/formly-address-extension-form/formly-address-extension-form.component'; import { FormlyAddressFormComponent } from 'ish-shared/formly-address-forms/components/formly-address-form/formly-address-form.component'; import { FormlyTestingModule } from 'ish-shared/formly/dev/testing/formly-testing.module'; @@ -18,7 +19,11 @@ describe('Checkout Address Anonymous Form Component', () => { beforeEach(async () => { await TestBed.configureTestingModule({ - declarations: [CheckoutAddressAnonymousFormComponent, MockComponent(FormlyAddressFormComponent)], + declarations: [ + CheckoutAddressAnonymousFormComponent, + MockComponent(FormlyAddressExtensionFormComponent), + MockComponent(FormlyAddressFormComponent), + ], imports: [ FeatureToggleModule.forTesting('businessCustomerRegistration'), FormlyTestingModule.withPresetMocks(['taxationID']), @@ -43,11 +48,6 @@ describe('Checkout Address Anonymous Form Component', () => { expect(() => fixture.detectChanges()).not.toThrow(); }); - it('should set input field for taxation-id, when businessCustomerRegistration feature is enabled', () => { - fixture.detectChanges(); - expect(component.parentForm.get('additionalAddressAttributes').value).toContainKey('taxationID'); - }); - it('should add shipping address form to parent form, when shipOption is set to shipToDifferentAddress', () => { fixture.detectChanges(); diff --git a/src/app/pages/checkout-address/formly/components/checkout-address-anonymous-form/checkout-address-anonymous-form.component.ts b/src/app/pages/checkout-address/formly/components/checkout-address-anonymous-form/checkout-address-anonymous-form.component.ts index b103d5cc54..ed411c2628 100644 --- a/src/app/pages/checkout-address/formly/components/checkout-address-anonymous-form/checkout-address-anonymous-form.component.ts +++ b/src/app/pages/checkout-address/formly/components/checkout-address-anonymous-form/checkout-address-anonymous-form.component.ts @@ -19,7 +19,6 @@ export class CheckoutAddressAnonymousFormComponent implements OnInit, OnDestroy attributesForm: FormGroup = new FormGroup({}); shipOptionForm: FormGroup = new FormGroup({}); - attributesFields: FormlyFieldConfig[]; shipOptionFields: FormlyFieldConfig[]; isBusinessCustomer = false; @@ -33,28 +32,7 @@ export class CheckoutAddressAnonymousFormComponent implements OnInit, OnDestroy constructor(private featureToggleService: FeatureToggleService) {} ngOnInit() { - this.attributesFields = [ - { - type: 'ish-fieldset-field', - fieldGroup: [ - { - key: 'email', - type: 'ish-email-field', - templateOptions: { - required: true, - label: 'checkout.addresses.email.label', - customDescription: { - key: 'account.address.email.hint', - }, - postWrappers: [{ wrapper: 'description', index: -1 }], - }, - }, - ], - }, - ]; - if (this.featureToggleService.enabled('businessCustomerRegistration')) { - this.attributesFields = [this.createTaxationIDField(), ...this.attributesFields]; this.isBusinessCustomer = true; } @@ -93,17 +71,6 @@ export class CheckoutAddressAnonymousFormComponent implements OnInit, OnDestroy }); } - private createTaxationIDField(): FormlyFieldConfig { - return { - type: 'ish-fieldset-field', - fieldGroup: [ - { - type: '#taxationID', - }, - ], - }; - } - ngOnDestroy() { this.destroy$.next(); this.destroy$.complete(); diff --git a/src/app/pages/checkout-receipt/checkout-receipt/checkout-receipt.component.html b/src/app/pages/checkout-receipt/checkout-receipt/checkout-receipt.component.html index eebebac84e..ac01eff225 100644 --- a/src/app/pages/checkout-receipt/checkout-receipt/checkout-receipt.component.html +++ b/src/app/pages/checkout-receipt/checkout-receipt/checkout-receipt.component.html @@ -13,7 +13,7 @@
- + diff --git a/src/app/pages/checkout-review/checkout-review/checkout-review.component.html b/src/app/pages/checkout-review/checkout-review/checkout-review.component.html index 52ac1c0de7..44d86ae664 100644 --- a/src/app/pages/checkout-review/checkout-review/checkout-review.component.html +++ b/src/app/pages/checkout-review/checkout-review/checkout-review.component.html @@ -48,7 +48,7 @@

editRouterLink="/checkout/address" class="infobox-wrapper col-md-6" > - + diff --git a/src/app/shared/components/basket/basket-address-summary/basket-address-summary.component.html b/src/app/shared/components/basket/basket-address-summary/basket-address-summary.component.html index 78a8666224..d819a54191 100644 --- a/src/app/shared/components/basket/basket-address-summary/basket-address-summary.component.html +++ b/src/app/shared/components/basket/basket-address-summary/basket-address-summary.component.html @@ -7,7 +7,7 @@

{{ 'checkout.address.billing.label' | translate }}
- +
diff --git a/src/app/shared/components/checkout/basket-invoice-address-widget/basket-invoice-address-widget.component.html b/src/app/shared/components/checkout/basket-invoice-address-widget/basket-invoice-address-widget.component.html index 250d64c4ae..8d0ad35d71 100644 --- a/src/app/shared/components/checkout/basket-invoice-address-widget/basket-invoice-address-widget.component.html +++ b/src/app/shared/components/checkout/basket-invoice-address-widget/basket-invoice-address-widget.component.html @@ -13,7 +13,7 @@

{{ 'checkout.address.billing.label' | translate }}

- +

{{ 'checkout.addresses.no_Selection.invoice.error' | translate }} @@ -28,7 +28,7 @@

{{ 'checkout.address.billing.label' | translate }}

-
+

diff --git a/src/app/shared/components/checkout/basket-invoice-address-widget/basket-invoice-address-widget.component.spec.ts b/src/app/shared/components/checkout/basket-invoice-address-widget/basket-invoice-address-widget.component.spec.ts index 683be5da2a..e6b1fba96b 100644 --- a/src/app/shared/components/checkout/basket-invoice-address-widget/basket-invoice-address-widget.component.spec.ts +++ b/src/app/shared/components/checkout/basket-invoice-address-widget/basket-invoice-address-widget.component.spec.ts @@ -31,6 +31,7 @@ describe('Basket Invoice Address Widget Component', () => { accountFacade = mock(AccountFacade); when(accountFacade.addresses$()).thenReturn(EMPTY); + when(accountFacade.isLoggedIn$).thenReturn(of(true)); await TestBed.configureTestingModule({ imports: [FormlyTestingModule, TranslateModule.forRoot()], @@ -160,11 +161,11 @@ describe('Basket Invoice Address Widget Component', () => { component.addresses$.subscribe(addrs => { expect(addrs.map(add => add.id)).toMatchInlineSnapshot(` - Array [ - "3", - "4", - ] - `); + Array [ + "3", + "4", + ] + `); done(); }); }); diff --git a/src/app/shared/components/checkout/basket-invoice-address-widget/basket-invoice-address-widget.component.ts b/src/app/shared/components/checkout/basket-invoice-address-widget/basket-invoice-address-widget.component.ts index 1801a52fa5..73e3543558 100644 --- a/src/app/shared/components/checkout/basket-invoice-address-widget/basket-invoice-address-widget.component.ts +++ b/src/app/shared/components/checkout/basket-invoice-address-widget/basket-invoice-address-widget.component.ts @@ -33,6 +33,7 @@ export class BasketInvoiceAddressWidgetComponent implements OnInit, OnDestroy { invoiceAddress$: Observable
; addresses$: Observable; customerAddresses$: Observable; + isLoggedIn$: Observable; form = new UntypedFormGroup({}); fields: FormlyFieldConfig[]; @@ -64,6 +65,7 @@ export class BasketInvoiceAddressWidgetComponent implements OnInit, OnDestroy { addresses?.filter(address => address.invoiceToAddress).filter(address => address.id !== invoiceAddress?.id) ) ); + this.isLoggedIn$ = this.accountFacade.isLoggedIn$; this.fields = [ { diff --git a/src/app/shared/components/checkout/basket-shipping-address-widget/basket-shipping-address-widget.component.html b/src/app/shared/components/checkout/basket-shipping-address-widget/basket-shipping-address-widget.component.html index f086a07e26..d0d8079ed7 100644 --- a/src/app/shared/components/checkout/basket-shipping-address-widget/basket-shipping-address-widget.component.html +++ b/src/app/shared/components/checkout/basket-shipping-address-widget/basket-shipping-address-widget.component.html @@ -62,7 +62,7 @@

{{ 'checkout.address.shipping.label' | translate }}

-
+
+
+ + + +
+
+ + + + +
+
+ + + +
+
+
+ + + + + {{ option.label }} + {{ option.label }} + + - {{ 'product.available_in_different_configuration' | translate }} + + + diff --git a/src/app/shared/components/product/product-variation-select-enhanced/product-variation-select-enhanced.component.scss b/src/app/shared/components/product/product-variation-select-enhanced/product-variation-select-enhanced.component.scss new file mode 100644 index 0000000000..19b1663aff --- /dev/null +++ b/src/app/shared/components/product/product-variation-select-enhanced/product-variation-select-enhanced.component.scss @@ -0,0 +1,66 @@ +@import 'variables'; + +.variation-select { + width: 100%; + padding-right: 12px; + padding-left: 12px; + overflow: hidden; + text-align: left; + border: 1px solid $gray-400; + + &.dropdown-toggle::after { + position: absolute; + top: 17px; + right: 10px; + } + + .label.selected { + font-family: $font-family-regular; + } +} + +.variation-options { + width: 100%; + + .dropdown-item { + padding-right: 12px; + padding-left: 12px; + } +} + +.mobile-variation-select { + margin: $space-default * 0.5 0 $space-default 0; + font-family: $font-family-regular; + + .mobile-variation-option { + margin-bottom: $space-default * 0.5; + } +} + +span.color-code, +img.image-swatch { + display: inline-block; + width: 24px; + height: 24px; + margin-right: $space-default * 0.5; + vertical-align: middle; +} + +span.color-code { + border: 1px solid transparent; + border-radius: 50%; + + &.light-color { + border: 1px solid $border-color-light; + } +} + +span.label { + font-family: $font-family-regular; + color: $text-color-primary; + text-transform: none; + + &.selected { + font-family: $font-family-bold; + } +} diff --git a/src/app/shared/components/product/product-variation-select-enhanced/product-variation-select-enhanced.component.spec.ts b/src/app/shared/components/product/product-variation-select-enhanced/product-variation-select-enhanced.component.spec.ts new file mode 100644 index 0000000000..1198f369c2 --- /dev/null +++ b/src/app/shared/components/product/product-variation-select-enhanced/product-variation-select-enhanced.component.spec.ts @@ -0,0 +1,134 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { of } from 'rxjs'; +import { anything, capture, instance, mock, spy, verify, when } from 'ts-mockito'; + +import { AppFacade } from 'ish-core/facades/app.facade'; +import { VariationOptionGroup } from 'ish-core/models/product-variation/variation-option-group.model'; + +import { ProductVariationSelectEnhancedComponent } from './product-variation-select-enhanced.component'; + +describe('Product Variation Select Enhanced Component', () => { + let component: ProductVariationSelectEnhancedComponent; + let fixture: ComponentFixture; + let element: HTMLElement; + let appFacade: AppFacade; + + const group_colorCode = { + id: 'color', + attributeType: 'defaultAndColorCode', + options: [ + { value: 'black', label: 'Black', metaData: '000000', active: true }, + { value: 'white', label: 'White', metaData: 'FFFFFF' }, + ], + } as VariationOptionGroup; + + const group_swatchImage = { + id: 'swatch', + attributeType: 'defaultAndSwatchImage', + options: [ + { value: 'Y', label: 'yyy', metaData: 'imageY.png' }, + { value: 'Z', label: 'zzz', metaData: 'imageZ.png', active: true }, + ], + } as VariationOptionGroup; + + beforeEach(async () => { + appFacade = mock(AppFacade); + await TestBed.configureTestingModule({ + declarations: [ProductVariationSelectEnhancedComponent], + providers: [{ provide: AppFacade, useFactory: () => instance(appFacade) }], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ProductVariationSelectEnhancedComponent); + component = fixture.componentInstance; + element = fixture.nativeElement; + component.group = group_colorCode; + component.uuid = 'uuid'; + }); + + it('should be created', () => { + expect(component).toBeTruthy(); + expect(element).toBeTruthy(); + expect(() => fixture.detectChanges()).not.toThrow(); + }); + + it('should render a color code select when the attribute type is "defaultAndColorCode" for mobile', () => { + component.group = group_colorCode; + fixture.detectChanges(); + expect(element).toMatchInlineSnapshot(` +
+
+ Black +
+
+ White +
+
+ `); + }); + + it('should render a swatch image select when the attribute type is "defaultAndColorCode" for desktop', () => { + when(appFacade.deviceType$).thenReturn(of('desktop')); + component.group = group_swatchImage; + fixture.detectChanges(); + expect(element).toMatchInlineSnapshot(` +
+ +
+ +
+
+ `); + }); + + it('should trigger changeOption output handler if color code element is clicked (mobile)', () => { + component.group = group_colorCode; + fixture.detectChanges(); + const emitter = spy(component.changeOption); + const link = fixture.debugElement.query(By.css('.label.selected')).parent.nativeElement; + link.dispatchEvent(new Event('click')); + + verify(emitter.emit(anything())).once(); + const [arg] = capture(emitter.emit).last(); + expect(arg).toMatchInlineSnapshot(` + Object { + "group": "color", + "value": "black", + } + `); + }); + + it('should trigger changeOption output handler if swatch image element is clicked (desktop)', () => { + when(appFacade.deviceType$).thenReturn(of('desktop')); + component.group = group_swatchImage; + fixture.detectChanges(); + const emitter = spy(component.changeOption); + const link = fixture.debugElement.queryAll(By.css('.label.selected')).pop().parent.nativeElement; + link.dispatchEvent(new Event('click')); + + verify(emitter.emit(anything())).once(); + const [arg] = capture(emitter.emit).last(); + expect(arg).toMatchInlineSnapshot(` + Object { + "group": "swatch", + "value": "Z", + } + `); + }); +}); diff --git a/src/app/shared/components/product/product-variation-select-enhanced/product-variation-select-enhanced.component.ts b/src/app/shared/components/product/product-variation-select-enhanced/product-variation-select-enhanced.component.ts new file mode 100644 index 0000000000..ece21caa84 --- /dev/null +++ b/src/app/shared/components/product/product-variation-select-enhanced/product-variation-select-enhanced.component.ts @@ -0,0 +1,32 @@ +import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { Observable } from 'rxjs'; + +import { AppFacade } from 'ish-core/facades/app.facade'; +import { VariationOptionGroup } from 'ish-core/models/product-variation/variation-option-group.model'; +import { DeviceType } from 'ish-core/models/viewtype/viewtype.types'; + +@Component({ + selector: 'ish-product-variation-select-enhanced', + templateUrl: './product-variation-select-enhanced.component.html', + styleUrls: ['./product-variation-select-enhanced.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ProductVariationSelectEnhancedComponent implements OnInit { + @Input() group: VariationOptionGroup; + @Input() uuid: string; + @Input() multipleOptions: boolean; + + @Output() changeOption = new EventEmitter<{ group: string; value: string }>(); + + deviceType$: Observable; + + constructor(private appFacade: AppFacade) {} + + ngOnInit() { + this.deviceType$ = this.appFacade.deviceType$; + } + + optionChange(group: string, value: string) { + this.changeOption.emit({ group, value }); + } +} diff --git a/src/app/shared/components/product/product-variation-select-swatch/product-variation-select-swatch.component.html b/src/app/shared/components/product/product-variation-select-swatch/product-variation-select-swatch.component.html new file mode 100644 index 0000000000..4fba65af2e --- /dev/null +++ b/src/app/shared/components/product/product-variation-select-swatch/product-variation-select-swatch.component.html @@ -0,0 +1,12 @@ + diff --git a/src/app/shared/components/product/product-variation-select-swatch/product-variation-select-swatch.component.scss b/src/app/shared/components/product/product-variation-select-swatch/product-variation-select-swatch.component.scss new file mode 100644 index 0000000000..9c1cf2f785 --- /dev/null +++ b/src/app/shared/components/product/product-variation-select-swatch/product-variation-select-swatch.component.scss @@ -0,0 +1,36 @@ +@import 'variables'; + +ul { + padding: 0; + margin: 10px 0; + + li { + display: inline-block; + margin-right: 5px; + border: 1px solid transparent; + border-radius: 50%; + + &.selected { + border: 1px solid $border-color-light; + } + + a { + display: block; + border: 1px solid transparent; + border-radius: 50%; + + span, + img { + display: block; + width: 32px; + height: 32px; + border: 1px solid transparent; + border-radius: 50%; + + &.light-color { + border: 1px solid $border-color-light; + } + } + } + } +} diff --git a/src/app/shared/components/product/product-variation-select-swatch/product-variation-select-swatch.component.spec.ts b/src/app/shared/components/product/product-variation-select-swatch/product-variation-select-swatch.component.spec.ts new file mode 100644 index 0000000000..3944981abc --- /dev/null +++ b/src/app/shared/components/product/product-variation-select-swatch/product-variation-select-swatch.component.spec.ts @@ -0,0 +1,116 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { anything, capture, spy, verify } from 'ts-mockito'; + +import { VariationOptionGroup } from 'ish-core/models/product-variation/variation-option-group.model'; + +import { ProductVariationSelectSwatchComponent } from './product-variation-select-swatch.component'; + +describe('Product Variation Select Swatch Component', () => { + let component: ProductVariationSelectSwatchComponent; + let fixture: ComponentFixture; + let element: HTMLElement; + + const group_colorCode = { + id: 'color', + attributeType: 'colorCode', + options: [ + { value: 'black', label: 'Black', metaData: '000000', active: true }, + { value: 'white', label: 'White', metaData: 'FFFFFF' }, + ], + } as VariationOptionGroup; + + const group_swatchImage = { + id: 'swatch', + attributeType: 'swatchImage', + options: [ + { value: 'Y', label: 'yyy', metaData: 'imageY.png' }, + { value: 'Z', label: 'zzz', metaData: 'imageZ.png', active: true }, + ], + } as VariationOptionGroup; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ProductVariationSelectSwatchComponent], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ProductVariationSelectSwatchComponent); + component = fixture.componentInstance; + element = fixture.nativeElement; + component.group = group_colorCode; + }); + + it('should be created', () => { + expect(component).toBeTruthy(); + expect(element).toBeTruthy(); + expect(() => fixture.detectChanges()).not.toThrow(); + }); + + it('should render a color code when the attribute type is "colorCode"', () => { + component.group = group_colorCode; + fixture.detectChanges(); + expect(element).toMatchInlineSnapshot(` +
    +
  • + +
  • +
  • + +
  • +
+ `); + }); + + it('should render a swatch image when the attribute type is "swatchImage"', () => { + component.group = group_swatchImage; + fixture.detectChanges(); + expect(element).toMatchInlineSnapshot(` +
    +
  • + yyy +
  • +
  • + zzz +
  • +
+ `); + }); + + it('should trigger changeOption output handler if color code element is clicked', () => { + component.group = group_colorCode; + fixture.detectChanges(); + const emitter = spy(component.changeOption); + const link = fixture.debugElement.query(By.css('li.selected a')).nativeElement; + link.dispatchEvent(new Event('click')); + + verify(emitter.emit(anything())).once(); + const [arg] = capture(emitter.emit).last(); + expect(arg).toMatchInlineSnapshot(` + Object { + "group": "color", + "value": "black", + } + `); + }); + + it('should trigger changeOption output handler if swatch image element is clicked', () => { + component.group = group_swatchImage; + fixture.detectChanges(); + const emitter = spy(component.changeOption); + const link = fixture.debugElement.query(By.css('li.selected a')).nativeElement; + link.dispatchEvent(new Event('click')); + + verify(emitter.emit(anything())).once(); + const [arg] = capture(emitter.emit).last(); + expect(arg).toMatchInlineSnapshot(` + Object { + "group": "swatch", + "value": "Z", + } + `); + }); +}); diff --git a/src/app/shared/components/product/product-variation-select-swatch/product-variation-select-swatch.component.ts b/src/app/shared/components/product/product-variation-select-swatch/product-variation-select-swatch.component.ts new file mode 100644 index 0000000000..799c2381c1 --- /dev/null +++ b/src/app/shared/components/product/product-variation-select-swatch/product-variation-select-swatch.component.ts @@ -0,0 +1,19 @@ +import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; + +import { VariationOptionGroup } from 'ish-core/models/product-variation/variation-option-group.model'; + +@Component({ + selector: 'ish-product-variation-select-swatch', + templateUrl: './product-variation-select-swatch.component.html', + styleUrls: ['./product-variation-select-swatch.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ProductVariationSelectSwatchComponent { + @Input() group: VariationOptionGroup; + + @Output() changeOption = new EventEmitter<{ group: string; value: string }>(); + + optionChange(group: string, value: string) { + this.changeOption.emit({ group, value }); + } +} diff --git a/src/app/shared/components/product/product-variation-select/product-variation-select.component.html b/src/app/shared/components/product/product-variation-select/product-variation-select.component.html index f5246033b9..b651096143 100644 --- a/src/app/shared/components/product/product-variation-select/product-variation-select.component.html +++ b/src/app/shared/components/product/product-variation-select/product-variation-select.component.html @@ -2,27 +2,37 @@
- - + + + + + + +
diff --git a/src/app/shared/components/product/product-variation-select/product-variation-select.component.spec.ts b/src/app/shared/components/product/product-variation-select/product-variation-select.component.spec.ts index e1750c6661..0cd27ba792 100644 --- a/src/app/shared/components/product/product-variation-select/product-variation-select.component.spec.ts +++ b/src/app/shared/components/product/product-variation-select/product-variation-select.component.spec.ts @@ -1,13 +1,15 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { By } from '@angular/platform-browser'; -import { TranslateModule } from '@ngx-translate/core'; +import { MockComponent } from 'ng-mocks'; import { of } from 'rxjs'; import { anything, capture, instance, mock, verify, when } from 'ts-mockito'; import { ProductContextFacade } from 'ish-core/facades/product-context.facade'; import { ProductView } from 'ish-core/models/product-view/product-view.model'; import { VariationProduct, VariationProductMaster } from 'ish-core/models/product/product.model'; -import { findAllDataTestingIDs } from 'ish-core/utils/dev/html-query-utils'; +import { findAllCustomElements } from 'ish-core/utils/dev/html-query-utils'; +import { ProductVariationSelectDefaultComponent } from 'ish-shared/components/product/product-variation-select-default/product-variation-select-default.component'; +import { ProductVariationSelectEnhancedComponent } from 'ish-shared/components/product/product-variation-select-enhanced/product-variation-select-enhanced.component'; +import { ProductVariationSelectSwatchComponent } from 'ish-shared/components/product/product-variation-select-swatch/product-variation-select-swatch.component'; import { ProductVariationSelectComponent } from './product-variation-select.component'; @@ -19,17 +21,26 @@ describe('Product Variation Select Component', () => { const productMaster = { variationAttributeValues: [ - { variationAttributeId: 'a1', value: 'A' }, - { variationAttributeId: 'a1', value: 'B' }, - { variationAttributeId: 'a2', value: 'C' }, - { variationAttributeId: 'a2', value: 'D' }, + { variationAttributeId: 'a1', value: 'A', attributeType: 'colorCode' }, + { variationAttributeId: 'a1', value: 'B', attributeType: 'colorCode' }, + { variationAttributeId: 'a2', value: 'C', attributeType: 'defaultAndColorCode' }, + { variationAttributeId: 'a2', value: 'D', attributeType: 'defaultAndColorCode' }, + { variationAttributeId: 'a3', value: 'E', attributeType: 'swatchImage' }, + { variationAttributeId: 'a3', value: 'F', attributeType: 'swatchImage' }, + { variationAttributeId: 'a4', value: 'G', attributeType: 'defaultAndSwatchImage' }, + { variationAttributeId: 'a4', value: 'H', attributeType: 'defaultAndSwatchImage' }, + { variationAttributeId: 'a5', value: 'I', attributeType: 'default' }, + { variationAttributeId: 'a5', value: 'J', attributeType: 'default' }, ], } as VariationProductMaster; const variationProduct = { variableVariationAttributes: [ - { variationAttributeId: 'a1', value: 'B' }, - { variationAttributeId: 'a2', value: 'D' }, + { variationAttributeId: 'a1', value: 'B', attributeType: 'colorCode' }, + { variationAttributeId: 'a2', value: 'D', attributeType: 'defaultAndColorCode' }, + { variationAttributeId: 'a3', value: 'F', attributeType: 'swatchImage' }, + { variationAttributeId: 'a4', value: 'H', attributeType: 'defaultAndSwatchImage' }, + { variationAttributeId: 'a5', value: 'J', attributeType: 'default' }, ], } as VariationProduct; @@ -42,8 +53,12 @@ describe('Product Variation Select Component', () => { beforeEach(async () => { context = mock(ProductContextFacade); await TestBed.configureTestingModule({ - imports: [TranslateModule.forRoot()], - declarations: [ProductVariationSelectComponent], + declarations: [ + MockComponent(ProductVariationSelectDefaultComponent), + MockComponent(ProductVariationSelectEnhancedComponent), + MockComponent(ProductVariationSelectSwatchComponent), + ProductVariationSelectComponent, + ], providers: [{ provide: ProductContextFacade, useFactory: () => instance(context) }], }).compileComponents(); }); @@ -63,45 +78,30 @@ describe('Product Variation Select Component', () => { expect(() => fixture.detectChanges()).not.toThrow(); }); - it('should initialize form of option groups', () => { + it('should render the different attribute types with the fitting product variation select components', () => { fixture.detectChanges(); - expect(findAllDataTestingIDs(fixture)).toMatchInlineSnapshot(` + expect(findAllCustomElements(element)).toMatchInlineSnapshot(` Array [ - "a1", - "a1-A", - "a1-B", - "a2", - "a2-C", - "a2-D", + "ish-product-variation-select-swatch", + "ish-product-variation-select-enhanced", + "ish-product-variation-select-swatch", + "ish-product-variation-select-enhanced", + "ish-product-variation-select-default", ] `); }); - it('should set active values for form', () => { + it('should trigger a contex value change if value changes', () => { fixture.detectChanges(); - expect(fixture.debugElement.query(By.css('select[data-testing-id=a1]')).nativeElement.value).toMatchInlineSnapshot( - `"B"` - ); - - expect(fixture.debugElement.query(By.css('select[data-testing-id=a2]')).nativeElement.value).toMatchInlineSnapshot( - `"D"` - ); - }); - - it('should trigger value change if value changes', () => { - fixture.detectChanges(); - - const select = fixture.debugElement.query(By.css('select')).nativeElement; - select.value = 'A'; - select.dispatchEvent(new Event('change')); + component.optionChange({ group: 'a2', value: 'C' }); verify(context.changeVariationOption(anything(), anything())).once(); expect(capture(context.changeVariationOption).last()).toMatchInlineSnapshot(` Array [ - "a1", - "A", + "a2", + "C", ] `); }); diff --git a/src/app/shared/components/product/product-variation-select/product-variation-select.component.ts b/src/app/shared/components/product/product-variation-select/product-variation-select.component.ts index e183dd0464..6714e7f261 100644 --- a/src/app/shared/components/product/product-variation-select/product-variation-select.component.ts +++ b/src/app/shared/components/product/product-variation-select/product-variation-select.component.ts @@ -26,7 +26,7 @@ export class ProductVariationSelectComponent implements OnInit { this.visible$ = this.context.select('displayProperties', 'variations'); } - optionChange(group: string, target: EventTarget) { - this.context.changeVariationOption(group, (target as HTMLDataElement).value); + optionChange(event: { group: string; value: string }) { + this.context.changeVariationOption(event.group, event.value); } } diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index 60bff6e572..5cab0ada2f 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -129,6 +129,9 @@ import { ProductRowComponent } from './components/product/product-row/product-ro import { ProductShipmentComponent } from './components/product/product-shipment/product-shipment.component'; import { ProductTileComponent } from './components/product/product-tile/product-tile.component'; import { ProductVariationDisplayComponent } from './components/product/product-variation-display/product-variation-display.component'; +import { ProductVariationSelectDefaultComponent } from './components/product/product-variation-select-default/product-variation-select-default.component'; +import { ProductVariationSelectEnhancedComponent } from './components/product/product-variation-select-enhanced/product-variation-select-enhanced.component'; +import { ProductVariationSelectSwatchComponent } from './components/product/product-variation-select-swatch/product-variation-select-swatch.component'; import { ProductVariationSelectComponent } from './components/product/product-variation-select/product-variation-select.component'; import { ProductsListComponent } from './components/product/products-list/products-list.component'; import { PromotionDetailsComponent } from './components/promotion/promotion-details/promotion-details.component'; @@ -207,6 +210,7 @@ const declaredComponents = [ LineItemEditDialogComponent, LineItemListElementComponent, LoginModalComponent, + PagingComponent, ProductChooseVariationComponent, ProductIdComponent, ProductItemVariationsComponent, @@ -247,9 +251,9 @@ const exportedComponents = [ ErrorMessageComponent, FilterNavigationComponent, IdentityProviderLoginComponent, - InPlaceEditComponent, InfoBoxComponent, InfoMessageComponent, + InPlaceEditComponent, LineItemListComponent, LoadingComponent, LoginFormComponent, @@ -274,9 +278,12 @@ const exportedComponents = [ ProductQuantityComponent, ProductQuantityLabelComponent, ProductShipmentComponent, + ProductsListComponent, ProductVariationDisplayComponent, ProductVariationSelectComponent, - ProductsListComponent, + ProductVariationSelectDefaultComponent, + ProductVariationSelectEnhancedComponent, + ProductVariationSelectSwatchComponent, PromotionDetailsComponent, PromotionRemoveComponent, SearchBoxComponent, @@ -285,7 +292,7 @@ const exportedComponents = [ @NgModule({ imports: [...importExportModules], - declarations: [...declaredComponents, ...exportedComponents, PagingComponent], + declarations: [...declaredComponents, ...exportedComponents], exports: [...exportedComponents, ...importExportModules], }) export class SharedModule { diff --git a/src/styles/global/forms.scss b/src/styles/global/forms.scss index 1fe359cf0e..ad601008d7 100644 --- a/src/styles/global/forms.scss +++ b/src/styles/global/forms.scss @@ -127,6 +127,10 @@ input.form-check-input { color: $text-color-quaternary; } +select { + font-family: $font-family-regular; +} + // ERROR and SUCCESS .has-feedback { .form-control[type='number'] { diff --git a/src/styles/pages/category/product-list.scss b/src/styles/pages/category/product-list.scss index da5c4e15de..ac7931512d 100644 --- a/src/styles/pages/category/product-list.scss +++ b/src/styles/pages/category/product-list.scss @@ -23,6 +23,10 @@ width: 60%; text-align: left; + @include media-breakpoint-down(sm) { + width: 100%; + } + &.read-only { text-align: center; } From 4e02efd4c8a12cd325d8ed853d4f8c258b8d9445 Mon Sep 17 00:00:00 2001 From: Marcel Eisentraut Date: Tue, 18 Oct 2022 11:44:45 +0200 Subject: [PATCH 35/39] feat: use ICM '/token' REST endpoint for authentication (#1156) * old token behavior with the token exchange for each request is replaced by the exchange via token API * get tokens for an anonymous user and for a user with credentials * refresh token before it is expired * revoke token on logout BREAKING CHANGES: PWA uses the ICM `/token` REST endpoint to retrieve user token, every anonymous user will get a anonymous user token, every identity provider has to configure the `oAuthService` with information about the token endpoint, before expiration the given token should be refreshed. --- docs/guides/migrations.md | 6 + e2e/cypress/e2e/pages/account/login.page.ts | 4 +- e2e/cypress/e2e/pages/header.module.ts | 9 +- .../account/register-user.b2b.e2e-spec.ts | 8 +- .../system/cookie-consent.b2c.e2e-spec.ts | 4 +- .../retain-authentication.b2c.e2e-spec.ts | 32 +++- src/app/core/facades/account.facade.ts | 15 +- src/app/core/identity-provider.module.ts | 26 ++- .../auth0.identity-provider.spec.ts | 8 +- .../auth0.identity-provider.ts | 164 ++++++++++------ .../icm.identity-provider.spec.ts | 98 ++++++++++ .../icm.identity-provider.ts | 82 ++++++-- .../identity-provider.factory.ts | 8 + .../identity-provider.interceptor.ts | 2 +- src/app/core/models/token/token.interface.ts | 26 +++ src/app/core/services/api/api.service.ts | 42 ++-- .../core/services/user/user.service.spec.ts | 93 ++++++--- src/app/core/services/user/user.service.ts | 69 +++++-- .../customer/basket/basket.effects.spec.ts | 6 +- .../store/customer/customer-store.spec.ts | 16 +- .../core/store/customer/user/user.actions.ts | 6 + .../store/customer/user/user.effects.spec.ts | 85 ++++++++- .../core/store/customer/user/user.effects.ts | 60 +++++- .../core/store/customer/user/user.reducer.ts | 16 +- .../core/utils/api-token/api-token.service.ts | 179 ++++++++++++------ .../http-error/login-user.error-handler.ts | 11 +- src/app/core/utils/meta-reducers.spec.ts | 10 +- src/app/core/utils/meta-reducers.ts | 4 +- .../oauth-configuration.service.spec.ts | 75 ++++++++ .../oauth-configuration.service.ts | 50 +++++ .../punchout-identity-provider.spec.ts | 55 ++++-- .../punchout-identity-provider.ts | 170 +++++++++++------ 32 files changed, 1118 insertions(+), 321 deletions(-) create mode 100644 src/app/core/identity-provider/icm.identity-provider.spec.ts create mode 100644 src/app/core/models/token/token.interface.ts create mode 100644 src/app/core/utils/oauth-configuration/oauth-configuration.service.spec.ts create mode 100644 src/app/core/utils/oauth-configuration/oauth-configuration.service.ts diff --git a/docs/guides/migrations.md b/docs/guides/migrations.md index c14da7f746..3b816b37b0 100644 --- a/docs/guides/migrations.md +++ b/docs/guides/migrations.md @@ -30,6 +30,12 @@ To handle the different variation select rendering types the existing `ProductVa The rendering and behavior of the existing `ProductVariationSelectComponent` as a standard select box was moved to the new `ProductVariationSelectDefaultComponent`. A `ProductVariationSelectSwatchComponent` for colorCode and swatchImage variation select rendering and a `ProductVariationSelectEnhancedComponent` for a select box rendering with color codes or swatch images and a mobile optimization were added. +The user authentication process has changed. +User authentication tokens are requested from the ICM server using the `/token` REST endpoint now. +Regarding this, the logout action triggers a service, which revokes the current available access token on the ICM backend. +If the logout was successful, then all personalized information is removed from the ngrx store. +Please use `logoutUser({ revokeToken: false })` from the account facade or dispatch `logoutUserSuccess` instead of the `logoutUser` action to use the old behavior. + ## 3.0 to 3.1 The SSR environment variable 'ICM_IDENTITY_PROVIDER' will be removed in a future release ( PWA 5.0 ). diff --git a/e2e/cypress/e2e/pages/account/login.page.ts b/e2e/cypress/e2e/pages/account/login.page.ts index 4915d803dd..4aa4307296 100644 --- a/e2e/cypress/e2e/pages/account/login.page.ts +++ b/e2e/cypress/e2e/pages/account/login.page.ts @@ -21,12 +21,12 @@ export class LoginPage { } submit() { - cy.intercept('GET', /.*\/customers\/-.*/).as('currentCustomer'); + cy.intercept('POST', /.*\/token/).as('token'); cy.wait(500); cy.get('button[name="login"]').click(); - return cy.wait('@currentCustomer'); + return cy.wait('@token'); } get errorText() { diff --git a/e2e/cypress/e2e/pages/header.module.ts b/e2e/cypress/e2e/pages/header.module.ts index cc344ae5c7..ef06eb0a6d 100644 --- a/e2e/cypress/e2e/pages/header.module.ts +++ b/e2e/cypress/e2e/pages/header.module.ts @@ -26,7 +26,7 @@ export class HeaderModule { gotoLoginPage(wait: () => unknown = waitLoadingEnd) { cy.scrollTo('top'); - cy.get('[data-testing-id="user-status-desktop"] .my-account-login').click(); + this.loginLink.click(); wait(); } @@ -51,8 +51,13 @@ export class HeaderModule { wait(); } - logout() { + logout(wait: () => unknown = waitLoadingEnd) { cy.get('[data-testing-id="link-logout"]').first().click(); + wait(); + } + + get loginLink() { + return cy.get('[data-testing-id="user-status-desktop"] .my-account-login'); } get myAccountLink() { diff --git a/e2e/cypress/e2e/specs/account/register-user.b2b.e2e-spec.ts b/e2e/cypress/e2e/specs/account/register-user.b2b.e2e-spec.ts index 792d10ab9f..b321a7e190 100644 --- a/e2e/cypress/e2e/specs/account/register-user.b2b.e2e-spec.ts +++ b/e2e/cypress/e2e/specs/account/register-user.b2b.e2e-spec.ts @@ -37,10 +37,16 @@ describe('New B2B User', () => { }); }); - it('should log out and log in and log out again', () => { + it('should log out', () => { at(MyAccountPage, page => { page.header.logout(); }); + at(HomePage, page => { + page.header.loginLink.should('be.visible'); + }); + }); + + it('should log in and log out again', () => { at(HomePage, page => { page.header.gotoLoginPage(); }); diff --git a/e2e/cypress/e2e/specs/system/cookie-consent.b2c.e2e-spec.ts b/e2e/cypress/e2e/specs/system/cookie-consent.b2c.e2e-spec.ts index c791ec35d3..8b8e35d1a8 100644 --- a/e2e/cypress/e2e/specs/system/cookie-consent.b2c.e2e-spec.ts +++ b/e2e/cypress/e2e/specs/system/cookie-consent.b2c.e2e-spec.ts @@ -17,10 +17,12 @@ describe('Cookie Consent', () => { it('should accept all cookies', () => { at(HomePage, () => { cy.get('[data-testing-id="acceptAllButton"]').click(); - cy.wait(3000); + cy.wait(4000); cy.getCookies().then(cookies => { expect(cookies[cookies.length - 1]).to.have.property('name', 'cookieConsent'); + cy.wait(500); + cy.get('.cookies-banner').should('not.exist'); }); }); }); diff --git a/e2e/cypress/e2e/specs/system/retain-authentication.b2c.e2e-spec.ts b/e2e/cypress/e2e/specs/system/retain-authentication.b2c.e2e-spec.ts index 06fae38a38..9a7b8f109e 100644 --- a/e2e/cypress/e2e/specs/system/retain-authentication.b2c.e2e-spec.ts +++ b/e2e/cypress/e2e/specs/system/retain-authentication.b2c.e2e-spec.ts @@ -26,32 +26,52 @@ describe('Returning User', () => { at(MyAccountPage, page => page.header.myAccountLink.should('have.text', `${_.user.firstName} ${_.user.lastName}`) ); - cy.getCookie('apiToken').should('not.be.empty'); + cy.getCookie('apiToken') + .should('not.be.empty') + .should(cookie => { + cy.wrap(JSON.parse(decodeURIComponent(cookie.value))).should('have.property', 'type', 'user'); + }); }); it('should stay logged in when refreshing page once', () => { MyAccountPage.navigateTo(); at(MyAccountPage); - cy.getCookie('apiToken').should('not.be.empty'); + cy.getCookie('apiToken') + .should('not.be.empty') + .should(cookie => { + cy.wrap(JSON.parse(decodeURIComponent(cookie.value))).should('have.property', 'type', 'user'); + }); }); it('should stay logged in when refreshing page twice', () => { MyAccountPage.navigateTo(); at(MyAccountPage); - cy.getCookie('apiToken').should('not.be.empty'); + cy.getCookie('apiToken') + .should('not.be.empty') + .should(cookie => { + cy.wrap(JSON.parse(decodeURIComponent(cookie.value))).should('have.property', 'type', 'user'); + }); }); it('should stay logged in when refreshing page thrice', () => { MyAccountPage.navigateTo(); at(MyAccountPage); - cy.getCookie('apiToken').should('not.be.empty'); + cy.getCookie('apiToken') + .should('not.be.empty') + .should(cookie => { + cy.wrap(JSON.parse(decodeURIComponent(cookie.value))).should('have.property', 'type', 'user'); + }); }); - it('should log out and loose the cookie', () => { + it('should log out and get the anonymous token', () => { at(MyAccountPage, page => page.header.logout()); at(HomePage); // eslint-disable-next-line unicorn/no-null - cy.getCookie('apiToken').should('equal', null); + cy.getCookie('apiToken') + .should('not.be.empty') + .should(cookie => { + cy.wrap(JSON.parse(decodeURIComponent(cookie.value))).should('have.property', 'type', 'anonymous'); + }); }); }); diff --git a/src/app/core/facades/account.facade.ts b/src/app/core/facades/account.facade.ts index 0f886259be..9d9dcf429a 100644 --- a/src/app/core/facades/account.facade.ts +++ b/src/app/core/facades/account.facade.ts @@ -38,6 +38,7 @@ import { import { createUser, deleteUserPaymentInstrument, + fetchAnonymousUserToken, getCustomerApprovalEmail, getLoggedInCustomer, getLoggedInUser, @@ -53,6 +54,7 @@ import { loginUser, loginUserWithToken, logoutUser, + logoutUserSuccess, requestPasswordReminder, resetPasswordReminder, updateCustomer, @@ -92,8 +94,17 @@ export class AccountFacade { this.store.dispatch(loginUserWithToken({ token })); } - logoutUser() { - this.store.dispatch(logoutUser()); + /** + * Trigger logout action + * + * @param revokeToken option to revoke api token on server side before logout success action is dispatched + */ + logoutUser(options = { revokeApiToken: true }) { + options?.revokeApiToken ? this.store.dispatch(logoutUser()) : this.store.dispatch(logoutUserSuccess()); + } + + fetchAnonymousToken() { + this.store.dispatch(fetchAnonymousUserToken()); } createUser(body: CustomerRegistrationType) { diff --git a/src/app/core/identity-provider.module.ts b/src/app/core/identity-provider.module.ts index 1686259cb4..a5f9ebe2bd 100644 --- a/src/app/core/identity-provider.module.ts +++ b/src/app/core/identity-provider.module.ts @@ -1,7 +1,7 @@ import { HttpHandler, HttpRequest } from '@angular/common/http'; -import { ModuleWithProviders, NgModule } from '@angular/core'; +import { APP_INITIALIZER, ModuleWithProviders, NgModule } from '@angular/core'; import { OAuthModule, OAuthStorage } from 'angular-oauth2-oidc'; -import { noop } from 'rxjs'; +import { BehaviorSubject, noop, of, race, timer } from 'rxjs'; import { PunchoutIdentityProviderModule } from '../extensions/punchout/identity-provider/punchout-identity-provider.module'; @@ -9,6 +9,7 @@ import { Auth0IdentityProvider } from './identity-provider/auth0.identity-provid import { ICMIdentityProvider } from './identity-provider/icm.identity-provider'; import { IDENTITY_PROVIDER_IMPLEMENTOR, IdentityProviderFactory } from './identity-provider/identity-provider.factory'; import { IdentityProviderCapabilities } from './identity-provider/identity-provider.interface'; +import { OAuthConfigurationService } from './utils/oauth-configuration/oauth-configuration.service'; /** * provider factory for storage @@ -20,9 +21,23 @@ export function storageFactory(): OAuthStorage { } } +/** + * load configuration object for OAuth Service + * OAuth Service should be configured, when app is initialized + */ +function loadOAuthConfig(configService: OAuthConfigurationService) { + return () => race(configService.loadConfig$, timer(4000)); +} + @NgModule({ imports: [OAuthModule.forRoot({ resourceServer: { sendAccessToken: false } }), PunchoutIdentityProviderModule], providers: [ + { + provide: APP_INITIALIZER, + useFactory: loadOAuthConfig, + deps: [OAuthConfigurationService], + multi: true, + }, { provide: OAuthStorage, useFactory: storageFactory }, { provide: IDENTITY_PROVIDER_IMPLEMENTOR, @@ -63,6 +78,13 @@ export class IdentityProviderModule { getType: () => 'ICM', }, }, + { + provide: OAuthConfigurationService, + useValue: { + loadConfig$: of({}), + config$: new BehaviorSubject({}), + }, + }, ], }; } diff --git a/src/app/core/identity-provider/auth0.identity-provider.spec.ts b/src/app/core/identity-provider/auth0.identity-provider.spec.ts index 54a0290c5f..ad3dbc4c5e 100644 --- a/src/app/core/identity-provider/auth0.identity-provider.spec.ts +++ b/src/app/core/identity-provider/auth0.identity-provider.spec.ts @@ -4,7 +4,7 @@ import { Router } from '@angular/router'; import { RouterTestingModule } from '@angular/router/testing'; import { MockStore, provideMockStore } from '@ngrx/store/testing'; import { OAuthService } from 'angular-oauth2-oidc'; -import { of } from 'rxjs'; +import { BehaviorSubject, of } from 'rxjs'; import { anything, capture, instance, mock, resetCalls, spy, verify, when } from 'ts-mockito'; import { Customer } from 'ish-core/models/customer/customer.model'; @@ -13,6 +13,7 @@ import { ApiService } from 'ish-core/services/api/api.service'; import { getSsoRegistrationCancelled, getSsoRegistrationRegistered } from 'ish-core/store/customer/sso-registration'; import { getLoggedInCustomer, getUserAuthorized, getUserLoading } from 'ish-core/store/customer/user'; import { ApiTokenService } from 'ish-core/utils/api-token/api-token.service'; +import { OAuthConfigurationService } from 'ish-core/utils/oauth-configuration/oauth-configuration.service'; import { Auth0Config, Auth0IdentityProvider } from './auth0.identity-provider'; @@ -35,6 +36,7 @@ describe('Auth0 Identity Provider', () => { const oAuthService = mock(OAuthService); const apiService = mock(ApiService); const apiTokenService = mock(ApiTokenService); + const oAuthConfigurationService = mock(OAuthConfigurationService); let auth0IdentityProvider: Auth0IdentityProvider; let store$: MockStore; let storeSpy$: MockStore; @@ -55,6 +57,7 @@ describe('Auth0 Identity Provider', () => { { provide: ApiService, useFactory: () => instance(apiService) }, { provide: ApiTokenService, useFactory: () => instance(apiTokenService) }, { provide: APP_BASE_HREF, useValue: baseHref }, + { provide: OAuthConfigurationService, useFactory: () => instance(oAuthConfigurationService) }, { provide: OAuthService, useFactory: () => instance(oAuthService) }, provideMockStore(), ], @@ -67,7 +70,7 @@ describe('Auth0 Identity Provider', () => { }); beforeEach(() => { - when(apiTokenService.restore$(anything())).thenReturn(of(true)); + when(apiTokenService.restore$(anything(), anything())).thenReturn(of(true)); when(oAuthService.getIdToken()).thenReturn(idToken); when(oAuthService.loadDiscoveryDocumentAndTryLogin()).thenReturn( new Promise((res, _) => { @@ -75,6 +78,7 @@ describe('Auth0 Identity Provider', () => { }) ); when(oAuthService.state).thenReturn(undefined); + when(oAuthConfigurationService.config$).thenReturn(new BehaviorSubject({})); when(apiService.post(anything(), anything())).thenReturn(of(userData)); }); diff --git a/src/app/core/identity-provider/auth0.identity-provider.ts b/src/app/core/identity-provider/auth0.identity-provider.ts index ca5e392bd3..e668ab38a9 100644 --- a/src/app/core/identity-provider/auth0.identity-provider.ts +++ b/src/app/core/identity-provider/auth0.identity-provider.ts @@ -4,7 +4,7 @@ import { Inject, Injectable } from '@angular/core'; import { ActivatedRouteSnapshot, Router } from '@angular/router'; import { Store, select } from '@ngrx/store'; import { OAuthService } from 'angular-oauth2-oidc'; -import { Observable, combineLatest, from, of, race, timer } from 'rxjs'; +import { BehaviorSubject, Observable, combineLatest, from, of, race, timer } from 'rxjs'; import { catchError, filter, first, map, switchMap, take, tap } from 'rxjs/operators'; import { HttpError } from 'ish-core/models/http-error/http-error.model'; @@ -18,9 +18,10 @@ import { loadUserByAPIToken, } from 'ish-core/store/customer/user'; import { ApiTokenService } from 'ish-core/utils/api-token/api-token.service'; -import { whenTruthy } from 'ish-core/utils/operators'; +import { OAuthConfigurationService } from 'ish-core/utils/oauth-configuration/oauth-configuration.service'; +import { delayUntil, whenTruthy } from 'ish-core/utils/operators'; -import { IdentityProvider, TriggerReturnType } from './identity-provider.interface'; +import { IdentityProvider, IdentityProviderCapabilities, TriggerReturnType } from './identity-provider.interface'; export interface Auth0Config { type: 'auth0'; @@ -30,16 +31,21 @@ export interface Auth0Config { @Injectable({ providedIn: 'root' }) export class Auth0IdentityProvider implements IdentityProvider { + // emits true, when OAuth Service is successfully configured + // used as an additional condition to check that the OAuth Service is configured before OAuth Service actions are used + private oAuthServiceConfigured$ = new BehaviorSubject(false); + constructor( - private oauthService: OAuthService, private apiService: ApiService, private store: Store, private router: Router, private apiTokenService: ApiTokenService, + private oauthService: OAuthService, + private configService: OAuthConfigurationService, @Inject(APP_BASE_HREF) private baseHref: string ) {} - getCapabilities() { + getCapabilities(): IdentityProviderCapabilities { return { editPassword: false, editEmail: false, @@ -50,60 +56,78 @@ export class Auth0IdentityProvider implements IdentityProvider { init(config: Auth0Config) { const effectiveOrigin = this.baseHref === '/' ? window.location.origin : window.location.origin + this.baseHref; - this.oauthService.configure({ - // Your Auth0 app's domain - // Important: Don't forget to start with https:// AND the trailing slash! - issuer: `https://${config.domain}/`, + // use internal OAuth configuration service for tokenEndpoint configuration + this.configService.config$.pipe(whenTruthy(), take(1)).subscribe(serviceConf => { + this.oauthService.configure({ + // Your Auth0 app's domain + // Important: Don't forget to start with https:// AND the trailing slash! + issuer: `https://${config.domain}/`, - // The app's clientId configured in Auth0 - clientId: config.clientID, + // The app's clientId configured in Auth0 + clientId: config.clientID, - // The app's redirectUri configured in Auth0 - redirectUri: `${effectiveOrigin}/loading`, + // The app's redirectUri configured in Auth0 + redirectUri: `${effectiveOrigin}/loading`, - // logout redirect URL - postLogoutRedirectUri: effectiveOrigin, + // logout redirect URL + postLogoutRedirectUri: effectiveOrigin, - // Scopes ("rights") the Angular application wants get delegated - // https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims - scope: 'openid email profile offline_access', + // Scopes ("rights") the Angular application wants get delegated + // https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims + scope: 'openid email profile offline_access', - // Using Authorization Code Flow - // (PKCE is activated by default for authorization code flow) - responseType: 'code', + // Using Authorization Code Flow + // (PKCE is activated by default for authorization code flow) + responseType: 'code', - // Your Auth0 account's logout url - // Derive it from your application's domain - logoutUrl: `https://${config.domain}/v2/logout`, + // Your Auth0 account's logout url + // Derive it from your application's domain + logoutUrl: `https://${config.domain}/v2/logout`, - sessionChecksEnabled: true, + sessionChecksEnabled: true, + + // ICM token endpoint to retrieve a valid token for an anonymous user + tokenEndpoint: serviceConf?.tokenEndpoint, + + requireHttps: serviceConf?.requireHttps, + }); + this.oauthService.setupAutomaticSilentRefresh(); + this.oAuthServiceConfigured$.next(true); }); - this.oauthService.setupAutomaticSilentRefresh(); - this.apiTokenService - .restore$(['user', 'order']) + + // OAuth Service should be configured before apiToken informations are restored + this.oAuthServiceConfigured$ .pipe( - switchMap(() => from(this.oauthService.loadDiscoveryDocumentAndTryLogin())), + whenTruthy(), + take(1), switchMap(() => - timer(0, 200).pipe( - map(() => this.oauthService.getIdToken()), - take(100), + // anonymous user token should only be fetched when no user is logged in + this.apiTokenService.restore$(['user', 'order'], !this.oauthService.getIdToken()).pipe( + delayUntil(this.oAuthServiceConfigured$), + switchMap(() => from(this.oauthService.loadDiscoveryDocumentAndTryLogin())), + switchMap(() => + timer(0, 200).pipe( + map(() => this.oauthService.getIdToken()), + take(100), + whenTruthy(), + take(1) + ) + ), whenTruthy(), - take(1) + switchMap(idToken => { + const inviteUserId = window.sessionStorage.getItem('invite-userid'); + const inviteHash = window.sessionStorage.getItem('invite-hash'); + return inviteUserId && inviteHash + ? this.inviteRegistration(idToken, inviteUserId, inviteHash).pipe( + tap(() => { + window.sessionStorage.removeItem('invite-userid'); + window.sessionStorage.removeItem('invite-hash'); + }) + ) + : this.normalSignInRegistration(idToken); + }) ) - ), - whenTruthy(), - switchMap(idToken => { - const inviteUserId = window.sessionStorage.getItem('invite-userid'); - const inviteHash = window.sessionStorage.getItem('invite-hash'); - return inviteUserId && inviteHash - ? this.inviteRegistration(idToken, inviteUserId, inviteHash).pipe( - tap(() => { - window.sessionStorage.removeItem('invite-userid'); - window.sessionStorage.removeItem('invite-hash'); - }) - ) - : this.normalSignInRegistration(idToken); - }) + ) ) .subscribe(() => { this.apiTokenService.removeApiToken(); @@ -196,27 +220,45 @@ export class Auth0IdentityProvider implements IdentityProvider { if (route.queryParamMap.get('userid')) { return of(true); } else { - this.router.navigateByUrl('/loading'); - return this.oauthService.loadDiscoveryDocumentAndLogin({ - state: route.queryParams.returnUrl, - }); + return this.oAuthServiceConfigured$.pipe( + whenTruthy(), + take(1), + tap(() => { + this.router.navigateByUrl('/loading'); + this.oauthService.loadDiscoveryDocumentAndLogin({ + state: route.queryParams.returnUrl, + }); + }) + ); } } triggerLogin(route: ActivatedRouteSnapshot): TriggerReturnType { - this.router.navigateByUrl('/loading'); - return this.oauthService.loadDiscoveryDocumentAndLogin({ - state: route.queryParams.returnUrl, - }); + return this.oAuthServiceConfigured$.pipe( + whenTruthy(), + take(1), + tap(() => { + this.router.navigateByUrl('/loading'); + this.oauthService.loadDiscoveryDocumentAndLogin({ + state: route.queryParams.returnUrl, + }); + }) + ); } triggerInvite(route: ActivatedRouteSnapshot): TriggerReturnType { - this.router.navigateByUrl('/loading'); - window.sessionStorage.setItem('invite-userid', route.queryParams.uid); - window.sessionStorage.setItem('invite-hash', route.queryParams.Hash); - return this.oauthService.loadDiscoveryDocumentAndLogin({ - state: route.queryParams.returnUrl, - }); + return this.oAuthServiceConfigured$.pipe( + whenTruthy(), + take(1), + tap(() => { + this.router.navigateByUrl('/loading'); + window.sessionStorage.setItem('invite-userid', route.queryParams.uid); + window.sessionStorage.setItem('invite-hash', route.queryParams.Hash); + this.oauthService.loadDiscoveryDocumentAndLogin({ + state: route.queryParams.returnUrl, + }); + }) + ); } triggerLogout(): TriggerReturnType { diff --git a/src/app/core/identity-provider/icm.identity-provider.spec.ts b/src/app/core/identity-provider/icm.identity-provider.spec.ts new file mode 100644 index 0000000000..c15f38cbf5 --- /dev/null +++ b/src/app/core/identity-provider/icm.identity-provider.spec.ts @@ -0,0 +1,98 @@ +import { TestBed } from '@angular/core/testing'; +import { Router, UrlTree } from '@angular/router'; +import { MockStore, provideMockStore } from '@ngrx/store/testing'; +import { OAuthService } from 'angular-oauth2-oidc'; +import { BehaviorSubject, Observable, Subject, of } from 'rxjs'; +import { anything, instance, mock, resetCalls, spy, verify, when } from 'ts-mockito'; + +import { AccountFacade } from 'ish-core/facades/account.facade'; +import { selectQueryParam } from 'ish-core/store/core/router'; +import { ApiTokenService } from 'ish-core/utils/api-token/api-token.service'; +import { OAuthConfigurationService } from 'ish-core/utils/oauth-configuration/oauth-configuration.service'; + +import { ICMIdentityProvider } from './icm.identity-provider'; + +describe('Icm Identity Provider', () => { + const apiTokenService = mock(ApiTokenService); + const accountFacade = mock(AccountFacade); + const oAuthConfigurationService = mock(OAuthConfigurationService); + + let icmIdentityProvider: ICMIdentityProvider; + let store$: MockStore; + let router: Router; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + { provide: AccountFacade, useFactory: () => instance(accountFacade) }, + { provide: ApiTokenService, useFactory: () => instance(apiTokenService) }, + + { provide: OAuthConfigurationService, useFactory: () => instance(oAuthConfigurationService) }, + { provide: OAuthService, useFactory: () => instance(mock(OAuthService)) }, + + provideMockStore(), + ], + }).compileComponents(); + + icmIdentityProvider = TestBed.inject(ICMIdentityProvider); + router = TestBed.inject(Router); + store$ = TestBed.inject(MockStore); + }); + + beforeEach(() => { + when(apiTokenService.restore$()).thenReturn(of(true)); + when(apiTokenService.cookieVanishes$).thenReturn(new Subject()); + when(oAuthConfigurationService.config$).thenReturn(new BehaviorSubject({})); + + resetCalls(apiTokenService); + resetCalls(accountFacade); + + window.sessionStorage.clear(); + }); + + describe('init', () => { + it('should restore apiToken on startup', () => { + icmIdentityProvider.init(); + verify(apiTokenService.restore$()).once(); + verify(apiTokenService.removeApiToken()).never(); + }); + }); + + describe('triggerLogout', () => { + beforeEach(() => { + when(accountFacade.isLoggedIn$).thenReturn(of(false)); + store$.overrideSelector(selectQueryParam(anything()), undefined); + icmIdentityProvider.init(); + }); + + it('should remove api token on logout', done => { + const logoutTrigger$ = icmIdentityProvider.triggerLogout() as Observable; + + logoutTrigger$.subscribe(() => { + verify(accountFacade.logoutUser()).once(); + done(); + }); + }); + + it('should return to home page', done => { + const routerSpy = spy(router); + + const logoutTrigger$ = icmIdentityProvider.triggerLogout() as Observable; + + logoutTrigger$.subscribe(() => { + verify(routerSpy.parseUrl('/home')).once(); + done(); + }); + }); + }); + + describe('triggerLogin', () => { + beforeEach(() => { + icmIdentityProvider.init(); + }); + + it('should always return true without any further functionality', () => { + expect(icmIdentityProvider.triggerLogin()).toBeTrue(); + }); + }); +}); diff --git a/src/app/core/identity-provider/icm.identity-provider.ts b/src/app/core/identity-provider/icm.identity-provider.ts index c10c5922e4..f0c9862a73 100644 --- a/src/app/core/identity-provider/icm.identity-provider.ts +++ b/src/app/core/identity-provider/icm.identity-provider.ts @@ -2,18 +2,32 @@ import { HttpEvent, HttpHandler, HttpRequest } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { ActivatedRouteSnapshot, Router } from '@angular/router'; import { Store, select } from '@ngrx/store'; -import { Observable, noop } from 'rxjs'; -import { map } from 'rxjs/operators'; +import { OAuthService } from 'angular-oauth2-oidc'; +import { BehaviorSubject, Observable, merge, noop } from 'rxjs'; +import { filter, map, switchMap, take, tap, withLatestFrom } from 'rxjs/operators'; +import { AccountFacade } from 'ish-core/facades/account.facade'; import { selectQueryParam } from 'ish-core/store/core/router'; -import { logoutUser } from 'ish-core/store/customer/user'; import { ApiTokenService } from 'ish-core/utils/api-token/api-token.service'; +import { OAuthConfigurationService } from 'ish-core/utils/oauth-configuration/oauth-configuration.service'; +import { whenTruthy } from 'ish-core/utils/operators'; import { IdentityProvider, TriggerReturnType } from './identity-provider.interface'; @Injectable({ providedIn: 'root' }) export class ICMIdentityProvider implements IdentityProvider { - constructor(protected router: Router, protected store: Store, protected apiTokenService: ApiTokenService) {} + // emits true, when OAuth Service is successfully configured + // used as an additional condition to check that the OAuth Service is configured before OAuth Service actions are used + private oAuthServiceConfigured$ = new BehaviorSubject(false); + + constructor( + private router: Router, + private store: Store, + private apiTokenService: ApiTokenService, + private accountFacade: AccountFacade, + private oAuthService: OAuthService, + private configService: OAuthConfigurationService + ) {} getCapabilities() { return { @@ -24,16 +38,34 @@ export class ICMIdentityProvider implements IdentityProvider { } init() { - this.apiTokenService.restore$().subscribe(noop); - - this.apiTokenService.cookieVanishes$.subscribe(type => { - this.store.dispatch(logoutUser()); - if (type === 'user') { - this.router.navigate(['/login'], { - queryParams: { returnUrl: this.router.url, messageKey: 'session_timeout' }, - }); - } + // OAuth Service should be configured by internal OAuth configuration service + this.configService.config$.pipe(whenTruthy(), take(1)).subscribe(config => { + this.oAuthService.configure(config); + this.oAuthServiceConfigured$.next(true); }); + + this.apiTokenService.cookieVanishes$ + .pipe(withLatestFrom(this.apiTokenService.apiToken$)) + .subscribe(([type, apiToken]) => { + this.accountFacade.logoutUser({ revokeApiToken: false }); + if (!apiToken) { + this.accountFacade.fetchAnonymousToken(); + } + if (type === 'user') { + this.router.navigate(['/login'], { + queryParams: { returnUrl: this.router.url, messageKey: 'session_timeout' }, + }); + } + }); + + // OAuth Service should be configured before apiToken informations are restored and the refresh token mechanism is setup + this.oAuthServiceConfigured$ + .pipe( + whenTruthy(), + take(1), + switchMap(() => merge(this.apiTokenService.restore$(), this.configService.setupRefreshTokenMechanism$())) + ) + .subscribe(noop); } triggerLogin(): TriggerReturnType { @@ -41,12 +73,24 @@ export class ICMIdentityProvider implements IdentityProvider { } triggerLogout(): TriggerReturnType { - this.apiTokenService.removeApiToken(); - this.store.dispatch(logoutUser()); - return this.store.pipe( - select(selectQueryParam('returnUrl')), - map(returnUrl => returnUrl || '/home'), - map(returnUrl => this.router.parseUrl(returnUrl)) + return this.oAuthServiceConfigured$.pipe( + whenTruthy(), + take(1), + tap(() => this.accountFacade.logoutUser()), // user will be logged out and related refresh token is revoked on server + switchMap(() => + this.accountFacade.isLoggedIn$.pipe( + // wait until the user is logged out before you go to homepage to prevent unnecessary REST calls + filter(loggedIn => !loggedIn), + take(1), + switchMap(() => + this.store.pipe( + select(selectQueryParam('returnUrl')), + map(returnUrl => returnUrl || '/home'), + map(returnUrl => this.router.parseUrl(returnUrl)) + ) + ) + ) + ) ); } diff --git a/src/app/core/identity-provider/identity-provider.factory.ts b/src/app/core/identity-provider/identity-provider.factory.ts index 0a68d74d74..efc6098eca 100644 --- a/src/app/core/identity-provider/identity-provider.factory.ts +++ b/src/app/core/identity-provider/identity-provider.factory.ts @@ -1,5 +1,6 @@ import { Injectable, InjectionToken, Injector, Type } from '@angular/core'; import { Store, select } from '@ngrx/store'; +import { once } from 'lodash-es'; import { noop } from 'rxjs'; import { first } from 'rxjs/operators'; @@ -57,7 +58,14 @@ export class IdentityProviderFactory { } } + private logNoIdpError = once(() => + console.error('No identity provider instance exists. Please double-check your configuration:', this.config) + ); + getInstance() { + if (!this.instance) { + this.logNoIdpError(); + } return this.instance; } diff --git a/src/app/core/interceptors/identity-provider.interceptor.ts b/src/app/core/interceptors/identity-provider.interceptor.ts index 5fb4a72928..a01d019f0b 100644 --- a/src/app/core/interceptors/identity-provider.interceptor.ts +++ b/src/app/core/interceptors/identity-provider.interceptor.ts @@ -12,7 +12,7 @@ export class IdentityProviderInterceptor implements HttpInterceptor { intercept(req: HttpRequest, next: HttpHandler): Observable> { // TODO: check if this works with PROXY_ICM if (req.url.startsWith(this.appFacade.icmBaseUrl)) { - return this.identityProviderFactory.getInstance().intercept(req, next); + return this.identityProviderFactory.getInstance()?.intercept(req, next) ?? next.handle(req); } return next.handle(req); } diff --git a/src/app/core/models/token/token.interface.ts b/src/app/core/models/token/token.interface.ts new file mode 100644 index 0000000000..195b688125 --- /dev/null +++ b/src/app/core/models/token/token.interface.ts @@ -0,0 +1,26 @@ +/** + * return correct token options for given grantType (NOTE: 'anonymous' grant type has no token options) + */ +export type FetchTokenOptions = T extends 'password' + ? FetchTokenPasswordOptions + : T extends 'client_credentials' + ? FetchTokenClientCredentialsOptions + : FetchTokenRefreshTokenOptions; + +interface FetchTokenPasswordOptions { + username: string; + password: string; + organization?: string; +} + +interface FetchTokenClientCredentialsOptions { + username: string; + password: string; + organization?: string; +} + +interface FetchTokenRefreshTokenOptions { + refresh_token: string; +} + +export type GrantType = 'anonymous' | 'password' | 'client_credentials' | 'refresh_token'; diff --git a/src/app/core/services/api/api.service.ts b/src/app/core/services/api/api.service.ts index 3501500937..1237477ef8 100644 --- a/src/app/core/services/api/api.service.ts +++ b/src/app/core/services/api/api.service.ts @@ -126,30 +126,14 @@ export class ApiService { return httpCall$.pipe(this.handleErrors(!options?.skipApiErrorHandling)); } - private constructUrlForPath(path: string, options?: AvailableOptions): Observable { + constructUrlForPath(path: string, options?: AvailableOptions): Observable { if (path.startsWith('http://') || path.startsWith('https://')) { return of(path); } return combineLatest([ - // base url this.store.pipe(select(getRestEndpoint)), - // locale - options?.sendLocale === undefined || options.sendLocale - ? this.store.pipe( - select(getCurrentLocale), - whenTruthy(), - map(l => `;loc=${l}`) - ) - : of(''), - // currency - options?.sendCurrency === undefined || options.sendCurrency - ? this.store.pipe( - select(getCurrentCurrency), - whenTruthy(), - map(l => `;cur=${l}`) - ) - : of(''), - // first path segment + this.getLocale$(options), + this.getCurrency$(options), of('/'), of(path.includes('/') ? path.split('/')[0] : path), // pgid @@ -165,6 +149,26 @@ export class ApiService { ); } + private getLocale$(options: AvailableOptions): Observable { + return options?.sendLocale === undefined || options.sendLocale + ? this.store.pipe( + select(getCurrentLocale), + whenTruthy(), + map(l => `;loc=${l}`) + ) + : of(''); + } + + private getCurrency$(options: AvailableOptions): Observable { + return options?.sendCurrency === undefined || options.sendCurrency + ? this.store.pipe( + select(getCurrentCurrency), + whenTruthy(), + map(l => `;cur=${l}`) + ) + : of(''); + } + private constructHttpClientParams( path: string, options?: AvailableOptions diff --git a/src/app/core/services/user/user.service.spec.ts b/src/app/core/services/user/user.service.spec.ts index 19cd8562ca..d77b706b2d 100644 --- a/src/app/core/services/user/user.service.spec.ts +++ b/src/app/core/services/user/user.service.spec.ts @@ -1,6 +1,6 @@ -import { HttpHeaders } from '@angular/common/http'; import { TestBed } from '@angular/core/testing'; import { MockStore, provideMockStore } from '@ngrx/store/testing'; +import { OAuthService, TokenResponse } from 'angular-oauth2-oidc'; import { of, throwError } from 'rxjs'; import { anyString, anything, capture, instance, mock, verify, when } from 'ts-mockito'; @@ -13,51 +13,82 @@ import { User } from 'ish-core/models/user/user.model'; import { ApiService, AvailableOptions } from 'ish-core/services/api/api.service'; import { getUserPermissions } from 'ish-core/store/customer/authorization'; import { getLoggedInCustomer, getLoggedInUser } from 'ish-core/store/customer/user'; +import { ApiTokenService } from 'ish-core/utils/api-token/api-token.service'; import { encodeResourceID } from 'ish-core/utils/url-resource-ids'; import { UserService } from './user.service'; describe('User Service', () => { + const token = { + access_token: 'DEMO@access-token', + token_type: 'user', + expires_in: 3600, + refresh_token: 'DEMO@refresh-token', + id_token: 'DEMO@id-token', + } as TokenResponse; + let userService: UserService; let apiServiceMock: ApiService; + let apiTokenServiceMock: ApiTokenService; + let oAuthServiceMock: OAuthService; let appFacade: AppFacade; let store$: MockStore; beforeEach(() => { apiServiceMock = mock(ApiService); + apiTokenServiceMock = mock(ApiTokenService); appFacade = mock(AppFacade); + oAuthServiceMock = mock(OAuthService); + + when(oAuthServiceMock.fetchTokenUsingGrant(anyString(), anything(), anything())).thenResolve(token); + when(appFacade.isAppTypeREST$).thenReturn(of(true)); + when(appFacade.currentLocale$).thenReturn(of('en_US')); + when(appFacade.customerRestResource$).thenReturn(of('customers')); TestBed.configureTestingModule({ providers: [ { provide: ApiService, useFactory: () => instance(apiServiceMock) }, + { provide: ApiTokenService, useFactory: () => instance(apiTokenServiceMock) }, { provide: AppFacade, useFactory: () => instance(appFacade) }, + { provide: OAuthService, useFactory: () => instance(oAuthServiceMock) }, provideMockStore({ selectors: [{ selector: getLoggedInCustomer, value: undefined }] }), ], }); userService = TestBed.inject(UserService); - when(appFacade.isAppTypeREST$).thenReturn(of(true)); - when(appFacade.currentLocale$).thenReturn(of('en_US')); - when(appFacade.customerRestResource$).thenReturn(of('customers')); store$ = TestBed.inject(MockStore); }); describe('SignIn a user', () => { it('should login a user when correct credentials are entered', done => { const loginDetail = { login: 'patricia@test.intershop.de', password: '!InterShop00!' }; + when(apiServiceMock.get('customers/-', anything())).thenReturn( of({ customerNo: 'PC', customerType: 'PRIVATE' } as CustomerData) ); - when(apiServiceMock.get('privatecustomers/-')).thenReturn( + when(apiServiceMock.get('privatecustomers/-', anything())).thenReturn( of({ customerNo: 'PC', customerType: 'PRIVATE' } as CustomerData) ); - when(apiServiceMock.get('personalization')).thenReturn(of({ pgid: '6FGMJtFU2xuRpG9I3CpTS7fc0000' })); + when(apiServiceMock.get('personalization', anything())).thenReturn(of({ pgid: '6FGMJtFU2xuRpG9I3CpTS7fc0000' })); userService.signInUser(loginDetail).subscribe(data => { - const [, options] = capture<{}, { headers: HttpHeaders }>(apiServiceMock.get).first(); - const headers = options?.headers; - expect(headers).toBeTruthy(); - expect(headers.get('Authorization')).toEqual('BASIC cGF0cmljaWFAdGVzdC5pbnRlcnNob3AuZGU6IUludGVyU2hvcDAwIQ=='); + verify(oAuthServiceMock.fetchTokenUsingGrant(anyString(), anything(), anything())).once(); + expect(data).toHaveProperty('customer.customerNo', 'PC'); + expect(data).toHaveProperty('pgid', '6FGMJtFU2xuRpG9I3CpTS7fc0000'); + done(); + }); + }); + it('should not fetch a new token, when credentials are not entered', done => { + when(apiServiceMock.get('customers/-', anything())).thenReturn( + of({ customerNo: 'PC', customerType: 'PRIVATE' } as CustomerData) + ); + when(apiServiceMock.get('privatecustomers/-', anything())).thenReturn( + of({ customerNo: 'PC', customerType: 'PRIVATE' } as CustomerData) + ); + when(apiServiceMock.get('personalization', anything())).thenReturn(of({ pgid: '6FGMJtFU2xuRpG9I3CpTS7fc0000' })); + + userService.signInUser(undefined).subscribe(data => { + verify(oAuthServiceMock.fetchTokenUsingGrant(anyString(), anything(), anything())).never(); expect(data).toHaveProperty('customer.customerNo', 'PC'); expect(data).toHaveProperty('pgid', '6FGMJtFU2xuRpG9I3CpTS7fc0000'); done(); @@ -66,31 +97,34 @@ describe('User Service', () => { it('should login a private user when correct credentials are entered', done => { const loginDetail = { login: 'patricia@test.intershop.de', password: '!InterShop00!' }; + when(apiServiceMock.get('customers/-', anything())).thenReturn( of({ customerNo: 'PC', customerType: 'PRIVATE' } as CustomerData) ); - when(apiServiceMock.get('privatecustomers/-')).thenReturn(of({ customerNo: 'PC' } as CustomerData)); - when(apiServiceMock.get('personalization')).thenReturn(of({ pgid: '123' })); + when(apiServiceMock.get('privatecustomers/-', anything())).thenReturn(of({ customerNo: 'PC' } as CustomerData)); + when(apiServiceMock.get('personalization', anything())).thenReturn(of({ pgid: '123' })); userService.signInUser(loginDetail).subscribe(() => { verify(apiServiceMock.get(`customers/-`, anything())).once(); - verify(apiServiceMock.get(`privatecustomers/-`)).once(); - verify(apiServiceMock.get('personalization')).once(); + verify(apiServiceMock.get(`privatecustomers/-`, anything())).once(); + verify(apiServiceMock.get('personalization', anything())).once(); done(); }); }); it('should login a business user when correct credentials are entered', done => { const loginDetail = { login: 'patricia@test.intershop.de', password: '!InterShop00!' }; - when(apiServiceMock.get(anything(), anything())).thenReturn( + + when(apiServiceMock.get('customers/-', anything())).thenReturn( of({ customerNo: 'PC', customerType: 'SMBCustomer' } as CustomerData) ); - when(apiServiceMock.get('personalization')).thenReturn(of({ pgid: '123' })); + + when(apiServiceMock.get('personalization', anything())).thenReturn(of({ pgid: '123' })); userService.signInUser(loginDetail).subscribe(() => { verify(apiServiceMock.get(`customers/-`, anything())).once(); verify(apiServiceMock.get(`privatecustomers/-`, anything())).never(); - verify(apiServiceMock.get('personalization')).once(); + verify(apiServiceMock.get('personalization', anything())).once(); done(); }); }); @@ -113,12 +147,12 @@ describe('User Service', () => { when(apiServiceMock.get(anything(), anything())).thenReturn( of({ customerNo: '4711', type: 'SMBCustomer', customerType: 'SMBCustomer' } as CustomerData) ); - when(apiServiceMock.get('personalization')).thenReturn(of({ pgid: '1234' })); + when(apiServiceMock.get('personalization', anything())).thenReturn(of({ pgid: '1234' })); userService.signInUserByToken().subscribe(() => { verify(apiServiceMock.get('customers/-', anything())).once(); verify(apiServiceMock.get('privatecustomers/-', anything())).never(); - verify(apiServiceMock.get('personalization')).once(); + verify(apiServiceMock.get('personalization', anything())).once(); const [path] = capture(apiServiceMock.get).first(); expect(path).toEqual('customers/-'); done(); @@ -129,14 +163,13 @@ describe('User Service', () => { when(apiServiceMock.get(anything(), anything())).thenReturn( of({ customerNo: '4711', type: 'SMBCustomer', customerType: 'SMBCustomer' } as CustomerData) ); - when(apiServiceMock.get('personalization')).thenReturn(of({ pgid: '1234' })); + when(apiServiceMock.get('personalization', anything())).thenReturn(of({ pgid: '1234' })); userService.signInUserByToken('12345').subscribe(() => { verify(apiServiceMock.get('customers/-', anything())).once(); verify(apiServiceMock.get('privatecustomers/-', anything())).never(); - verify(apiServiceMock.get('personalization')).once(); - const [path, options] = capture(apiServiceMock.get).first(); - expect(options.headers.get(ApiService.TOKEN_HEADER_KEY)).toMatchInlineSnapshot(`"12345"`); + verify(apiServiceMock.get('personalization', anything())).once(); + const [path] = capture(apiServiceMock.get).first(); expect(path).toEqual('customers/-'); done(); }); @@ -267,6 +300,20 @@ describe('User Service', () => { }); }); + describe('Revoke Api Token', () => { + beforeEach(() => { + when(apiServiceMock.put(anyString())).thenReturn(of({})); + }); + + it("should revoke an existing api token when 'logoutUser' is called", done => { + userService.logoutUser().subscribe(() => { + verify(apiServiceMock.put('token/logout')).once(); + verify(apiTokenServiceMock.removeApiToken()).once(); + done(); + }); + }); + }); + describe('Updates a customer', () => { it('should return an error when called and the customer parameter is missing', done => { when(apiServiceMock.put(anything(), anything())).thenReturn(of({})); diff --git a/src/app/core/services/user/user.service.ts b/src/app/core/services/user/user.service.ts index 56c41a0f50..0ce951adef 100644 --- a/src/app/core/services/user/user.service.ts +++ b/src/app/core/services/user/user.service.ts @@ -1,9 +1,10 @@ import { HttpHeaders } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Store, select } from '@ngrx/store'; +import { OAuthService, TokenResponse } from 'angular-oauth2-oidc'; import { pick } from 'lodash-es'; -import { Observable, combineLatest, forkJoin, of, throwError } from 'rxjs'; -import { concatMap, first, map, switchMap, take, withLatestFrom } from 'rxjs/operators'; +import { Observable, combineLatest, defer, forkJoin, from, of, throwError } from 'rxjs'; +import { concatMap, first, map, switchMap, take, tap, withLatestFrom } from 'rxjs/operators'; import { AppFacade } from 'ish-core/facades/app.facade'; import { Address } from 'ish-core/models/address/address.model'; @@ -19,12 +20,14 @@ import { } from 'ish-core/models/customer/customer.model'; import { PasswordReminderUpdate } from 'ish-core/models/password-reminder-update/password-reminder-update.model'; import { PasswordReminder } from 'ish-core/models/password-reminder/password-reminder.model'; +import { FetchTokenOptions, GrantType } from 'ish-core/models/token/token.interface'; import { UserCostCenter } from 'ish-core/models/user-cost-center/user-cost-center.model'; import { UserMapper } from 'ish-core/models/user/user.mapper'; import { User } from 'ish-core/models/user/user.model'; import { ApiService, AvailableOptions, unpackEnvelope } from 'ish-core/services/api/api.service'; import { getUserPermissions } from 'ish-core/store/customer/authorization'; import { getLoggedInCustomer, getLoggedInUser } from 'ish-core/store/customer/user'; +import { ApiTokenService } from 'ish-core/utils/api-token/api-token.service'; import { whenTruthy } from 'ish-core/utils/operators'; import { encodeResourceID } from 'ish-core/utils/url-resource-ids'; @@ -50,7 +53,13 @@ interface CreateBusinessCustomerType extends Customer { */ @Injectable({ providedIn: 'root' }) export class UserService { - constructor(private apiService: ApiService, private appFacade: AppFacade, private store: Store) {} + constructor( + private apiService: ApiService, + private apiTokenService: ApiTokenService, + private appFacade: AppFacade, + private store: Store, + private oauthService: OAuthService + ) {} /** * Sign in an existing user with the given login credentials (login, password). @@ -61,46 +70,66 @@ export class UserService { * For business customers user data are returned by a separate call (getCompanyUserData). */ signInUser(loginCredentials: Credentials): Observable { - const headers = new HttpHeaders().set( - ApiService.AUTHORIZATION_HEADER_KEY, - `BASIC ${window.btoa(`${loginCredentials.login}:${loginCredentials.password}`)}` + return defer(() => + loginCredentials + ? this.fetchToken('password', { username: loginCredentials.login, password: loginCredentials.password }).pipe( + switchMap(() => this.fetchCustomer()) + ) + : this.fetchCustomer() ); - - return this.fetchCustomer({ headers }); } + /** * Sign in an existing user with the given token or if no token is given, using token stored in cookie. * - * @param token The token that is used to login user. + * @param token The refresh token that is used to login user. * @returns The logged in customer data. * For private customers user data are also returned. * For business customers user data are returned by a separate call (getCompanyUserData). */ signInUserByToken(token?: string): Observable { if (token) { - return this.fetchCustomer({ - headers: new HttpHeaders().set(ApiService.TOKEN_HEADER_KEY, token), - }); + return this.fetchToken('refresh_token', { refresh_token: token }).pipe(switchMap(() => this.fetchCustomer())); } else { return this.fetchCustomer({ skipApiErrorHandling: true }); } } - private fetchCustomer(options?: AvailableOptions): Observable { + private fetchCustomer(options: AvailableOptions = {}): Observable { return this.apiService.get('customers/-', options).pipe( withLatestFrom(this.appFacade.isAppTypeREST$), concatMap(([data, isAppTypeRest]) => forkJoin([ isAppTypeRest && data.customerType === 'PRIVATE' - ? this.apiService.get('privatecustomers/-') + ? this.apiService.get('privatecustomers/-', options) : of(data), - this.apiService.get<{ pgid: string }>('personalization').pipe(map(data => data.pgid)), + this.apiService.get<{ pgid: string }>('personalization', options).pipe(map(data => data.pgid)), ]) ), map(([data, pgid]) => ({ ...CustomerMapper.mapLoginData(data), pgid })) ); } + /** + * Fetches a new user token. Based on the grantType the user has to apply certain token options to the method. + * + * @param grantType The given type ('anonymous', 'password', 'client_credentials', 'refresh_token') is used to specify, how the user token should be fetched. + */ + fetchToken(grantType: T): Observable; + fetchToken>(grantType: T, options: R): Observable; + fetchToken>( + grantType: T, + options?: R + ): Observable { + return from( + this.oauthService.fetchTokenUsingGrant( + grantType, + options ?? {}, + new HttpHeaders({ 'content-type': 'application/x-www-form-urlencoded' }) + ) + ); + } + /** * Creates a new user for the given data. * @@ -164,7 +193,7 @@ export class UserService { .post(AppFacade.getCustomerRestResource(body.customer.isBusinessCustomer, isAppTypeRest), newCustomer, { captcha: pick(body, ['captcha', 'captchaAction']), }) - .pipe(map(() => ({ customer: body.customer, user: body.user }))) + .pipe(map(() => ({ customer: body.customer, user: body.user }))) ) ); } @@ -246,6 +275,14 @@ export class UserService { ); } + /** + * Logs out the current user associated with the specified authentication token. + * All (refresh) tokens issued for this user will expire and become invalid. + */ + logoutUser() { + return this.apiService.put('token/logout').pipe(tap(() => this.apiTokenService.removeApiToken())); + } + /** * Updates the customer data of the (currently logged in) b2b customer. * diff --git a/src/app/core/store/customer/basket/basket.effects.spec.ts b/src/app/core/store/customer/basket/basket.effects.spec.ts index f6fb2b4c97..a49ea6f60b 100644 --- a/src/app/core/store/customer/basket/basket.effects.spec.ts +++ b/src/app/core/store/customer/basket/basket.effects.spec.ts @@ -68,7 +68,11 @@ describe('Basket Effects', () => { RouterTestingModule.withRoutes([{ path: '**', children: [] }]), ], providers: [ - { provide: ApiTokenService, useFactory: () => instance(mock(ApiTokenService)) }, + { + provide: ApiTokenService, + useFactory: () => instance(mock(ApiTokenService)), + useValue: { apiToken$: of({ apiToken: 'apiToken' }) }, + }, { provide: BasketService, useFactory: () => instance(basketServiceMock) }, BasketEffects, provideMockActions(() => actions$), diff --git a/src/app/core/store/customer/customer-store.spec.ts b/src/app/core/store/customer/customer-store.spec.ts index 54d957ee63..bb98cf117c 100644 --- a/src/app/core/store/customer/customer-store.spec.ts +++ b/src/app/core/store/customer/customer-store.spec.ts @@ -1,8 +1,9 @@ import { TestBed } from '@angular/core/testing'; import { RouterTestingModule } from '@angular/router/testing'; import { TranslateModule } from '@ngx-translate/core'; +import { OAuthService, TokenResponse } from 'angular-oauth2-oidc'; import { EMPTY, of } from 'rxjs'; -import { anyNumber, anything, instance, mock, when } from 'ts-mockito'; +import { anyNumber, anyString, anything, instance, mock, when } from 'ts-mockito'; import { Basket } from 'ish-core/models/basket/basket.model'; import { Credentials } from 'ish-core/models/credentials/credentials.model'; @@ -106,6 +107,14 @@ describe('Customer Store', () => { useExternalUrl: false, } as Promotion; + const token = { + access_token: 'DEMO@access-token', + token_type: 'user', + expires_in: 3600, + refresh_token: 'DEMO@refresh-token', + id_token: 'DEMO@id-token', + } as TokenResponse; + beforeEach(() => { const categoriesServiceMock = mock(CategoriesService); when(categoriesServiceMock.getTopLevelCategories(anyNumber())).thenReturn(of(categoryTree())); @@ -140,6 +149,7 @@ describe('Customer Store', () => { const userServiceMock = mock(UserService); when(userServiceMock.signInUser(anything())).thenReturn(of({ customer, user, pgid })); + when(userServiceMock.fetchToken(anyString(), anything())).thenReturn(of(token)); const dataRequestsServiceMock = mock(DataRequestsService); const filterServiceMock = mock(FilterService); @@ -149,6 +159,9 @@ describe('Customer Store', () => { const productPriceServiceMock = mock(PricesService); when(productPriceServiceMock.getProductPrices(anything())).thenReturn(of([])); + const oAuthService = mock(OAuthService); + when(oAuthService.events).thenReturn(of()); + TestBed.configureTestingModule({ imports: [ CoreStoreModule.forTesting(['configuration', 'serverConfig'], true), @@ -174,6 +187,7 @@ describe('Customer Store', () => { { provide: CookiesService, useFactory: () => instance(mock(CookiesService)) }, { provide: DataRequestsService, useFactory: () => instance(dataRequestsServiceMock) }, { provide: FilterService, useFactory: () => instance(filterServiceMock) }, + { provide: OAuthService, useFactory: () => instance(oAuthService) }, { provide: OrderService, useFactory: () => instance(orderServiceMock) }, { provide: PaymentService, useFactory: () => instance(mock(PaymentService)) }, { provide: PricesService, useFactory: () => instance(productPriceServiceMock) }, diff --git a/src/app/core/store/customer/user/user.actions.ts b/src/app/core/store/customer/user/user.actions.ts index d1fe8c0840..fa203d0512 100644 --- a/src/app/core/store/customer/user/user.actions.ts +++ b/src/app/core/store/customer/user/user.actions.ts @@ -25,6 +25,10 @@ export const loadCompanyUserSuccess = createAction('[User API] Load Company User export const logoutUser = createAction('[User] Logout User'); +export const logoutUserSuccess = createAction('[User API] Logout User Success'); + +export const logoutUserFail = createAction('[User API] Logout User Failed', httpError()); + export const createUser = createAction('[User] Create User', payload()); export const createUserSuccess = createAction('[User API] Create User Success', payload<{ email: string }>()); @@ -140,3 +144,5 @@ export const updateUserPasswordByPasswordReminderFail = createAction( '[Password Reminder] Update User Password Failed', httpError() ); + +export const fetchAnonymousUserToken = createAction('[Token API] Fetch Anonymous User Token'); diff --git a/src/app/core/store/customer/user/user.effects.spec.ts b/src/app/core/store/customer/user/user.effects.spec.ts index 1ed707702b..7230e00bea 100644 --- a/src/app/core/store/customer/user/user.effects.spec.ts +++ b/src/app/core/store/customer/user/user.effects.spec.ts @@ -4,6 +4,7 @@ import { Router } from '@angular/router'; import { RouterTestingModule } from '@angular/router/testing'; import { provideMockActions } from '@ngrx/effects/testing'; import { Action, Store } from '@ngrx/store'; +import { OAuthService, TokenResponse } from 'angular-oauth2-oidc'; import { cold, hot } from 'jasmine-marbles'; import { EMPTY, Observable, noop, of, throwError } from 'rxjs'; import { anyString, anything, instance, mock, verify, when } from 'ts-mockito'; @@ -30,6 +31,7 @@ import { deleteUserPaymentInstrument, deleteUserPaymentInstrumentFail, deleteUserPaymentInstrumentSuccess, + fetchAnonymousUserToken, loadCompanyUser, loadCompanyUserFail, loadCompanyUserSuccess, @@ -44,6 +46,9 @@ import { loginUserFail, loginUserSuccess, loginUserWithToken, + logoutUser, + logoutUserFail, + logoutUserSuccess, requestPasswordReminder, requestPasswordReminderFail, requestPasswordReminderSuccess, @@ -67,6 +72,7 @@ describe('User Effects', () => { let userServiceMock: UserService; let paymentServiceMock: PaymentService; let apiTokenServiceMock: ApiTokenService; + let oAuthServiceMock: OAuthService; let router: Router; let location: Location; @@ -83,12 +89,23 @@ describe('User Effects', () => { isBusinessCustomer: true, } as Customer; + const token = { + access_token: 'DEMO@access-token', + token_type: 'user', + expires_in: 3600, + refresh_token: 'DEMO@refresh-token', + id_token: 'DEMO@id-token', + } as TokenResponse; + beforeEach(() => { userServiceMock = mock(UserService); paymentServiceMock = mock(PaymentService); apiTokenServiceMock = mock(ApiTokenService); + oAuthServiceMock = mock(OAuthService); when(userServiceMock.signInUser(anything())).thenReturn(of(loginResponseData)); + when(userServiceMock.fetchToken(anyString(), anything())).thenReturn(of(token)); + when(userServiceMock.fetchToken(anyString())).thenReturn(of(token)); when(userServiceMock.signInUserByToken(anything())).thenReturn(of(loginResponseData)); when(userServiceMock.createUser(anything())).thenReturn(of(undefined)); when(userServiceMock.updateUser(anything(), anything())).thenReturn(of({ firstName: 'Patricia' } as User)); @@ -97,10 +114,12 @@ describe('User Effects', () => { when(userServiceMock.getCompanyUserData()).thenReturn(of({ firstName: 'Patricia' } as User)); when(userServiceMock.requestPasswordReminder(anything())).thenReturn(of({})); when(userServiceMock.getEligibleCostCenters()).thenReturn(of([])); + when(userServiceMock.logoutUser()).thenReturn(of(undefined)); when(paymentServiceMock.getUserPaymentMethods(anything())).thenReturn(of([])); when(paymentServiceMock.createUserPayment(anything(), anything())).thenReturn(of({ id: 'paymentInstrumentId' })); when(paymentServiceMock.deleteUserPaymentInstrument(anyString(), anyString())).thenReturn(of(undefined)); when(apiTokenServiceMock.hasUserApiTokenCookie()).thenReturn(false); + when(oAuthServiceMock.events).thenReturn(of()); TestBed.configureTestingModule({ imports: [ @@ -110,6 +129,7 @@ describe('User Effects', () => { ], providers: [ { provide: ApiTokenService, useFactory: () => instance(apiTokenServiceMock) }, + { provide: OAuthService, useFactory: () => instance(oAuthServiceMock) }, { provide: PaymentService, useFactory: () => instance(paymentServiceMock) }, { provide: UserService, useFactory: () => instance(userServiceMock) }, provideMockActions(() => actions$), @@ -170,6 +190,69 @@ describe('User Effects', () => { }); }); + describe('loginUserWithToken$', () => { + it('should call the api service when LoginUserWithToken event is called', done => { + const action = loginUserWithToken({ token: '12345' }); + + actions$ = of(action); + + effects.loginUserWithToken$.subscribe(() => { + verify(userServiceMock.signInUserByToken(anything())).once(); + done(); + }); + }); + }); + + describe('logoutUser$', () => { + it('should call the api service to revoke current token when logoutUser action is called', done => { + const action = logoutUser(); + + actions$ = of(action); + + effects.logoutUser$.subscribe(() => { + verify(userServiceMock.logoutUser()).once(); + done(); + }); + }); + + it('should dispatch a success action on a successful request and should fetch a new anonymous user token', () => { + const action = logoutUser(); + const completion1 = logoutUserSuccess(); + const completion2 = fetchAnonymousUserToken(); + + actions$ = hot('-a', { a: action }); + const expected$ = cold('-(bc)', { b: completion1, c: completion2 }); + + expect(effects.logoutUser$).toBeObservable(expected$); + }); + + it('should dispatch an error action on a failed request', () => { + const error = makeHttpError({ status: 401, code: 'error' }); + when(userServiceMock.logoutUser()).thenReturn(throwError(() => error)); + + const action = logoutUser(); + const completion = logoutUserFail({ error }); + + actions$ = hot('-a-a-a', { a: action }); + const expected$ = cold('-b-b-b', { b: completion }); + + expect(effects.logoutUser$).toBeObservable(expected$); + }); + }); + + describe('fetchAnonymousUserToken$', () => { + it('should call apiTokenService with token response', done => { + const action = fetchAnonymousUserToken(); + + actions$ = of(action); + + effects.fetchAnonymousUserToken$.subscribe(() => { + verify(userServiceMock.fetchToken('anonymous')).once(); + done(); + }); + }); + }); + describe('loadCompanyUser$', () => { it('should call the registration service for LoadCompanyUser', done => { const action = loadCompanyUser(); @@ -296,7 +379,7 @@ describe('User Effects', () => { const action = createUser({ customer, credentials } as CustomerRegistrationType); const completion1 = createUserSuccess({ email: customerLoginType.user.email }); - const completion2 = loginUserWithToken({ token: undefined }); + const completion2 = loginUser({ credentials }); actions$ = hot('-a', { a: action }); const expected$ = cold('-(bc)', { b: completion1, c: completion2 }); diff --git a/src/app/core/store/customer/user/user.effects.ts b/src/app/core/store/customer/user/user.effects.ts index 612b9498b3..093bda3204 100644 --- a/src/app/core/store/customer/user/user.effects.ts +++ b/src/app/core/store/customer/user/user.effects.ts @@ -3,8 +3,20 @@ import { Router } from '@angular/router'; import { Actions, createEffect, ofType } from '@ngrx/effects'; import { routerNavigatedAction } from '@ngrx/router-store'; import { Store, select } from '@ngrx/store'; +import { OAuthService, OAuthSuccessEvent } from 'angular-oauth2-oidc'; import { from } from 'rxjs'; -import { concatMap, delay, exhaustMap, filter, map, mergeMap, sample, takeWhile, withLatestFrom } from 'rxjs/operators'; +import { + concatMap, + delay, + exhaustMap, + filter, + map, + mergeMap, + sample, + switchMap, + takeWhile, + withLatestFrom, +} from 'rxjs/operators'; import { CustomerRegistrationType } from 'ish-core/models/customer/customer.model'; import { PaymentService } from 'ish-core/services/payment/payment.service'; @@ -24,6 +36,7 @@ import { deleteUserPaymentInstrument, deleteUserPaymentInstrumentFail, deleteUserPaymentInstrumentSuccess, + fetchAnonymousUserToken, loadCompanyUser, loadCompanyUserFail, loadCompanyUserSuccess, @@ -38,6 +51,9 @@ import { loginUserFail, loginUserSuccess, loginUserWithToken, + logoutUser, + logoutUserFail, + logoutUserSuccess, requestPasswordReminder, requestPasswordReminderFail, requestPasswordReminderSuccess, @@ -66,7 +82,8 @@ export class UserEffects { private userService: UserService, private paymentService: PaymentService, private router: Router, - private apiTokenService: ApiTokenService + private apiTokenService: ApiTokenService, + private oAuthService: OAuthService ) {} loginUser$ = createEffect(() => @@ -79,6 +96,43 @@ export class UserEffects { ) ); + /** + * Revoke token on server side + */ + logoutUser$ = createEffect(() => + this.actions$.pipe( + ofType(logoutUser), + switchMap(() => + this.userService.logoutUser().pipe( + concatMap(() => [logoutUserSuccess(), fetchAnonymousUserToken()]), + mapErrorToAction(logoutUserFail) + ) + ) + ) + ); + + fetchAnonymousUserToken$ = createEffect( + () => + this.actions$.pipe( + ofType(fetchAnonymousUserToken), + switchMap(() => this.userService.fetchToken('anonymous')) + ), + { dispatch: false } + ); + + setApiToken$ = createEffect( + () => + this.oAuthService.events.pipe( + filter(event => event instanceof OAuthSuccessEvent && event.type === 'token_received'), + map(() => + this.apiTokenService.setApiToken(this.oAuthService.getAccessToken(), { + expires: new Date(this.oAuthService.getAccessTokenExpiration()), + }) + ) + ), + { dispatch: false } + ); + loginUserWithToken$ = createEffect(() => this.actions$.pipe( ofType(loginUserWithToken), @@ -129,7 +183,7 @@ export class UserEffects { createUserSuccess({ email: createUserResponse.user.email }), customerTypeForLoginApproval?.includes(createUserResponse.customer.isBusinessCustomer ? 'SMB' : 'PRIVATE') ? createUserApprovalRequired({ email: createUserResponse.user.email }) - : loginUserWithToken({ token: undefined }), + : loginUser({ credentials: data.credentials }), ]), mapErrorToAction(createUserFail) ) diff --git a/src/app/core/store/customer/user/user.reducer.ts b/src/app/core/store/customer/user/user.reducer.ts index accf414714..75267cd532 100644 --- a/src/app/core/store/customer/user/user.reducer.ts +++ b/src/app/core/store/customer/user/user.reducer.ts @@ -10,7 +10,9 @@ import { setErrorOn, setLoadingOn, unsetLoadingAndErrorOn, unsetLoadingOn } from import { createUser, + createUserApprovalRequired, createUserFail, + createUserSuccess, deleteUserPaymentInstrument, deleteUserPaymentInstrumentFail, deleteUserPaymentInstrumentSuccess, @@ -26,6 +28,9 @@ import { loadUserPaymentMethodsSuccess, loginUserFail, loginUserSuccess, + logoutUser, + logoutUserFail, + logoutUserSuccess, requestPasswordReminder, requestPasswordReminderFail, requestPasswordReminderSuccess, @@ -43,8 +48,6 @@ import { updateUserPasswordSuccess, updateUserSuccess, userErrorReset, - createUserSuccess, - createUserApprovalRequired, } from './user.actions'; export interface UserState { @@ -95,7 +98,8 @@ export const userReducer = createReducer( loadUserPaymentMethods, deleteUserPaymentInstrument, updateUserPasswordByPasswordReminder, - requestPasswordReminder + requestPasswordReminder, + logoutUser ), unsetLoadingOn( loadUserCostCentersFail, @@ -113,7 +117,8 @@ export const userReducer = createReducer( updateCustomerSuccess, loadUserCostCentersSuccess, loadUserPaymentMethodsSuccess, - deleteUserPaymentInstrumentSuccess + deleteUserPaymentInstrumentSuccess, + logoutUserSuccess ), setErrorOn( updateUserFail, @@ -121,7 +126,8 @@ export const userReducer = createReducer( updateCustomerFail, loadUserPaymentMethodsFail, deleteUserPaymentInstrumentFail, - loadRolesAndPermissionsFail + loadRolesAndPermissionsFail, + logoutUserFail ), on(loginUserFail, loadCompanyUserFail, createUserFail, (_, action): UserState => { const error = action.payload.error; diff --git a/src/app/core/utils/api-token/api-token.service.ts b/src/app/core/utils/api-token/api-token.service.ts index 7a6613ee3e..3aa7b2d4c4 100644 --- a/src/app/core/utils/api-token/api-token.service.ts +++ b/src/app/core/utils/api-token/api-token.service.ts @@ -1,9 +1,21 @@ -import { HttpErrorResponse, HttpEvent, HttpHandler, HttpRequest, HttpResponse } from '@angular/common/http'; +import { HttpErrorResponse, HttpEvent, HttpHandler, HttpParams, HttpRequest, HttpResponse } from '@angular/common/http'; import { ApplicationRef, Injectable } from '@angular/core'; import { Router } from '@angular/router'; import { Store, select } from '@ngrx/store'; +import { CookieOptions } from 'express'; import { isEqual } from 'lodash-es'; -import { Observable, ReplaySubject, Subject, combineLatest, interval, of, race, throwError, timer } from 'rxjs'; +import { + Observable, + OperatorFunction, + ReplaySubject, + Subject, + combineLatest, + interval, + of, + race, + throwError, + timer, +} from 'rxjs'; import { catchError, concatMap, @@ -14,20 +26,27 @@ import { mergeMap, pairwise, skip, + startWith, switchMap, take, - tap, withLatestFrom, } from 'rxjs/operators'; +import { BasketView } from 'ish-core/models/basket/basket.model'; +import { User } from 'ish-core/models/user/user.model'; import { ApiService } from 'ish-core/services/api/api.service'; import { getCurrentBasket, getCurrentBasketId, loadBasket, loadBasketByAPIToken } from 'ish-core/store/customer/basket'; import { getOrder, getSelectedOrderId, loadOrderByAPIToken } from 'ish-core/store/customer/orders'; -import { getLoggedInUser, getUserAuthorized, loadUserByAPIToken } from 'ish-core/store/customer/user'; +import { + fetchAnonymousUserToken, + getLoggedInUser, + getUserAuthorized, + loadUserByAPIToken, +} from 'ish-core/store/customer/user'; import { CookiesService } from 'ish-core/utils/cookies/cookies.service'; import { mapToProperty, whenTruthy } from 'ish-core/utils/operators'; -export type ApiTokenCookieType = 'user' | 'order'; +type ApiTokenCookieType = 'user' | 'order' | 'anonymous'; interface ApiTokenCookie { apiToken: string; @@ -37,11 +56,16 @@ interface ApiTokenCookie { creator?: string; } +// If no expiry date is supplied by the token endpoint, this value (in ms) is used +const DEFAULT_EXPIRY_TIME = 3600000; + @Injectable({ providedIn: 'root' }) export class ApiTokenService { apiToken$ = new ReplaySubject(1); cookieVanishes$ = new Subject(); + private cookieOptions: CookieOptions = {}; + private initialCookie$: Observable; constructor( @@ -50,6 +74,7 @@ export class ApiTokenService { private store: Store, appRef: ApplicationRef ) { + // setup initial values const initialCookie = this.parseCookie(); this.initialCookie$ = of(!SSR ? initialCookie : undefined); this.initialCookie$.pipe(mapToProperty('apiToken')).subscribe(token => { @@ -59,37 +84,18 @@ export class ApiTokenService { if (!SSR) { // save token routine combineLatest([ - store.pipe(select(getLoggedInUser)), + store.pipe(select(getLoggedInUser), startWith(undefined), pairwise()), store.pipe(select(getCurrentBasket)), store.pipe(select(getSelectedOrderId)), - this.apiToken$.pipe(skip(1)), + this.apiToken$, ]) - .pipe( - map(([user, basket, orderId, apiToken]): ApiTokenCookie => { - if (user) { - return { apiToken, type: 'user', isAnonymous: false, creator: 'pwa' }; - } else if (basket) { - return { apiToken, type: 'user', isAnonymous: true, creator: 'pwa' }; - } else if (orderId) { - return { apiToken, type: 'order', orderId, creator: 'pwa' }; - } else { - const apiTokenCookieString = this.cookiesService.get('apiToken'); - const apiTokenCookie: ApiTokenCookie = apiTokenCookieString - ? JSON.parse(apiTokenCookieString) - : undefined; - if (apiToken && apiTokenCookie) { - return { ...apiTokenCookie, apiToken }; - } - } - }), - distinctUntilChanged(isEqual) - ) + .pipe(skip(1), this.mapToApiTokenCookie(), distinctUntilChanged(isEqual)) .subscribe(apiToken => { const cookieContent = apiToken?.apiToken ? JSON.stringify(apiToken) : undefined; if (cookieContent) { cookiesService.put('apiToken', cookieContent, { - expires: new Date(Date.now() + 3600000), - secure: true, + expires: this.cookieOptions?.expires ?? new Date(Date.now() + DEFAULT_EXPIRY_TIME), + secure: this.cookieOptions?.secure ?? true, sameSite: 'Strict', path: '/', }); @@ -98,7 +104,7 @@ export class ApiTokenService { } }); - // token vanishes routine + // access token vanishes routine appRef.isStable .pipe( whenTruthy(), @@ -119,6 +125,37 @@ export class ApiTokenService { this.cookieVanishes$.next(type); }); + // cookie vanishes routine when user is logged out in an another tab + appRef.isStable + .pipe( + whenTruthy(), + first(), + mergeMap(() => + interval(1000).pipe( + map(() => this.parseCookie()), + pairwise(), + filter(([previous, current]) => previous?.type === 'user' && current?.type === 'anonymous'), // user is logged out and got a new token as an anonymous user + switchMap(([previous, current]) => + combineLatest([ + store.pipe(select(getLoggedInUser), startWith(undefined), pairwise()), + store.pipe(select(getCurrentBasket)), + store.pipe(select(getSelectedOrderId)), + this.apiToken$, + ]).pipe( + take(1), + this.mapToApiTokenCookie(), + filter(calculated => calculated?.type === 'user'), // application calculated an user api token cookie although an anonymous cookie is stored + map(() => [previous.type, current.apiToken]) + ) + ) + ) + ) + ) + .subscribe(([type, apiToken]) => { + this.apiToken$.next(apiToken); + this.cookieVanishes$.next(type); + }); + // session keep alive appRef.isStable .pipe( @@ -138,19 +175,51 @@ export class ApiTokenService { } } + private mapToApiTokenCookie(): OperatorFunction<[[User, User], BasketView, string, string], ApiTokenCookie> { + return (source$: Observable<[[User, User], BasketView, string, string]>) => + source$.pipe( + map(([[prevUser, user], basket, orderId, apiToken]): ApiTokenCookie => { + if (user) { + return { apiToken, type: 'user', isAnonymous: false, creator: 'pwa' }; + } else if (basket) { + return { apiToken, type: 'user', isAnonymous: true, creator: 'pwa' }; + } else if (orderId) { + return { apiToken, type: 'order', orderId, creator: 'pwa' }; + } + // user is logged out and is now anonymous + else if (apiToken && !user && prevUser) { + return { apiToken, type: 'anonymous', creator: 'pwa', isAnonymous: true }; + } + + const apiTokenCookieString = this.cookiesService.get('apiToken'); + const apiTokenCookie: ApiTokenCookie = apiTokenCookieString ? JSON.parse(apiTokenCookieString) : undefined; + if (apiToken) { + if (apiTokenCookie) { + return { ...apiTokenCookie, apiToken }; // overwrite existing cookie informations with new apiToken + } + return { apiToken, type: 'anonymous', creator: 'pwa', isAnonymous: true }; // initial api token cookie + } + }) + ); + } + hasUserApiTokenCookie() { const apiTokenCookie = this.parseCookie(); return apiTokenCookie?.type === 'user' && !apiTokenCookie?.isAnonymous; } - restore$(types: ApiTokenCookieType[] = ['user', 'order']): Observable { + restore$(types: ApiTokenCookieType[] = ['user', 'order'], fetchAnonymousToken = true): Observable { if (SSR) { return of(true); } return this.router.events.pipe( first(), switchMap(() => this.initialCookie$), - switchMap(cookie => { + withLatestFrom(this.apiToken$), + switchMap(([cookie, apiToken]) => { + if (!apiToken && fetchAnonymousToken) { + this.store.dispatch(fetchAnonymousUserToken()); + } if (types.includes(cookie?.type)) { switch (cookie?.type) { case 'user': { @@ -192,7 +261,7 @@ export class ApiTokenService { ); } - private parseCookie() { + private parseCookie(): ApiTokenCookie { const cookieContent = this.cookiesService.get('apiToken'); if (cookieContent) { try { @@ -204,17 +273,18 @@ export class ApiTokenService { return; } - private setApiToken(apiToken: string) { - if (!apiToken) { - console.warn('do not use setApiToken to unset token, use remove or invalidate instead'); - } - this.apiToken$.next(apiToken); - } - + /** + * Should remove the actual apiToken cookie and fetch a new anonymous user token + */ removeApiToken() { this.apiToken$.next(undefined); } + setApiToken(apiToken: string, options?: CookieOptions) { + this.cookieOptions = options; + this.apiToken$.next(apiToken); + } + private invalidateApiToken() { const cookie = this.parseCookie(); @@ -231,23 +301,10 @@ export class ApiTokenService { ); } - private setTokenFromResponse(event: HttpEvent) { - if (event instanceof HttpResponse) { - const apiToken = event.headers.get(ApiService.TOKEN_HEADER_KEY); - if (apiToken) { - if (apiToken.startsWith('AuthenticationTokenOutdated') || apiToken.startsWith('AuthenticationTokenInvalid')) { - this.invalidateApiToken(); - } else if (!event.url.endsWith('/configurations') && !event.url.endsWith('/contact')) { - this.setApiToken(apiToken); - } - } - } - } - private appendAuthentication(req: HttpRequest): Observable> { return this.apiToken$.pipe( map(apiToken => - apiToken && !req.headers?.has(ApiService.AUTHORIZATION_HEADER_KEY) + apiToken && !req.headers?.has(ApiService.TOKEN_HEADER_KEY) ? req.clone({ headers: req.headers.set(ApiService.TOKEN_HEADER_KEY, apiToken) }) : req ), @@ -259,6 +316,17 @@ export class ApiTokenService { return this.appendAuthentication(req).pipe( concatMap(request => next.handle(request).pipe( + map(event => { + // remove id_token from /token response + // TODO: remove http request body adaptions if correct id_tokens are returned + if (event instanceof HttpResponse && event.url.endsWith('token') && request.body instanceof HttpParams) { + const { id_token: _, ...body } = event.body; + return event.clone({ + body, + }); + } + return event; + }), catchError(err => { if (this.isAuthTokenError(err)) { this.invalidateApiToken(); @@ -269,8 +337,7 @@ export class ApiTokenService { return timer(500).pipe(switchMap(() => next.handle(retryRequest))); } return throwError(() => err); - }), - tap(event => this.setTokenFromResponse(event)) + }) ) ) ); diff --git a/src/app/core/utils/http-error/login-user.error-handler.ts b/src/app/core/utils/http-error/login-user.error-handler.ts index 0f7665e60f..18c3c49bb3 100644 --- a/src/app/core/utils/http-error/login-user.error-handler.ts +++ b/src/app/core/utils/http-error/login-user.error-handler.ts @@ -1,10 +1,9 @@ -import { HttpErrorResponse, HttpRequest } from '@angular/common/http'; +import { HttpErrorResponse } from '@angular/common/http'; import { Inject, Injectable } from '@angular/core'; import { USER_REGISTRATION_LOGIN_TYPE } from 'ish-core/configurations/injection-keys'; import { SpecialHttpErrorHandler } from 'ish-core/interceptors/icm-error-mapper.interceptor'; import { HttpError } from 'ish-core/models/http-error/http-error.model'; -import { ApiService } from 'ish-core/services/api/api.service'; /* eslint-disable @typescript-eslint/ban-types */ @@ -12,12 +11,8 @@ import { ApiService } from 'ish-core/services/api/api.service'; export class LoginUserErrorHandler implements SpecialHttpErrorHandler { constructor(@Inject(USER_REGISTRATION_LOGIN_TYPE) public loginType: string) {} - test(error: HttpErrorResponse, request: HttpRequest): boolean { - return ( - request.headers.has(ApiService.AUTHORIZATION_HEADER_KEY) && - (error.status === 401 || error.status === 403) && - error.url.includes('customers/-') - ); + test(error: HttpErrorResponse): boolean { + return (error.status === 401 || error.status === 403) && error.url.includes('token'); } map(error: HttpErrorResponse): Partial { if (error.status === 403) { diff --git a/src/app/core/utils/meta-reducers.spec.ts b/src/app/core/utils/meta-reducers.spec.ts index ac1ee3a3b7..e593616379 100644 --- a/src/app/core/utils/meta-reducers.spec.ts +++ b/src/app/core/utils/meta-reducers.spec.ts @@ -5,7 +5,7 @@ import { identity } from 'rxjs'; import { applyConfiguration, getICMBaseURL } from 'ish-core/store/core/configuration'; import { CoreState } from 'ish-core/store/core/core-store'; import { CoreStoreModule } from 'ish-core/store/core/core-store.module'; -import { loginUser, logoutUser } from 'ish-core/store/customer/user'; +import { loginUser, logoutUserSuccess } from 'ish-core/store/customer/user'; import { StoreWithSnapshots, provideStoreSnapshots } from './dev/ngrx-testing'; import { resetOnLogoutMeta, resetSubStatesOnActionsMeta } from './meta-reducers'; @@ -19,7 +19,7 @@ describe('Meta Reducers', () => { TestBed.configureTestingModule({ imports: [ CoreStoreModule.forTesting(['configuration'], true, [ - resetSubStatesOnActionsMeta(['configuration'], [logoutUser]), + resetSubStatesOnActionsMeta(['configuration'], [logoutUserSuccess]), ]), ], providers: [provideStoreSnapshots()], @@ -36,7 +36,7 @@ describe('Meta Reducers', () => { it('should reset the configuration sub state', () => { expect(getICMBaseURL(store$.state)).toEqual(baseURL); - store$.dispatch(logoutUser()); + store$.dispatch(logoutUserSuccess()); expect(getICMBaseURL(store$.state)).toBeUndefined(); }); @@ -71,12 +71,12 @@ describe('Meta Reducers', () => { }); it('should reset state when reducing LogoutUser action', () => { - const result = resetOnLogoutMeta(identity)(state, logoutUser()); + const result = resetOnLogoutMeta(identity)(state, logoutUserSuccess()); expect(result).toBeUndefined(); }); it('should reset and delegate to reducer initial state when reducing LogoutUser action', () => { - const result = resetOnLogoutMeta(reducer)(state, logoutUser()); + const result = resetOnLogoutMeta(reducer)(state, logoutUserSuccess()); expect(result).toEqual({ a: 'initialA', b: 'initialB' }); }); diff --git a/src/app/core/utils/meta-reducers.ts b/src/app/core/utils/meta-reducers.ts index 484ca08568..94da024827 100644 --- a/src/app/core/utils/meta-reducers.ts +++ b/src/app/core/utils/meta-reducers.ts @@ -2,13 +2,13 @@ import { Action, ActionReducer, MetaReducer } from '@ngrx/store'; import { isEqual } from 'lodash-es'; import { identity } from 'rxjs'; -import { logoutUser } from 'ish-core/store/customer/user'; +import { logoutUserSuccess } from 'ish-core/store/customer/user'; import { omit } from './functions'; export function resetOnLogoutMeta(reducer: ActionReducer): ActionReducer { return (state: S, action: Action) => { - if (action.type === logoutUser.type) { + if (action.type === logoutUserSuccess.type) { return reducer(undefined, action); } return reducer(state, action); diff --git a/src/app/core/utils/oauth-configuration/oauth-configuration.service.spec.ts b/src/app/core/utils/oauth-configuration/oauth-configuration.service.spec.ts new file mode 100644 index 0000000000..1004b61869 --- /dev/null +++ b/src/app/core/utils/oauth-configuration/oauth-configuration.service.spec.ts @@ -0,0 +1,75 @@ +import { TestBed } from '@angular/core/testing'; +import { AuthConfig, OAuthInfoEvent, OAuthService, TokenResponse } from 'angular-oauth2-oidc'; +import { combineLatest, lastValueFrom, of } from 'rxjs'; +import { anything, instance, mock, verify, when } from 'ts-mockito'; + +import { ApiService } from 'ish-core/services/api/api.service'; + +import { OAuthConfigurationService } from './oauth-configuration.service'; + +describe('Oauth Configuration Service', () => { + const TOKEN_ENDPOINT = 'http://test-icm-url.de/token'; + + const config: AuthConfig = { + tokenEndpoint: TOKEN_ENDPOINT, + requireHttps: false, + }; + + const apiService = mock(ApiService); + const oAuthService = mock(OAuthService); + + let component: OAuthConfigurationService; + + beforeEach(() => { + when(apiService.constructUrlForPath('token', anything())).thenReturn(of(TOKEN_ENDPOINT)); + const infoEvent = Object.create(OAuthInfoEvent.prototype); + when(oAuthService.events).thenReturn( + // eslint-disable-next-line ban/ban + of(Object.assign(infoEvent, { type: 'token_expires', info: 'access_token' })) + ); + when(oAuthService.refreshToken()).thenReturn( + lastValueFrom(of({ access_token: 'access', expires_in: 10, refresh_token: 'refresh' } as TokenResponse)) + ); + + TestBed.configureTestingModule({ + providers: [ + { provide: ApiService, useFactory: () => instance(apiService) }, + { provide: OAuthService, useFactory: () => instance(oAuthService) }, + ], + }).compileComponents(); + + component = TestBed.inject(OAuthConfigurationService); + }); + + it('should be created', () => { + expect(component).toBeTruthy(); + }); + + describe('loadConfig$', () => { + it('should calculate correct configuration object', done => { + component.loadConfig$.subscribe(authConf => { + verify(apiService.constructUrlForPath('token', anything())).once(); + expect(authConf).toEqual(config); + done(); + }); + }); + }); + + describe('config$', () => { + it('should contain the calculated authConfig after successful loadConfig$ action', done => { + combineLatest([component.loadConfig$, component.config$]).subscribe(([, authConf]) => { + expect(authConf).toEqual(config); + done(); + }); + }); + }); + + describe('setupRefreshTokenMechanism$', () => { + it('should refresh access token, when access token is about to expire', done => { + component.setupRefreshTokenMechanism$().subscribe(() => { + verify(oAuthService.refreshToken()).once(); + done(); + }); + }); + }); +}); diff --git a/src/app/core/utils/oauth-configuration/oauth-configuration.service.ts b/src/app/core/utils/oauth-configuration/oauth-configuration.service.ts new file mode 100644 index 0000000000..bf58651fba --- /dev/null +++ b/src/app/core/utils/oauth-configuration/oauth-configuration.service.ts @@ -0,0 +1,50 @@ +import { Injectable } from '@angular/core'; +import { AuthConfig, OAuthInfoEvent, OAuthService, TokenResponse } from 'angular-oauth2-oidc'; +import { BehaviorSubject, Observable, filter, from, map, switchMap, take, tap } from 'rxjs'; + +import { ApiService } from 'ish-core/services/api/api.service'; +import { whenTruthy } from 'ish-core/utils/operators'; + +@Injectable({ + providedIn: 'root', +}) +export class OAuthConfigurationService { + config$ = new BehaviorSubject(undefined); + + constructor(private apiService: ApiService, private oAuthService: OAuthService) {} + + /** + * load an AuthConfig configuration object with specified tokenEndpoint + */ + get loadConfig$(): Observable { + return this.apiService + .constructUrlForPath('token', { + sendCurrency: true, + sendLocale: true, + }) + .pipe( + whenTruthy(), + filter(url => !url.startsWith('/')), // url should not be relative + take(1), + map(url => ({ + tokenEndpoint: url, + requireHttps: url.startsWith('https'), + })), + tap(config => this.config$.next(config)) + ); + } + + /** + * Refresh existing tokens, when token is about to expire + * + * @returns {TokenResponse} updated tokens + */ + setupRefreshTokenMechanism$(): Observable { + return this.oAuthService.events.pipe( + filter( + event => event instanceof OAuthInfoEvent && event.type === 'token_expires' && event.info === 'access_token' + ), + switchMap(() => from(this.oAuthService.refreshToken())) + ); + } +} diff --git a/src/app/extensions/punchout/identity-provider/punchout-identity-provider.spec.ts b/src/app/extensions/punchout/identity-provider/punchout-identity-provider.spec.ts index 2582add450..89e701c811 100644 --- a/src/app/extensions/punchout/identity-provider/punchout-identity-provider.spec.ts +++ b/src/app/extensions/punchout/identity-provider/punchout-identity-provider.spec.ts @@ -1,18 +1,20 @@ import { TestBed, fakeAsync, tick } from '@angular/core/testing'; import { ActivatedRouteSnapshot, Params, Router, UrlTree, convertToParamMap } from '@angular/router'; import { MockStore, provideMockStore } from '@ngrx/store/testing'; -import { EMPTY, Observable, Subject, noop, of, timer } from 'rxjs'; +import { OAuthService } from 'angular-oauth2-oidc'; +import { BehaviorSubject, EMPTY, Observable, Subject, noop, of, timer } from 'rxjs'; import { switchMap } from 'rxjs/operators'; -import { anyString, anything, capture, instance, mock, resetCalls, spy, verify, when } from 'ts-mockito'; +import { anyString, anything, instance, mock, resetCalls, spy, verify, when } from 'ts-mockito'; import { AccountFacade } from 'ish-core/facades/account.facade'; import { AppFacade } from 'ish-core/facades/app.facade'; import { CheckoutFacade } from 'ish-core/facades/checkout.facade'; import { selectQueryParam } from 'ish-core/store/core/router'; -import { ApiTokenCookieType, ApiTokenService } from 'ish-core/utils/api-token/api-token.service'; +import { ApiTokenService } from 'ish-core/utils/api-token/api-token.service'; import { CookiesService } from 'ish-core/utils/cookies/cookies.service'; import { makeHttpError } from 'ish-core/utils/dev/api-service-utils'; import { BasketMockData } from 'ish-core/utils/dev/basket-mock-data'; +import { OAuthConfigurationService } from 'ish-core/utils/oauth-configuration/oauth-configuration.service'; import { PunchoutSession } from '../models/punchout-session/punchout-session.model'; import { PunchoutService } from '../services/punchout/punchout.service'; @@ -32,12 +34,11 @@ describe('Punchout Identity Provider', () => { const accountFacade = mock(AccountFacade); const checkoutFacade = mock(CheckoutFacade); const cookiesService = mock(CookiesService); + const oAuthConfigurationService = mock(OAuthConfigurationService); let punchoutIdentityProvider: PunchoutIdentityProvider; let store$: MockStore; - let storeSpy$: MockStore; let router: Router; - let cookieVanishes$: Subject; beforeEach(() => { TestBed.configureTestingModule({ @@ -47,6 +48,8 @@ describe('Punchout Identity Provider', () => { { provide: AppFacade, useFactory: () => instance(appFacade) }, { provide: CheckoutFacade, useFactory: () => instance(checkoutFacade) }, { provide: CookiesService, useFactory: () => instance(cookiesService) }, + { provide: OAuthConfigurationService, useFactory: () => instance(oAuthConfigurationService) }, + { provide: OAuthService, useFactory: () => instance(mock(OAuthService)) }, { provide: PunchoutService, useFactory: () => instance(punchoutService) }, provideMockStore(), ], @@ -55,14 +58,13 @@ describe('Punchout Identity Provider', () => { punchoutIdentityProvider = TestBed.inject(PunchoutIdentityProvider); router = TestBed.inject(Router); store$ = TestBed.inject(MockStore); - storeSpy$ = spy(store$); }); beforeEach(() => { - cookieVanishes$ = new Subject(); when(apiTokenService.restore$(anything())).thenReturn(of(true)); + when(apiTokenService.cookieVanishes$).thenReturn(new Subject()); when(checkoutFacade.basket$).thenReturn(EMPTY); - when(apiTokenService.cookieVanishes$).thenReturn(cookieVanishes$); + when(oAuthConfigurationService.config$).thenReturn(new BehaviorSubject({})); resetCalls(apiTokenService); resetCalls(punchoutService); @@ -91,18 +93,21 @@ describe('Punchout Identity Provider', () => { describe('triggerLogout', () => { beforeEach(() => { when(checkoutFacade.basket$).thenReturn(of(BasketMockData.getBasket())); + when(accountFacade.isLoggedIn$).thenReturn(of(false)); store$.overrideSelector(selectQueryParam(anything()), undefined); punchoutIdentityProvider.init(); }); - it('should remove api token and basket-id on logout', () => { + it('should remove api token and basket-id on logout', done => { expect(window.sessionStorage.getItem('basket-id')).toEqual(BasketMockData.getBasket().id); - punchoutIdentityProvider.triggerLogout(); + const logoutTrigger$ = punchoutIdentityProvider.triggerLogout() as Observable; - expect(window.sessionStorage.getItem('basket-id')).toBeNull(); - expect(capture(storeSpy$.dispatch).first()).toMatchInlineSnapshot(`[User] Logout User`); - verify(apiTokenService.removeApiToken()).once(); + logoutTrigger$.subscribe(() => { + expect(window.sessionStorage.getItem('basket-id')).toBeNull(); + verify(accountFacade.logoutUser()).once(); + done(); + }); }); it('should return to home page per default on subscribe', done => { @@ -123,8 +128,8 @@ describe('Punchout Identity Provider', () => { beforeEach(() => { routerSpy = spy(router); punchoutIdentityProvider.init(); - when(accountFacade.userError$).thenReturn(EMPTY); - when(accountFacade.isLoggedIn$).thenReturn(EMPTY); + when(accountFacade.userError$).thenReturn(timer(Infinity).pipe(switchMap(() => EMPTY))); + when(accountFacade.isLoggedIn$).thenReturn(of(true)); }); it('should throw an business error without query params on login', () => { @@ -141,9 +146,13 @@ describe('Punchout Identity Provider', () => { queryParams = { sid: 'sid', 'access-token': accessToken }; }); - it('should trigger loginUserWithToken method on login', () => { - punchoutIdentityProvider.triggerLogin(getSnapshot(queryParams)); - verify(accountFacade.loginUserWithToken(accessToken)).once(); + it('should trigger loginUserWithToken method on login', done => { + const login$ = punchoutIdentityProvider.triggerLogin(getSnapshot(queryParams)) as Observable; + + login$.subscribe(() => { + verify(accountFacade.loginUserWithToken(accessToken)).once(); + done(); + }); }); }); @@ -155,9 +164,13 @@ describe('Punchout Identity Provider', () => { queryParams = { HOOK_URL: 'url', USERNAME: username, PASSWORD: password }; }); - it('should trigger loginUser method on login', () => { - punchoutIdentityProvider.triggerLogin(getSnapshot(queryParams)); - verify(accountFacade.loginUser(anything())).once(); + it('should trigger loginUser method on login', done => { + const login$ = punchoutIdentityProvider.triggerLogin(getSnapshot(queryParams)) as Observable; + + login$.subscribe(() => { + verify(accountFacade.loginUser(anything())).once(); + done(); + }); }); }); describe('race', () => { diff --git a/src/app/extensions/punchout/identity-provider/punchout-identity-provider.ts b/src/app/extensions/punchout/identity-provider/punchout-identity-provider.ts index 6124b6cdc6..1364a6fef2 100644 --- a/src/app/extensions/punchout/identity-provider/punchout-identity-provider.ts +++ b/src/app/extensions/punchout/identity-provider/punchout-identity-provider.ts @@ -2,23 +2,28 @@ import { HttpEvent, HttpHandler, HttpRequest } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { ActivatedRouteSnapshot, Router, UrlTree } from '@angular/router'; import { Store, select } from '@ngrx/store'; -import { Observable, noop, of, race, throwError } from 'rxjs'; -import { catchError, concatMap, delay, first, map, switchMap, take, tap } from 'rxjs/operators'; +import { OAuthService } from 'angular-oauth2-oidc'; +import { BehaviorSubject, Observable, merge, noop, of, race, throwError } from 'rxjs'; +import { catchError, concatMap, delay, filter, first, map, switchMap, take, tap, withLatestFrom } from 'rxjs/operators'; import { AccountFacade } from 'ish-core/facades/account.facade'; import { AppFacade } from 'ish-core/facades/app.facade'; import { CheckoutFacade } from 'ish-core/facades/checkout.facade'; import { IdentityProvider, TriggerReturnType } from 'ish-core/identity-provider/identity-provider.interface'; import { selectQueryParam } from 'ish-core/store/core/router'; -import { logoutUser } from 'ish-core/store/customer/user'; import { ApiTokenService } from 'ish-core/utils/api-token/api-token.service'; import { CookiesService } from 'ish-core/utils/cookies/cookies.service'; +import { OAuthConfigurationService } from 'ish-core/utils/oauth-configuration/oauth-configuration.service'; import { whenTruthy } from 'ish-core/utils/operators'; import { PunchoutService } from '../services/punchout/punchout.service'; @Injectable({ providedIn: 'root' }) export class PunchoutIdentityProvider implements IdentityProvider { + // emits true, when OAuth Service is successfully configured + // used as an additional condition to check that the OAuth Service is configured before OAuth Service actions are used + private oAuthServiceConfigured$ = new BehaviorSubject(false); + constructor( protected router: Router, protected store: Store, @@ -27,7 +32,9 @@ export class PunchoutIdentityProvider implements IdentityProvider { private accountFacade: AccountFacade, private punchoutService: PunchoutService, private cookiesService: CookiesService, - private checkoutFacade: CheckoutFacade + private checkoutFacade: CheckoutFacade, + private oAuthService: OAuthService, + private configService: OAuthConfigurationService ) {} getCapabilities() { @@ -39,13 +46,34 @@ export class PunchoutIdentityProvider implements IdentityProvider { } init() { - this.apiTokenService.restore$(['user', 'order']).subscribe(noop); - this.apiTokenService.cookieVanishes$.subscribe(type => { - if (type === 'user') { - this.store.dispatch(logoutUser()); - } + // OAuth Service should be configured by internal OAuth configuration service + this.configService.config$.pipe(whenTruthy(), take(1)).subscribe(config => { + this.oAuthService.configure(config); + this.oAuthServiceConfigured$.next(true); }); + this.apiTokenService.cookieVanishes$ + .pipe(withLatestFrom(this.apiTokenService.apiToken$)) + .subscribe(([type, apiToken]) => { + if (!apiToken) { + this.accountFacade.fetchAnonymousToken(); + } + if (type === 'user') { + this.accountFacade.logoutUser({ revokeApiToken: false }); + } + }); + + // OAuth Service should be configured before apiToken information are restored and the refresh token mechanism is setup + this.oAuthServiceConfigured$ + .pipe( + whenTruthy(), + take(1), + switchMap(() => + merge(this.apiTokenService.restore$(['user', 'order']), this.configService.setupRefreshTokenMechanism$()) + ) + ) + .subscribe(noop); + this.checkoutFacade.basket$.pipe(whenTruthy(), first()).subscribe(basketView => { window.sessionStorage.setItem('basket-id', basketView.id); }); @@ -66,67 +94,87 @@ export class PunchoutIdentityProvider implements IdentityProvider { return false; } - // initiate the punchout user login with the access-token (cXML) or the given credentials (OCI) - if (route.queryParamMap.has('access-token')) { - this.accountFacade.loginUserWithToken(route.queryParamMap.get('access-token')); - } else { - this.accountFacade.loginUser({ - login: route.queryParamMap.get('USERNAME'), - password: route.queryParamMap.get('PASSWORD'), - }); - } - return race( - // throw an error if a user login error occurs - this.accountFacade.userError$.pipe( - whenTruthy(), - take(1), - concatMap(userError => throwError(() => userError)) - ), - - // handle the punchout functions once the punchout user is logged in - this.accountFacade.isLoggedIn$.pipe( - whenTruthy(), - take(1), - switchMap(() => { - // handle cXML punchout with sid - if (route.queryParamMap.get('sid')) { - return this.handleCxmlPunchoutLogin(route); - // handle OCI punchout with HOOK_URL - } else if (route.queryParamMap.get('HOOK_URL')) { - return this.handleOciPunchoutLogin(route); - } - }), - // punchout error after successful authentication (needs to logout) - catchError(error => - this.accountFacade.userLoading$.pipe( - first(loading => !loading), - delay(0), + return this.oAuthServiceConfigured$.pipe( + whenTruthy(), + take(1), + tap(() => { + // initiate the punchout user login with the access-token (cXML) or the given credentials (OCI) + if (route.queryParamMap.has('access-token')) { + this.accountFacade.loginUserWithToken(route.queryParamMap.get('access-token')); + } else { + this.accountFacade.loginUser({ + login: route.queryParamMap.get('USERNAME'), + password: route.queryParamMap.get('PASSWORD'), + }); + } + }), + switchMap(() => + race( + // throw an error if a user login error occurs + this.accountFacade.userError$.pipe( + whenTruthy(), + take(1), + concatMap(userError => throwError(() => userError)) + ), + + // handle the punchout functions once the punchout user is logged in + this.accountFacade.isLoggedIn$.pipe( + whenTruthy(), + take(1), switchMap(() => { - this.accountFacade.logoutUser(); - this.apiTokenService.removeApiToken(); - this.appFacade.setBusinessError(error); - return of(this.router.parseUrl('/error')); - }) + // handle cXML punchout with sid + if (route.queryParamMap.get('sid')) { + return this.handleCxmlPunchoutLogin(route); + // handle OCI punchout with HOOK_URL + } else if (route.queryParamMap.get('HOOK_URL')) { + return this.handleOciPunchoutLogin(route); + } + }), + // punchout error after successful authentication (needs to logout) + catchError(error => + this.accountFacade.userLoading$.pipe( + first(loading => !loading), + delay(0), + switchMap(() => { + this.accountFacade.logoutUser(); + this.apiTokenService.removeApiToken(); + this.appFacade.setBusinessError(error); + return of(this.router.parseUrl('/error')); + }) + ) + ) ) + ).pipe( + // general punchout error handling (parameter missing, authentication error) + catchError(error => { + this.appFacade.setBusinessError(error); + return of(this.router.parseUrl('/error')); + }) ) ) - ).pipe( - // general punchout error handling (parameter missing, authentication error) - catchError(error => { - this.appFacade.setBusinessError(error); - return of(this.router.parseUrl('/error')); - }) ); } triggerLogout(): TriggerReturnType { window.sessionStorage.removeItem('basket-id'); - this.store.dispatch(logoutUser()); - this.apiTokenService.removeApiToken(); - return this.store.pipe( - select(selectQueryParam('returnUrl')), - map(returnUrl => returnUrl || '/home'), - map(returnUrl => this.router.parseUrl(returnUrl)) + return this.oAuthServiceConfigured$.pipe( + whenTruthy(), + take(1), + tap(() => this.accountFacade.logoutUser()), // user will be logged out and related refresh token is revoked on server + switchMap(() => + this.accountFacade.isLoggedIn$.pipe( + // wait until the user is logged out before you go to homepage to prevent unnecessary REST calls + filter(loggedIn => !loggedIn), + take(1), + switchMap(() => + this.store.pipe( + select(selectQueryParam('returnUrl')), + map(returnUrl => returnUrl || '/home'), + map(returnUrl => this.router.parseUrl(returnUrl)) + ) + ) + ) + ) ); } From cf899565b0400ebf132df5dacb81af922b883a6c Mon Sep 17 00:00:00 2001 From: Marcel Eisentraut Date: Mon, 19 Dec 2022 12:51:09 +0100 Subject: [PATCH 36/39] docs: improve authentication documentation (#1156) * reworked authentication documentation structuring after implementation of `/token` API integration * improved documentation regarding ICM authentication, SSO and Punchout Co-authored-by: Silke Co-authored-by: MGlatter --- docker-compose.yml | 12 +++ docs/README.md | 6 +- docs/concepts/authentication.md | 95 +++++++++++++++++++ docs/guides/authentication_icm.md | 38 ++++++++ docs/guides/authentication_punchout.md | 79 +++++++++++++++ .../sso.md => guides/authentication_sso.md} | 55 +++++++---- docs/guides/migrations.md | 40 ++++---- docs/guides/nginx-startup.md | 58 ++++++----- docs/guides/sso-auth0.md | 29 ------ docs/guides/ssr-startup.md | 64 ++++++------- 10 files changed, 345 insertions(+), 131 deletions(-) create mode 100644 docs/concepts/authentication.md create mode 100644 docs/guides/authentication_icm.md create mode 100644 docs/guides/authentication_punchout.md rename docs/{concepts/sso.md => guides/authentication_sso.md} (52%) delete mode 100644 docs/guides/sso-auth0.md diff --git a/docker-compose.yml b/docker-compose.yml index c70eb572cf..622a322442 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -25,6 +25,14 @@ services: # FEATURES: | # - compare # - rating + # IDENTITY_PROVIDER: 'Auth0' + # IDENTITY_PROVIDERS: | + # Auth0: + # type: auth0 + # domain: some-domain.auth0.com + # clientID: ASDF12345 + # Punchout: + # type: PUNCHOUT # # add 127.0.0.1 mypwa.net to your hosts file and @@ -81,6 +89,10 @@ services: # BASIC_AUTH_IP_WHITELIST: | # # - 172.22.0.1 # - 1.2.3.4 + # OVERRIDE_IDENTITY_PROVIDERS: | + # .+: + # - path: /en/punchout + # type: Punchout MULTI_CHANNEL: | .+: - baseHref: /en diff --git a/docs/README.md b/docs/README.md index efa8cacdeb..60c3fa7029 100644 --- a/docs/README.md +++ b/docs/README.md @@ -27,7 +27,10 @@ kb_sync_latest_only - [Guide - Formly](./guides/formly.md) - [Guide - Field Library](./guides/field-library.md) - [Concept - Deployment Scenarios for Angular Applications](./concepts/deployment-angular.md) -- [Concept - Single Sign-On (SSO) for PWA](./concepts/sso.md) +- [Concept - Authentication](./concepts/authentication.md) + - [Guide - Authentication by the ICM Server](./guides/authentication_icm.md) + - [Guide - Authentication with Single Sign-On (SSO)](./guides/authentication_sso.md) + - [Guide - Authentication with the Punchout Identity Provider](./guides/authentication_punchout.md) ### Developing @@ -80,5 +83,4 @@ kb_sync_latest_only - [Guide - Client-Side Error Monitoring with Sentry](./guides/sentry-error-monitoring.md) - [Guide - Extended Product Configurations with Tacton](./guides/tacton-product-configuration.md) - [Guide - Monitoring with Prometheus](./guides/prometheus-monitoring.md) -- [Guide - SSO with Auth0 for PWA](./guides/sso-auth0.md) - [Guide - Store Locator with Google Maps](./guides/store-locator.md) diff --git a/docs/concepts/authentication.md b/docs/concepts/authentication.md new file mode 100644 index 0000000000..d992f636ba --- /dev/null +++ b/docs/concepts/authentication.md @@ -0,0 +1,95 @@ + + +# Authentication Concept + +## Introduction + +Several ICM REST operations require an authenticated user. +Authentication also assures enterprise information security. +In the PWA a user can be verified with the help of an identity provider. +An identity provider (IdP) is a service that stores and manages digital identities. +The following identity providers are supported: The default [ICM server](../guides/authentication_icm.md), the [SSO Auth0](../guides/authentication_sso.md) and the [Punchout](../guides/authentication_punchout.md) identity provider. + +## Library angular-oauth2-oidc + +There is a lot of functionality related to authentication, e.g., logging a user in and out, registering a new user, keeping the user identified even if the user opens further browser tabs, etc. + +The PWA uses the library [angular-oauth2-oidc](https://github.com/manfredsteyer/angular-oauth2-oidc#readme) to support the implementation of these functionalities. +It can be configured to provide access to identity providers. +You can find the initialization of this library in the [oauth-configuration-service.ts](../../src/app/shared/../core/utils/oauth-configuration/oauth-configuration.service.ts). + +## Implementation and Configuration of Identity Providers + +To add or change the functionality of an identity provider, the following steps are necessary: + +1. Create/change an `.identity-provider.ts` class that implements the interface [`IdentityProvider`](../../src/app/core/identity-provider/identity-provider.interface.ts). In this interface all methods are declared which have to be implemented in your IdP class. + + In the following code you see a typical implementation of the init method of an IdP class. + + Note that all authentication-related functionality must not be executed before the oAuth service has been configured. + + ```typescript + @Injectable({ providedIn: 'root' }) + export class ExampleIdentityProvider implements IdentityProvider { + private configured$ = new BehaviorSubject(false); + + constructor(private oAuthService: OAuthService, private configService: OAuthConfigurationService) {} + + init() { + this.configService.config$.subscribe(config => { + this.oAuthService.configure(config); + this.configured.next(true); + }); + + this.configured + .pipe( + whenTruthy(), + switchMap(() => from(this.oAuthService.fetchTokenUsingGrant('anonymous'))) + ) + .subscribe(); + } + } + ``` + +2. Register the `.identity-provider.ts` in the [`IdentityProviderModule`](../../src/app/core/identity-provider.module.ts). The `APP_INITIALIZER` injection token is used to configure and initialize the identity provider before app initialization. + +3. Set the environment variables `IdentityProviders` and `IdentityProvider` accordingly. + +## PWA Initialization + +A PWA user has to be identified by the ICM server by a unique authentication token, even if it is an anonymous user. +Once a user opens the PWA for the first time, an authentication token is requested by the [ICM Token REST endpoint](https://support.intershop.com/kb/index.php?c=Display&q1=U29770&q2=Text). +This happens in the [`init()`](../../src/app/core/identity-provider/icm.identity-provider.ts) method of the active identity provider. +Subsequently, this token will be saved as `apiToken` cookie and added to all REST requests in the request header, e.g.: + +```typescript +authentication-token: encryption0@PBEWithMD5AndTripleDES:1D7T8HyFqQ0=|k3PQLgujzUq0tudtw+6HLjWnExiwrd4o9/jVU7ZH74kTfTy3RS7/sYadsg7ODRM2 +``` + +This way it is possible to identify users even they are opening a new browser tab or refreshing the PWA in the browser. + +If a user opens the PWA and already has a valid apiToken cookie, no new token is requested by the ICM server but this token is used in the header of the REST requests. + +## Login, Registration, Token Refreshment, Logout + +All these functionalities strongly depend on the implementation of the used identity provider. +This is described in the appropriate identity provider guides in more detail, see [Further References](#further-references) below. + +## Vanishing of the apiToken Cookie + +The PWA needs to react in case the `apiToken` cookie is not available anymore. +This could happen if a PWA is opened in many tabs and the user logs out, or when users remove the cookie themselves. +When the cookie vanishes, the PWA emits a new value for the [`cookieVanishes$` subject](../../src/app/core/utils/api-token/api-token.service.ts). +The identity provider implementation defines how the application should behave in such a case. +With the ICM identity provider, for example, the user is then automatically logged out and routed to the `/login` page. + +## Further References + +- [Guide - ICM Identity Provider](../guides/authentication_icm.md) +- [Guide - Punchout Identity Provider](../guides/authentication_punchout.md) +- [Guide - Single Sign-On (SSO) Identity Provider](../guides/authentication_sso.md) diff --git a/docs/guides/authentication_icm.md b/docs/guides/authentication_icm.md new file mode 100644 index 0000000000..51b07a1c6f --- /dev/null +++ b/docs/guides/authentication_icm.md @@ -0,0 +1,38 @@ + + +# Authentication by the ICM Server + +This document describes the main authentication mechanism if the ICM server is used as identity provider. +If you need an introduction to this topic, read the [Authentication Concept](../concepts/authentication.md) first. + +## Login + +If the user wants to login by clicking a login link or navigating to the `/login` route, either a popup or a page is displayed containing a login form. +After the user has entered the credentials (e-mail/user name and password) and could be verified successfully by the ICM server, a new token is fetched from the ICM `/token` REST endpoint. +The token of the registered user is saved as `apiToken` cookie and attached to the request header of the subsequent REST requests. +After logging in, the pgid of the user is requested from the ICM server (`/personalization` REST call) and the action `personalizationStatusDetermined` will be triggered. +If you want to request user-specific non-cached data from the ICM server, use the option `sendPGID` or `sendSPGID`, respectively when you call the _get_ method of the `ApiTokenService`. + +## Registration + +The registration of a user is similar to the login. +After the user has completed the registration form, the data are validated by the ICM server and a new user will be created. +Afterwards, the authentication token is requested from the server and the user will be logged in, see above. + +## Token Lifetime + +Each authentication token has a predefined lifetime. +That means, the token has to be refreshed to prevent it from expiring. +Once 75% of the token's lifetime have passed (this time can be configured in the oAuth library), an info event is emitted. +This event is used to call the [refresh mechanism `setupRefreshTokenMechanism$`](../../src/app/core/utils/oauth-configuration/oauth-configuration.service.ts) of the oAuth configuration service and the authentication token will be renewed. +Hence, the token will not expire as long as the user keeps the PWA open in the browser. + +## Logout + +When the user logs out by clicking the logout link or navigating to the `/logout` route, the configured [`logout()`](../../src/app/core/identity-provider/icm.identity-provider.ts) function will be executed, which will call the [`revokeApiToken()`](../../src/app/core/services/user/user.service.ts) user service in order to deactivate the token on server side. +Besides this, the PWA removes the token on browser side, fetches a new anonymous user token, and sets it as `apiToken` cookie. diff --git a/docs/guides/authentication_punchout.md b/docs/guides/authentication_punchout.md new file mode 100644 index 0000000000..1444f9330b --- /dev/null +++ b/docs/guides/authentication_punchout.md @@ -0,0 +1,79 @@ + + +# Authentication with the Punchout Identity Provider + +This document describes the main authentication mechanism if punchout is used as identity provider. +If you need an introduction to this topic, read the [Authentication Concept](../concepts/authentication.md) first. + +## Configuration + +The PWA must be configured in a correct way to use punchout as an identity provider. +Apart from the enabled `punchout` feature flag, the following configuration can be added to the Angular CLI environment files for development purposes: + +```typescript +features: [ + 'punchout' +], +identityProvider: 'Punchout', +identityProviders: { + 'Punchout': { + type: 'PUNCHOUT', + } +}, +``` + +For production, this configuration should be provided to the SSR process via environment variables (see [Building and Running Server-Side Rendering][ssr-startup]). +The usage of identity providers can also be set in the multi-channel configuration (see [Building and Running nginx Docker Image][nginx-startup]). + +Additionally, the PWA can be configured to use the punchout identity provider only, when the user enters the punchout route. +In that case the nginx should be configured with the `OVERRIDE_IDENTITY_PROVIDERS` environment variable (see [Override Identity Providers by Path][nginx-startup]). +Nevertheless, the SSR process needs to be provided with the punchout identity provider configuration. + +```yaml +pwa: + environment: + IDENTITY_PROVIDERS: | + Punchout: + type: PUNCHOUT + +nginx: + environment: + OVERRIDE_IDENTITY_PROVIDERS: | + .+: + - path: /punchout + type: Punchout +``` + +## Login + +A user can login by navigating to the `/punchout` or `/login` route. +For this purpose [specific query params](../../src/app/extensions/punchout/identity-provider/punchout-identity-provider.ts) need to be added to the given route depending on whether the OCI or the cXML punchout should be used. +For the OCI punchout login the user needs to add the `HOOK_URL`, `USERNAME` and `PASSWORD` as query parameters, while the cXML user has to include the `sid` and `access-token`. +In addition, the [cXML punchout tester](https://punchoutcommerce.com/tools/cxml-punchout-tester) could be used to log in a cXML punchout user. +The request [/customers/${CustomersKey}/punchouts/cxml1.2/setuprequest](https://support.intershop.com/kb/index.php/Display/29L952#l1142) to create a new cXML punchout session must be inserted as the URL with the credentials of the cXML punchout user. +When the session is created successfully, the punchout tester will redirect to the ICM configured PWA deployment `/punchout` route. + +## Registration + +There is currently no possibility to register a new punchout user in the PWA. + +## Token Lifetime + +Each authentication token has a predefined lifetime. +That means, the token has to be refreshed to prevent it from expiring. +Once 75% of the token's lifetime have passed ( this time can be configured in the oAuth library), an info event is emitted. +This event is used to call the [refresh mechanism `setupRefreshTokenMechanism$`](../../src/app/core/utils/oauth-configuration/oauth-configuration.service.ts) of the oAuth configuration service and the authentication token will be renewed. +Hence, the token will not expire as long as the user keeps the PWA open in the browser. + +## Logout + +When the user logs out by clicking the logout link or navigating to the `/logout` route, the configured [`logout()`](../../src/app/extensions/punchout/identity-provider/punchout-identity-provider.ts) function will be executed, which will call the [`revokeApiToken()`](../../src/app/core/services/user/user.service.ts) user service in order to deactivate the token on server side. +Besides this, the PWA removes the token and basket-id on browser side, fetches a new anonymous user token, and sets it as `apiToken` cookie. + +[ssr-startup]: ../guides/ssr-startup.md +[nginx-startup]: ../guides/nginx-startup.md diff --git a/docs/concepts/sso.md b/docs/guides/authentication_sso.md similarity index 52% rename from docs/concepts/sso.md rename to docs/guides/authentication_sso.md index fd6c08c1e6..a03dcecadb 100644 --- a/docs/concepts/sso.md +++ b/docs/guides/authentication_sso.md @@ -1,11 +1,11 @@ -# Single Sign-On (SSO) for PWA +# Authentication with Single Sign-On (SSO) Intershop Commerce Management supports logging in clients via SSO (see [Concept - Single Sign-On (SSO)][kb-concept-sso]). @@ -15,9 +15,9 @@ After setting up the ICM side with the identity provider, an implementation for For development purposes the configuration can be added to the Angular CLI environment files: ```typescript - identityProvider: 'MyProvider', + identityProvider: 'Auth0', identityProviders: { - 'MyProvider': { + 'Auth0': { type: 'auth0', domain: 'some-domain.auth0.com', clientID: 'ASDF12345', @@ -28,30 +28,51 @@ For development purposes the configuration can be added to the Angular CLI envir For production, this configuration should be provided to the SSR process via environment variables (see [Building and Running Server-Side Rendering][ssr-startup]). The usage of identity providers can also be set in the multi-channel configuration (see [Building and Running nginx Docker Image][nginx-startup]). -## Business cases +```yaml +pwa: + environment: + IDENTITY_PROVIDER: 'Auth0' + IDENTITY_PROVIDERS: | + Auth0: + type: auth0 + domain: some-domain.auth0.com + clientID: ASDF12345 +``` + +## SSO with Auth0 for PWA + +Follow [this guide](https://manfredsteyer.github.io/angular-oauth2-oidc/docs/additional-documentation/authorization-servers/auth0.html) to set up an application in the Auth0 configuration. + +The PWA contains a default SSO with Auth0 identity provider implementation located in the [`Auth0IdentityProvider`](../../src/app/core/identity-provider/auth0.identity-provider.ts). + +Use the configuration fields `domain` and `clientID` for configuring the provider. + +## Business Cases -### Create new user +### Create New User -| Authentication Provider | Route in ICM email | Behavior of PWA | -| ----------------------- | ------------------ | ------------------------------------------ | -| ICM | /invite | Redirect to /forgotPassword/updatePassword | -| SSO | /invite | Redirect to SSO provider | +| Authentication Provider | Route in ICM e-mail | Behavior of PWA | +| ----------------------- | ------------------- | ------------------------------------------ | +| ICM | /invite | Redirect to /forgotPassword/updatePassword | +| SSO | /invite | Redirect to SSO provider | -### User forgot password +### User Forgot Password -| Authentication Provider | Route in ICM email | Behavior of PWA | -| ----------------------- | ------------------------------ | ------------------------- | -| ICM | /forgotPassword/updatePassword | Show change password form | -| SSO | /forgotPassword/updatePassword | Redirect to SSO provider | +| Authentication Provider | Route in ICM e-mail | Behavior of PWA | +| ----------------------- | ------------------------------ | --------------------------- | +| ICM | /forgotPassword/updatePassword | Show _change password_ form | +| SSO | /forgotPassword/updatePassword | Redirect to SSO provider | -# Further References +## Further References - PWA - - [Guide - SSO with Auth0 for PWA](../guides/sso-auth0.md) + - [Concept - Authentication](../concepts/authentication.md) - [Guide - Building and Running Server-Side Rendering][ssr-startup] - [Guide - Building and Running nginx Docker Image][nginx-startup] - ICM - [Concept - Single Sign-On (SSO)][kb-concept-sso] +- General + - [SSO with OAuth 2 and OpenId Connect](https://angular.de/artikel/oauth-odic-plugin/) (in German) [kb-concept-sso]: https://support.intershop.com/kb/index.php/Display/29A407 [ssr-startup]: ../guides/ssr-startup.md diff --git a/docs/guides/migrations.md b/docs/guides/migrations.md index 3b816b37b0..94111ab110 100644 --- a/docs/guides/migrations.md +++ b/docs/guides/migrations.md @@ -9,7 +9,7 @@ kb_sync_latest_only ## 3.1 to 3.2 -A styling adaption was made to the application shell to expand it to the full page height so the footer now always stays at the bottom. +A styling adaption was made to the application shell to expand it to the full page height, so the footer now always stays at the bottom. Together with that an inline style of the `main-container` was moved to the global styling definition. Formly has been upgraded from version 5 to 6. @@ -17,23 +17,23 @@ Find more information in the [Formly Upgrade Guide](https://github.com/ngx-forml We still use deprecated form properties like 'templateOptions' and 'expressionProperties' for compatibility reasons but we are going to replace them in the next major release. The two small black triangle images `active_catalog.png` (header: when hovering a catalog) and `budget-bar-indicator.png` (my account: budget bar) are removed and replaced by CSS styling. -The basket empty image `empty-cart.png` is removed and replaced with CSS styling. -The sprite image `product_sprite.png` is removed and replaced with localized text for "New", "Sale" and "Top" with the according CSS styling. +The image for an empty basket `empty-cart.png` is removed and replaced with CSS styling. +The sprite image `product_sprite.png` is removed and replaced with localized text for "New", "Sale", and "Top" with the according CSS styling. -After entering a desired delivery date on the checkout shipping page and after submitting the order the desired delivery date will be saved at all basket items, if necessary. -In case of large basket (> 20 items) this might cause (unacceptable) long response times. -You can keep the existing behavior by modifying the updateBasketItemsDesiredDeliveryDate() method of the basket service to always return an empty array without doing anything. +After entering a desired delivery date on the checkout shipping page and after submitting the order, the desired delivery date will be saved at all basket items if necessary. +In case of large baskets (> 20 items) this might cause long response times. +You can keep the existing behavior by modifying the _updateBasketItemsDesiredDeliveryDate()_ method of the basket service to always return an empty array without doing anything. The `ProductsService` was changed to use `extended=true` REST calls for product details and variations to fetch variation attributes with additional `attributeType` and `metaData` information that can be used to control the rendering of different variation select types. The added `VariationAttributeMapper` maps the additional information in a backwards compatible way. -To handle the different variation select rendering types the existing `ProductVariationSelectComponent` now contains the logic to select the fitting variation select rendering component. +To handle the different variation select rendering types, the existing `ProductVariationSelectComponent` now contains the logic to select the fitting variation select rendering component. The rendering and behavior of the existing `ProductVariationSelectComponent` as a standard select box was moved to the new `ProductVariationSelectDefaultComponent`. A `ProductVariationSelectSwatchComponent` for colorCode and swatchImage variation select rendering and a `ProductVariationSelectEnhancedComponent` for a select box rendering with color codes or swatch images and a mobile optimization were added. The user authentication process has changed. User authentication tokens are requested from the ICM server using the `/token` REST endpoint now. -Regarding this, the logout action triggers a service, which revokes the current available access token on the ICM backend. -If the logout was successful, then all personalized information is removed from the ngrx store. +Regarding this, the logout action triggers a service which revokes the currently available access token on the ICM backend. +If the logout was successful, all personalized information is removed from the ngrx store. Please use `logoutUser({ revokeToken: false })` from the account facade or dispatch `logoutUserSuccess` instead of the `logoutUser` action to use the old behavior. ## 3.0 to 3.1 @@ -42,8 +42,8 @@ The SSR environment variable 'ICM_IDENTITY_PROVIDER' will be removed in a future Use variable 'IDENTITY_PROVIDER' to select the provider to be used instead. Keep this in mind before deploying or starting the Intershop PWA in server-side rendering mode. -The default value of the input parameter ['queryParamsHandling'](https://angular.io/api/router/QueryParamsHandling) has been changed from 'merge' to '' for the components product-name.component and product-image.component. -This has been done to prevent an unintentional application of filters for product variation master links if the product detail link does not originates from a product listing context (product list page, search result page). +The default value of the input parameter ['queryParamsHandling'](https://angular.io/api/router/QueryParamsHandling) has been changed from 'merge' to '' for the components `product-name.component` and `product-image.component`. +This has been done to prevent an unintentional application of filters for product variation master links if the product detail link does not originate from a product listing context (product list page, search result page). To prevent deprecation warnings we removed the unnecessary `~` from all 3rd party SCSS imports (see https://webpack.js.org/loaders/sass-loader/#resolving-import-at-rules - "Using ~ is deprecated and can be removed from your code (we recommend it)"). This should be done for additional imports in the customizations as well. @@ -53,22 +53,22 @@ For that reason we removed it. Use the validator `equalTo` instead. Find more information in the method description in the [`special-validators.ts`](https://github.com/intershop/intershop-pwa/blob/3.0.0/src/app/shared/forms/validators/special-validators.ts#L82-L87). -The "Product Image Not Available" PNG image `not_available.png` is removed and replaced by an SVG image `not-available.svg` which does not include a text inside the image any more to avoid localization issues. -The file references are updated accordingly, the product image component is updated to use the correct image attributes, a localized alternative text is added and the product and image mapper files are updated to provide the correct data. -In case the current PNG image file and the handling is customized in a project, you have to make sure to keep the project changes. +The "Product Image Not Available" PNG image `not_available.png` is removed and replaced by an SVG image `not-available.svg` which does not include a text inside the image anymore to avoid localization issues. +The file references are updated accordingly, the product image component is updated to use the correct image attributes, a localized alternative text is added, and the product and image mapper files are updated to provide the correct data. +In case the current PNG image file and the handling is customized in a project, you have to ensure to keep the project changes. ## 2.4 to 3.0 With the 2.4.1 Hotfix we introduced a more fixed Node.js version handling to the version used and tested by us. We set Node.js 16.16.0 and npm 8.11.0 as our application runtime and package management versions. This is supposed to prevent unexpected build issues in the future but requires manual updating of Node.js to newer versions if tested successfully. -Other Node.js versions might still work but you might get warnings regarding the projects recommended settings. +Other Node.js versions might still work but you might get warnings regarding the project's recommended settings. The Intershop PWA 3.0 release includes a Jest Update to version 28, see also https://jestjs.io/docs/upgrading-to-jest28. The jest-marbles package has been replaced by jasmine-marbles. It also contains the Angular 14 update and updates to a lot of other dependencies (NgRx, Typescript). -These updates require some code adaptions, e.g. form classes have been prefixed with _Untyped_ wherever necessary. +These updates require some code adaptions, e.g., form classes have been prefixed with _Untyped_ wherever necessary. The following official guides might help to migrate custom code as well: - https://update.angular.io/?v=13.0-14.0 @@ -83,7 +83,7 @@ Cypress has been upgraded from version 9 to 10. We went through the interactive migration to move our spec files from cypress/integration folder to the cypress/e2e folder and updated the config file as well as some scripts. Find more information in the [Cypress Migration Guide](https://docs.cypress.io/guides/references/migration-guide#Migrating-to-Cypress-version-10-0). -Since the used deferred load library is no longer maintained it is removed and replaced with similar standard browser functionality [`loading="lazy"`](https://developer.mozilla.org/en-US/docs/Web/Performance/Lazy_loading#images_and_iframes). +Since the used deferred load library is no longer maintained, it is removed and has been replaced with similar standard browser functionality [`loading="lazy"`](https://developer.mozilla.org/en-US/docs/Web/Performance/Lazy_loading#images_and_iframes). All uses of the `(deferLoad)` directive in custom code need to be replaced. We removed the unmaintained `angular2-uuid` library in favor of the standard `uuid` library that is already included as an Angular dependency. @@ -99,9 +99,9 @@ The deprecated `customized-copy` schematic for copying components and replacing We introduced a build variable `SSR` that is now used for all checks if the application is running in SSR or Browser context. We no longer use the verbose way of injecting the `PLATFORM_ID` and check it with the methods `isPlatformBrowser` or `isPlatformServer`. This way still works but it is discouraged by a new ESLint rule that suggests using the new `SSR` variable instead. -So running `npm run lint` will help with finding custom code that still relies on the platform checks. +So running `npm run lint` will help finding custom code that still relies on the platform checks. -To support e.g. special characters in email addresses with newer versions of ICM (7.10.38.x), like `+`, double encoding of resource ids in the REST API calls is necessary. +To support, e.g., special characters in e-mail addresses with newer versions of ICM (7.10.38.x), like `+`, double encoding of resource ids in the REST API calls is necessary. With the method `encodeResourceID` we provide a central place that implements the fitting resource encoding. In the PWA this was applied to all user logins in REST API calls. For project customizations the usage of the native `encodeURIComponent` functionality should be replaced with `encodeResourceID` for user logins in REST calls as well. @@ -114,7 +114,7 @@ For categories it was changed from `cat` to `ctg` and for products from `sku`to This way, it is intended to have less conflicts and limitations with potential category/product ids, e.g., 'cats' or 'skunks'. To improve the support of large baskets we update the ngrx store immediately after adding, updating and deleting basket items now. -Therefore we had to change the return values of the corresponding basket service functions as well as the payload of the success actions. +Therefore, we had to change the return values of the corresponding basket service functions as well as the payload of the success actions. We also limited the number of displayed line items in the mini basket and introduced a paging bar on the basket page to speed up the rendering of these components. ## 2.3 to 2.4 diff --git a/docs/guides/nginx-startup.md b/docs/guides/nginx-startup.md index 05f33f96b6..c365ff44ae 100644 --- a/docs/guides/nginx-startup.md +++ b/docs/guides/nginx-startup.md @@ -7,34 +7,33 @@ kb_sync_latest_only # Building and Running NGINX Docker Image -We provide a docker image based on [nginx](https://nginx.org/) for the [PWA deployment](../concepts/pwa-building-blocks.md#pwa---nginx). +We provide a Docker image based on [nginx](https://nginx.org/) for the [PWA deployment](../concepts/pwa-building-blocks.md#pwa---nginx). ## Building -The docker image can be built by running a docker build with the `Dockerfile` located in the `nginx` folder. +The Docker image can be built by running a Docker build with the `Dockerfile` located in the `nginx` folder. ## Configuration Mandatory environment variables: -- Connect it to the PWA with `UPSTREAM_PWA` in the form of `http://:` +- Connect the nginx to the PWA with the `UPSTREAM_PWA` value in the form of `http://:` For HTTP, the server will run on default port 80. For HTTPS, the server will run on default port 443. -We're using the standard NGinx Docker image. -Therefore we inherit all their configuration capabilities. -For further information please refer to [the official NGinx Docker image page](https://hub.docker.com/_/nginx?tab=description) +We are using the standard nginx Docker image. +Therefore, we inherit all their configuration capabilities. +For further information please refer to [the official nginx Docker image page](https://hub.docker.com/_/nginx?tab=description) ### HTTPS or SSL -You can switch on HTTPS for the nginx container to execute a production like setup locally or for demo purposes. -Just by changing `ENV SSL=0` to `ENV SSL=1` and adjusting the port mapping in `docker-compose.yml`. +You can switch on HTTPS for the nginx container to execute a production-like setup locally or for demo purposes by changing `ENV SSL=0` to `ENV SSL=1` and adjusting the port mapping in `docker-compose.yml`. No need to supply a certificate and a key. They are automatically generated inside the running container. The certificate is self-signed and will not work in your browser. You have to confirm the security exception. -As developer convenience you can volume mount an internal folder to your host system to effectively trust the generated cert. +As developer convenience you can volume mount an internal folder to your host system to effectively trust the generated certificate. Please check the nginx logs for the following output. @@ -47,8 +46,8 @@ You can now export the local CA by adjusting your docker-compose.yml /home/your- ### Basic Auth -For deploying to test environments that should not be indexed by search bots or should not be accessible by the public, the nginx container can be set up with basic authentication. -Just supply a single user-password combination as environment variable, i.e. `BASIC_AUTH=:`. +For deploying to test environments that are not to be indexed by search bots or are not to be accessible by the public, the nginx container can be set up with basic authentication. +To do so, supply a single user-password combination as environment variable, i.e. `BASIC_AUTH=:`. You can also whitelist IPs by supplying a YAML list to the environment variable `BASIC_AUTH_IP_WHITELIST`: ```yaml @@ -63,7 +62,7 @@ nginx: Entries of the IP whitelist are added to the nginx config as [`allow`](http://nginx.org/en/docs/http/ngx_http_access_module.html) statements, which also supports IP ranges. Please refer to the linked nginx documentation on how to configure this. -After globally activating basic authentication for your setup you can also disable it selectively per site. +After globally activating basic authentication for your setup, you can also disable it selectively per site. See [Multi-Site Configurations](../guides/multi-site-configurations.md#Examples) for examples on how to do that. ### Multi-Site @@ -73,22 +72,22 @@ Multiple PWA channels can be set up by supplying a [YAML](https://yaml.org) conf For more information on the multi-site syntax, refer to [Multi-Site Configurations](../guides/multi-site-configurations.md#Syntax) -The configuration can be supplied simply by setting the environment variable `MULTI_CHANNEL`. +The configuration can be supplied by setting the environment variable `MULTI_CHANNEL`. Alternatively, the source can be supplied by setting `MULTI_CHANNEL_SOURCE` in any [supported format by gomplate](https://docs.gomplate.ca/datasources/). If no environment variables for multi-channel configuration are provided, the configuration will fall back to the content of [`nginx/multi-channel.yaml`](../../nginx/multi-channel.yaml), which can also be customized. -> :warning: Multi-Channel configuration with context paths does not work in conjunction with [service workers](../concepts/progressive-web-app.md#service-worker) +> :warning: Multi-Channel configuration with context paths does not work in conjunction with [service workers](../concepts/progressive-web-app.md#service-worker). An extended list of examples can be found in the [Multi-Site Configurations](../guides/multi-site-configurations.md#Syntax) guide. ### Ignore Parameters During Caching Often, nginx receives requests from advertising networks or various user agents that append unused query parameters when making a request, for example `utm_source`.
-These parameters can lead to inefficient caching because even if the same URL is requested multiple times, if it is accessed with different query parameters, the cached version will not be used. +These parameters can lead to inefficient caching, because even if the same URL is requested multiple times, the cached version will not be used if the URL is accessed with different query parameters. To prevent this, you can define any number of blacklisted parameters that will be ignored by nginx during caching. -As with multi-site handling above, the configuration can be supplied simply by setting the environment variable `CACHING_IGNORE_PARAMS`.
+As with multi-site handling above, the configuration can be supplied by setting the environment variable `CACHING_IGNORE_PARAMS`.
Alternatively, the source can be supplied by setting `CACHING_IGNORE_PARAMS_SOURCE` in any [supported format by gomplate](https://docs.gomplate.ca/datasources/). Be aware that the supplied list of parameters must be declared under a `params` property. @@ -96,14 +95,14 @@ If no environment variables for ignoring parameters are provided, the configurat ### Access ICM Sitemap -Please refer to [this](https://support.intershop.com/kb/index.php/Display/23D962#ConceptXMLSitemaps-XMLSitemapsandIntershopPWAxml_sitemap_pwa) Intershop knowledge base article on how to configure ICM to generate PWA sitemap files. +Please refer to [Concept - XML Sitemaps](https://support.intershop.com/kb/index.php/Display/23D962#ConceptXMLSitemaps-XMLSitemapsandIntershopPWAxml_sitemap_pwa) on how to configure ICM to generate PWA sitemap files. ``` http://pwa/sitemap_pwa.xml ``` -To make above sitemap index file available under your deployment you need to add the environment variable `ICM_BASE_URL` to your nginx container. -Let `ICM_BASE_URL` point to your ICM backend installation, e.g. `https://pwa-ish-demo.test.intershop.com`. +To make above sitemap index file available under your deployment, you need to add the environment variable `ICM_BASE_URL` to your nginx container. +Let `ICM_BASE_URL` point to your ICM backend installation, e.g., `https://pwa-ish-demo.test.intershop.com`. When the container is started it will process cache-ignore and multi-channel templates as well as sitemap proxy rules like this: ```yaml @@ -118,7 +117,7 @@ Be sure to include `application` if you deviate from standard `rest` application ### Override Identity Providers by Path The PWA can be configured with multiple identity providers. -In some use cases a specific identity provider must be selected, when a certain route is requested. +In some use cases a specific identity provider must be selected when a certain route is requested. For example, a punchout user should be logged in by the punchout identity provider requesting a punchout route. For all other possible routes the default identity provider must be selected. This can be done by setting only the environment variable `OVERRIDE_IDENTITY_PROVIDER`. @@ -128,8 +127,8 @@ nginx: environment: OVERRIDE_IDENTITY_PROVIDERS: | .+: - - path: /b2b/punchout - type: PUNCHOUT + - path: /punchout + type: Punchout ``` This setting will generate rewrite rules for the URL paths for all given domains. @@ -146,27 +145,26 @@ Built-in features can be enabled and disabled: - `COMPRESSION=off` disables compression (default `on`) - `DEVICE_DETECTION=off` disables user-agent detection (default `on`) - `PROMETHEUS=on` enables [Prometheus](https://prometheus.io) metrics exports on port `9113` (default `off`) -- `SSL=on` to switch on HTTPS. Look above for further explanation. +- `SSL=on` to switch on HTTPS. See [HTTPS or SSL](#https-or-ssl) above for further explanation. ## Features New features can be supplied in the folder `nginx/features`. -A file named `.conf` is included if the environment variable `` is set to `on`, `1`, `true` or `yes` (case in-sensitive). -The feature is disabled otherwise and an optional file `-off.conf` is included in the configuration. -The feature name must only contain word characters (letters, numbers and underscore). +A file named `.conf` is included if the environment variable `` is set to `on`, `1`, `true` or `yes` (case insensitive). +Otherwise, the feature is disabled and an optional file `-off.conf` is included in the configuration. +The feature name must only contain word characters (letters, numbers, and underscore). ### Cache If the cache feature is switched off, all caching for pre-rendered pages is disabled. The cache duration for pre-rendered pages can be customized using `CACHE_DURATION_NGINX_OK` (for successful responses) and `CACHE_DURATION_NGINX_NF` (for 404 responses). -The value supplied must be in the `time` format that is supported by [nginx proxy_cache_valid](http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_cache_valid) +The value supplied must be in the `time` format that is supported by [nginx proxy_cache_valid](http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_cache_valid). -# Further References +## Further References - [Concept - Multi-Site Handling](../concepts/multi-site-handling.md) - [Concept - Configuration](../concepts/configuration.md) - [Concept - Logging](../concepts/logging.md) -- [Concept - Single Sign-On (SSO) for PWA](../concepts/sso.md) - [Guide - Monitoring with Prometheus](./prometheus-monitoring.md) -- [README of official NGinx Docker image](https://hub.docker.com/_/nginx?tab=description) +- [README of official nginx Docker image](https://hub.docker.com/_/nginx?tab=description) diff --git a/docs/guides/sso-auth0.md b/docs/guides/sso-auth0.md deleted file mode 100644 index 635768c469..0000000000 --- a/docs/guides/sso-auth0.md +++ /dev/null @@ -1,29 +0,0 @@ - - -# SSO with Auth0 for PWA - -Follow [this guide](https://manfredsteyer.github.io/angular-oauth2-oidc/docs/additional-documentation/authorization-servers/auth0.html) to set up an application in the Auth0 configuration. - -The PWA implementation for this identity provider is located in [`Auth0IdentityProvider`](../../src/app/core/identity-provider/auth0.identity-provider.ts). - -Use the fields "Domain" and "Client ID" for configuring the provider: - -```typescript - identityProvider: 'MyProvider', - identityProviders: { - 'MyProvider': { - type: 'auth0', - domain: 'some-domain.auth0.com', - clientID: 'ASDF12345', - } - }, -``` - -# Further References - -- [Concept - Single Sign-On (SSO) for PWA](../concepts/sso.md) diff --git a/docs/guides/ssr-startup.md b/docs/guides/ssr-startup.md index a1ce9c9f56..dfe1bbb020 100644 --- a/docs/guides/ssr-startup.md +++ b/docs/guides/ssr-startup.md @@ -21,7 +21,7 @@ The `package.json` property `config.active-themes` determines which themes shoul This will build server and client bundles for all active themes and supply them in the `dist` folder. The SSR process for each theme can be run individually using the generated scripts `dist//run-standalone`. -To run multiple themes with [PM2][pm2] the script `src/ssr/server-scripts/build-ecosystem.js` can be used to generate the ecosystem. +To run multiple themes with [PM2][pm2], the script `src/ssr/server-scripts/build-ecosystem.js` can be used to generate the ecosystem. If only one theme is active, the theme-specific SSR process will be run in cluster mode on the default port. If more themes are active, PM2 is provisioned to run a distributor process in front of all theme-specific processes, to direct incoming traffic to the correct SSR process. @@ -33,9 +33,9 @@ This will automatically build all active themes and configure [PM2][pm2] for run Overwriting configurations of the PWA is entirely done by environment variables. This approach was chosen to have the best possible compatibility when running the PWA either from the command line or in an orchestrator. -To [set environment variables in windows](https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/set_1) run for example `set SSR_HYBRID=true` on the command line before executing the `npm run` commands. +To [set environment variables in windows](https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/set_1), run, for example, `set SSR_HYBRID=true` on the command line before executing the `npm run` commands. -If the format is _any_, then the environment variable has to be set to any value to be active. +If the format is _any_, the environment variable has to be set to any value to be active. Setting it to `"false"` still counts as active. Only empty strings count as inactive. @@ -44,41 +44,41 @@ If the format is _switch_, the property is switched on by supplying `on`, `1`, ` All parameters are **case sensitive**. Make sure to use them as written in the table below. -| | parameter | format | comment | -| ------------------- | ------------------------------------------- | -------------------- | ----------------------------------------------------------------------------------------------- | -| **SSR Specific** | PORT | number | Port for running the application | -| | CONCURRENCY_SSR | number \| max | concurrency for SSR instances per theme (default: 2) | -| | CACHE_ICM_CALLS | recommended \| JSON | enable caching for ICM calls, see [Local ICM Cache](#local-icm-cache) (default: disabled) | -| **General** | ICM_BASE_URL | string | Sets the base URL for the ICM | -| | ICM_CHANNEL | string | Overrides the default channel | -| | ICM_APPLICATION | string | Overrides the default application | -| | FEATURES | comma-separated list | Overrides active features | -| | THEME | string | Overrides the default theme | -| | MULTI_SITE_LOCALE_MAP | JSON \| false | Used to map locales to [url modification parameters](../guides/multi-site-configurations.md) | -| | DEPLOY_URL | string | Set a [Deploy URL][concept-deploy-url] (default `/`) | -| **Debug** :warning: | TRUST_ICM | any | Use this if ICM is deployed with an insecure certificate | -| | LOGGING | switch | Enables extra log output | -| | SOURCE_MAPS | switch | Exposes source maps if activated | -| **Hybrid Approach** | SSR_HYBRID | any | Enables running PWA and ICM in [Hybrid Mode][concept-hybrid] | -| | SSR_HYBRID_BACKEND | URL | When running in K8S this contains the ICM WA service URL. For none K8S you can use ICM_BASE_URL | -| | PROXY_ICM | any \| URL | Proxy ICM via `/INTERSHOP` (enabled if SSR_HYBRID is active) | -| **Third party** | GTM_TOKEN | string | Token for Google Tag Manager | -| | GMA_KEY | string | API key for Google Maps | -| | SENTRY_DSN | string | Sentry DSN URL for using Sentry Error Monitor | -| | PROMETHEUS | switch | Exposes Prometheus metrics | -| | IDENTITY_PROVIDER ~~ICM_IDENTITY_PROVIDER~~ | string | ID of Identity Provider for [SSO][concept-sso] | -| | IDENTITY_PROVIDERS | JSON | Configuration of Identity Providers for [SSO][concept-sso] | +| | parameter | format | comment | +| ------------------- | ------------------------------------------- | -------------------- | ------------------------------------------------------------------------------------------------ | +| **SSR Specific** | PORT | number | Port for running the application | +| | CONCURRENCY_SSR | number \| max | Concurrency for SSR instances per theme (default: 2) | +| | CACHE_ICM_CALLS | recommended \| JSON | Enable caching for ICM calls, see [Local ICM Cache](#local-icm-cache) (default: disabled) | +| **General** | ICM_BASE_URL | string | Sets the base URL for the ICM | +| | ICM_CHANNEL | string | Overrides the default channel | +| | ICM_APPLICATION | string | Overrides the default application | +| | FEATURES | comma-separated list | Overrides active features | +| | THEME | string | Overrides the default theme | +| | MULTI_SITE_LOCALE_MAP | JSON \| false | Used to map locales to [url modification parameters](../guides/multi-site-configurations.md) | +| | DEPLOY_URL | string | Set a [Deploy URL][concept-deploy-url] (default `/`) | +| **Debug** :warning: | TRUST_ICM | any | Use this if ICM is deployed with an insecure certificate | +| | LOGGING | switch | Enables extra log output | +| | SOURCE_MAPS | switch | Exposes source maps if activated | +| **Hybrid Approach** | SSR_HYBRID | any | Enables running PWA and ICM in [Hybrid Mode][concept-hybrid] | +| | SSR_HYBRID_BACKEND | URL | When running in K8S, this contains the ICM WA service URL. For none K8S you can use ICM_BASE_URL | +| | PROXY_ICM | any \| URL | Proxy ICM via `/INTERSHOP` (enabled if SSR_HYBRID is active) | +| **Third party** | GTM_TOKEN | string | Token for Google Tag Manager | +| | GMA_KEY | string | API key for Google Maps | +| | SENTRY_DSN | string | Sentry DSN URL for using Sentry Error Monitor | +| | PROMETHEUS | switch | Exposes Prometheus metrics | +| | IDENTITY_PROVIDER ~~ICM_IDENTITY_PROVIDER~~ | string | ID of the default Identity Provider if other than `ICM` | +| | IDENTITY_PROVIDERS | JSON | Configuration of additional Identity Providers besides the default `ICM` | ## Development For live Angular Universal (SSR) development, you have to use means provided by Angular CLI. -The following command starts a SSR development environment. +The following command starts an SSR development environment. ``` npm run start:ssr-dev ``` -If the SSR development environment needs to run with `https` this can be achieved like this. +If the SSR development environment needs to run with `https`, this can be achieved like this: ``` npm run start:ssr-dev -- --ssl @@ -105,17 +105,16 @@ You can further customize the caching by supplying a JSON structure to the `CACH } ``` -This example will cache `/configurations` for 20 Minutes, product variations for 2 hours and everything else for 2 Minutes. +This example will cache `/configurations` for 20 minutes, product variations for 2 hours, and everything else for 2 minutes. This feature can also be used to benchmark the SSR render performance locally by caching all ICM calls. -# Further References +## Further References - [Concept - Configuration](../concepts/configuration.md) - [Concept - Deploy URL][concept-deploy-url] - [Concept - Hybrid Approach][concept-hybrid] - [Concept - Logging](../concepts/logging.md) -- [Concept - Single Sign-On (SSO) for PWA][concept-sso] - [Guide - Multiple Themes][multiple-themes] - [Guide - Client-Side Error Monitoring with Sentry](./sentry-error-monitoring.md) - [Guide - Google Tag Manager](./google-tag-manager.md) @@ -124,7 +123,6 @@ This feature can also be used to benchmark the SSR render performance locally by - [YouTube - Server Side Rendering and Pre Rendering with Angular Universal](https://www.youtube.com/watch?v=-VDOAjzLcvQ) - [Google Developers - Rendering on the Web](https://developers.google.com/web/updates/2019/02/rendering-on-the-web) -[concept-sso]: ../concepts/sso.md [concept-hybrid]: ../concepts/hybrid-approach.md [concept-deploy-url]: ../concepts/deploy-url.md [multiple-themes]: ./multiple-themes.md From 5887c413aeb3c0b20bd804a7a0384475d14a8cb7 Mon Sep 17 00:00:00 2001 From: Marcel Eisentraut Date: Thu, 22 Dec 2022 14:26:07 +0100 Subject: [PATCH 37/39] fix: apply ordered-import rule on multiline import statements (#1344) --- eslint-rules/src/rules/ordered-imports.ts | 9 ++++++++- src/app/core/facades/account.facade.ts | 2 +- .../store/core/configuration/configuration.effects.ts | 2 +- .../store/customer/addresses/addresses.effects.spec.ts | 4 ++-- .../core/store/customer/addresses/addresses.effects.ts | 6 +++--- .../store/customer/basket/basket-items.effects.spec.ts | 2 +- .../core/store/customer/basket/basket-items.effects.ts | 2 +- src/app/core/store/customer/basket/basket.reducer.ts | 2 +- src/app/core/store/shopping/products/products.effects.ts | 8 ++++---- src/app/core/utils/operators.ts | 2 +- .../exports/lazy-captcha/lazy-captcha.component.ts | 2 +- .../extensions/quoting/store/quoting/quoting.effects.ts | 6 +++--- 12 files changed, 27 insertions(+), 20 deletions(-) diff --git a/eslint-rules/src/rules/ordered-imports.ts b/eslint-rules/src/rules/ordered-imports.ts index 7793168dc5..5dddf491ec 100644 --- a/eslint-rules/src/rules/ordered-imports.ts +++ b/eslint-rules/src/rules/ordered-imports.ts @@ -35,7 +35,14 @@ const orderedImportsRule: TSESLint.RuleModule = { .map(imp => context.getSourceCode().getText(imp)) .sort(); - return nodeText.replace(/\{.*\}/, `{ ${sortedNamedImports.join(', ')} }`); + if (/\{.*\}/.test(nodeText)) { + return nodeText.replace(/\{.*\}/, `{ ${sortedNamedImports.join(', ')} }`); + } + + return nodeText.replace( + /\{(.|\n)*\}/, + `{${lineEnding} ${sortedNamedImports.join(`,${lineEnding} `)},${lineEnding}}` + ); } const importDeclarations: TSESTree.ImportDeclaration[] = []; diff --git a/src/app/core/facades/account.facade.ts b/src/app/core/facades/account.facade.ts index 9d9dcf429a..97844f1e59 100644 --- a/src/app/core/facades/account.facade.ts +++ b/src/app/core/facades/account.facade.ts @@ -15,11 +15,11 @@ import { MessagesPayloadType } from 'ish-core/store/core/messages'; import { createCustomerAddress, deleteCustomerAddress, - updateCustomerAddress, getAddressesError, getAddressesLoading, getAllAddresses, loadAddresses, + updateCustomerAddress, } from 'ish-core/store/customer/addresses'; import { getUserRoles } from 'ish-core/store/customer/authorization'; import { diff --git a/src/app/core/store/core/configuration/configuration.effects.ts b/src/app/core/store/core/configuration/configuration.effects.ts index 06a16d63a4..3a315fc280 100644 --- a/src/app/core/store/core/configuration/configuration.effects.ts +++ b/src/app/core/store/core/configuration/configuration.effects.ts @@ -16,8 +16,8 @@ import { distinctCompareWith, mapToPayload, whenTruthy } from 'ish-core/utils/op import { StatePropertiesService } from 'ish-core/utils/state-transfer/state-properties.service'; import { - applyConfiguration, ConfigurationType, + applyConfiguration, loadSingleServerTranslation, loadSingleServerTranslationSuccess, } from './configuration.actions'; diff --git a/src/app/core/store/customer/addresses/addresses.effects.spec.ts b/src/app/core/store/customer/addresses/addresses.effects.spec.ts index 398b96fb5f..46597c97c0 100644 --- a/src/app/core/store/customer/addresses/addresses.effects.spec.ts +++ b/src/app/core/store/customer/addresses/addresses.effects.spec.ts @@ -21,11 +21,11 @@ import { deleteCustomerAddress, deleteCustomerAddressFail, deleteCustomerAddressSuccess, + loadAddresses, + loadAddressesSuccess, updateCustomerAddress, updateCustomerAddressFail, updateCustomerAddressSuccess, - loadAddresses, - loadAddressesSuccess, } from './addresses.actions'; import { AddressesEffects } from './addresses.effects'; diff --git a/src/app/core/store/customer/addresses/addresses.effects.ts b/src/app/core/store/customer/addresses/addresses.effects.ts index 7f5e5add04..e47fc4c30e 100644 --- a/src/app/core/store/customer/addresses/addresses.effects.ts +++ b/src/app/core/store/customer/addresses/addresses.effects.ts @@ -12,15 +12,15 @@ import { createCustomerAddress, createCustomerAddressFail, createCustomerAddressSuccess, - updateCustomerAddress, - updateCustomerAddressFail, - updateCustomerAddressSuccess, deleteCustomerAddress, deleteCustomerAddressFail, deleteCustomerAddressSuccess, loadAddresses, loadAddressesFail, loadAddressesSuccess, + updateCustomerAddress, + updateCustomerAddressFail, + updateCustomerAddressSuccess, } from './addresses.actions'; @Injectable() diff --git a/src/app/core/store/customer/basket/basket-items.effects.spec.ts b/src/app/core/store/customer/basket/basket-items.effects.spec.ts index 4477d42041..a0931347a7 100644 --- a/src/app/core/store/customer/basket/basket-items.effects.spec.ts +++ b/src/app/core/store/customer/basket/basket-items.effects.spec.ts @@ -31,10 +31,10 @@ import { loadBasketSuccess, updateBasketItem, updateBasketItemFail, + updateBasketItemSuccess, updateBasketItems, updateBasketItemsFail, updateBasketItemsSuccess, - updateBasketItemSuccess, validateBasket, } from './basket.actions'; diff --git a/src/app/core/store/customer/basket/basket-items.effects.ts b/src/app/core/store/customer/basket/basket-items.effects.ts index aeb3e45663..4b24d8304f 100644 --- a/src/app/core/store/customer/basket/basket-items.effects.ts +++ b/src/app/core/store/customer/basket/basket-items.effects.ts @@ -38,10 +38,10 @@ import { loadBasket, updateBasketItem, updateBasketItemFail, + updateBasketItemSuccess, updateBasketItems, updateBasketItemsFail, updateBasketItemsSuccess, - updateBasketItemSuccess, validateBasket, } from './basket.actions'; import { getCurrentBasket, getCurrentBasketId } from './basket.selectors'; diff --git a/src/app/core/store/customer/basket/basket.reducer.ts b/src/app/core/store/customer/basket/basket.reducer.ts index 385a8ad874..81133ee51a 100644 --- a/src/app/core/store/customer/basket/basket.reducer.ts +++ b/src/app/core/store/customer/basket/basket.reducer.ts @@ -73,10 +73,10 @@ import { updateBasketFail, updateBasketItem, updateBasketItemFail, + updateBasketItemSuccess, updateBasketItems, updateBasketItemsFail, updateBasketItemsSuccess, - updateBasketItemSuccess, updateBasketPayment, updateBasketPaymentFail, updateBasketPaymentSuccess, diff --git a/src/app/core/store/shopping/products/products.effects.ts b/src/app/core/store/shopping/products/products.effects.ts index 66dd6b7869..ebbad2d60d 100644 --- a/src/app/core/store/shopping/products/products.effects.ts +++ b/src/app/core/store/shopping/products/products.effects.ts @@ -48,14 +48,14 @@ import { loadProductLinksSuccess, loadProductParts, loadProductPartsSuccess, - loadProductsForCategory, - loadProductsForCategoryFail, - loadProductsForMaster, - loadProductsForMasterFail, loadProductSuccess, loadProductVariationsFail, loadProductVariationsIfNotLoaded, loadProductVariationsSuccess, + loadProductsForCategory, + loadProductsForCategoryFail, + loadProductsForMaster, + loadProductsForMasterFail, } from './products.actions'; import { getBreadcrumbForProductPage, diff --git a/src/app/core/utils/operators.ts b/src/app/core/utils/operators.ts index d410b3cbd2..9646f947ca 100644 --- a/src/app/core/utils/operators.ts +++ b/src/app/core/utils/operators.ts @@ -7,9 +7,9 @@ import { OperatorFunction, combineLatest, concat, + connect, of, throwError, - connect, } from 'rxjs'; import { buffer, catchError, distinctUntilChanged, filter, map, mergeAll, take, withLatestFrom } from 'rxjs/operators'; diff --git a/src/app/extensions/captcha/exports/lazy-captcha/lazy-captcha.component.ts b/src/app/extensions/captcha/exports/lazy-captcha/lazy-captcha.component.ts index bbc8f609c2..1ee594771a 100644 --- a/src/app/extensions/captcha/exports/lazy-captcha/lazy-captcha.component.ts +++ b/src/app/extensions/captcha/exports/lazy-captcha/lazy-captcha.component.ts @@ -2,13 +2,13 @@ import { AfterViewInit, ChangeDetectionStrategy, Component, - createNgModule, Injector, Input, OnDestroy, OnInit, ViewChild, ViewContainerRef, + createNgModule, } from '@angular/core'; import { AbstractControl, FormArray, FormGroup, Validators } from '@angular/forms'; import { Subject } from 'rxjs'; diff --git a/src/app/extensions/quoting/store/quoting/quoting.effects.ts b/src/app/extensions/quoting/store/quoting/quoting.effects.ts index 657b5fefd2..536afdc0f7 100644 --- a/src/app/extensions/quoting/store/quoting/quoting.effects.ts +++ b/src/app/extensions/quoting/store/quoting/quoting.effects.ts @@ -32,6 +32,9 @@ import { createQuoteRequestFromQuoteRequest, createQuoteRequestFromQuoteRequestSuccess, createQuoteRequestFromQuoteSuccess, + deleteQuoteFromBasket, + deleteQuoteFromBasketFail, + deleteQuoteFromBasketSuccess, deleteQuotingEntity, deleteQuotingEntityFail, deleteQuotingEntitySuccess, @@ -46,9 +49,6 @@ import { submitQuoteRequestSuccess, updateQuoteRequest, updateQuoteRequestSuccess, - deleteQuoteFromBasket, - deleteQuoteFromBasketSuccess, - deleteQuoteFromBasketFail, } from './quoting.actions'; import { getQuotingEntity } from './quoting.selectors'; From 935962651024296169dd67d6b35be47e5b713083 Mon Sep 17 00:00:00 2001 From: Stefan Hauke Date: Thu, 22 Dec 2022 17:14:34 +0100 Subject: [PATCH 38/39] chore: customize '@commitlint/config-conventional' commit linting * adapted rules of `@commitlint/config-conventional` to be less restrictive regarding length * customized `type-enum`to fit `.cz-config.js` --- .cz-config.js | 2 +- package.json | 28 +++++++++++++++++++++++++++- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/.cz-config.js b/.cz-config.js index cb2e19d3a5..36200eaba7 100644 --- a/.cz-config.js +++ b/.cz-config.js @@ -53,5 +53,5 @@ module.exports = { breaklineChar: '|', // It is supported for fields body and footer. footerPrefix: 'Closes ', - breakingPrefix: 'BREAKING CHANGES: ', + breakingPrefix: 'BREAKING CHANGE: ', }; diff --git a/package.json b/package.json index 7221bf76a1..6bf10f639a 100644 --- a/package.json +++ b/package.json @@ -221,7 +221,33 @@ "commitlint": { "extends": [ "@commitlint/config-conventional" - ] + ], + "rules": { + "body-leading-blank": [2, "always"], + "body-max-line-length": [2, "always", 400], + "footer-leading-blank": [2, "always"], + "footer-max-line-length": [2, "always", 400], + "header-max-length": [2, "always", 200], + "type-enum": [ + 2, + "always", + [ + "feat", + "fix", + "perf", + "docs", + "style", + "i18n", + "refactor", + "test", + "build", + "deps", + "ci", + "chore", + "temp" + ] + ] + } }, "config": { "commitizen": { From ed84bd32778619087d044460ab43b8e2e3fa56c0 Mon Sep 17 00:00:00 2001 From: Stefan Hauke Date: Thu, 22 Dec 2022 17:25:52 +0100 Subject: [PATCH 39/39] chore: release 3.2.0 preparation --- 3rd-party-licenses.txt | 399 ++++++++++++++++++++------------------ CHANGELOG.md | 53 +++++ docs/guides/migrations.md | 4 +- package-lock.json | 4 +- package.json | 4 +- 5 files changed, 267 insertions(+), 197 deletions(-) diff --git a/3rd-party-licenses.txt b/3rd-party-licenses.txt index cf5dfd7912..c7285aaf42 100644 --- a/3rd-party-licenses.txt +++ b/3rd-party-licenses.txt @@ -2,36 +2,36 @@ "@adobe/css-tools@4.0.1","MIT","Copyright (c) 2012 TJ Holowaychuk . Copyright (c) 2022 Jean-Philippe Zolesio ","https://github.com/adobe/css-tools" "@aduh95/viz.js@3.7.0","MIT","Copyright (c) 2014-2018 Michael Daines","https://github.com/aduh95/viz.js" "@ampproject/remapping@2.2.0","Apache-2.0","","https://github.com/ampproject/remapping" -"@angular-builders/custom-webpack@14.0.1","MIT","Copyright (c) 2018 Evgeny Barabanov","https://github.com/just-jeb/angular-builders" -"@angular-devkit/architect@0.1402.5","MIT","Copyright (c) 2017 Google, Inc.","https://github.com/angular/angular-cli" -"@angular-devkit/build-angular@14.2.5","MIT","Copyright (c) 2017 Google, Inc.","https://github.com/angular/angular-cli" -"@angular-devkit/build-webpack@0.1402.5","MIT","Copyright (c) 2017 Google, Inc.","https://github.com/angular/angular-cli" +"@angular-builders/custom-webpack@14.1.0","MIT","Copyright (c) 2018 Evgeny Barabanov","https://github.com/just-jeb/angular-builders" +"@angular-devkit/architect@0.1402.10","MIT","Copyright (c) 2017 Google, Inc.","https://github.com/angular/angular-cli" +"@angular-devkit/build-angular@14.2.10","MIT","Copyright (c) 2017 Google, Inc.","https://github.com/angular/angular-cli" +"@angular-devkit/build-webpack@0.1402.10","MIT","Copyright (c) 2017 Google, Inc.","https://github.com/angular/angular-cli" "@angular-devkit/core@13.3.9","MIT","Copyright (c) 2017 Google, Inc.","https://github.com/angular/angular-cli" -"@angular-devkit/core@14.2.5","MIT","Copyright (c) 2017 Google, Inc.","https://github.com/angular/angular-cli" +"@angular-devkit/core@14.2.10","MIT","Copyright (c) 2017 Google, Inc.","https://github.com/angular/angular-cli" "@angular-devkit/schematics@13.3.9","MIT","Copyright (c) 2017 Google, Inc.","https://github.com/angular/angular-cli" -"@angular-devkit/schematics@14.2.5","MIT","Copyright (c) 2017 Google, Inc.","https://github.com/angular/angular-cli" -"@angular-eslint/builder@14.1.2","MIT","Copyright (c) 2020 James Henry","https://github.com/angular-eslint/angular-eslint" -"@angular-eslint/bundled-angular-compiler@14.1.2","MIT","Copyright (c) 2020 James Henry","https://github.com/angular-eslint/angular-eslint" -"@angular-eslint/eslint-plugin-template@14.1.2","MIT","Copyright (c) 2020 James Henry","https://github.com/angular-eslint/angular-eslint" -"@angular-eslint/eslint-plugin@14.1.2","MIT","Copyright (c) 2020 James Henry","https://github.com/angular-eslint/angular-eslint" -"@angular-eslint/schematics@14.1.2","MIT","Copyright (c) 2020 James Henry","https://github.com/angular-eslint/angular-eslint" -"@angular-eslint/template-parser@14.1.2","MIT","Copyright (c) 2020 James Henry","https://github.com/angular-eslint/angular-eslint" -"@angular-eslint/utils@14.1.2","MIT","Copyright (c) 2020 James Henry","" -"@angular/animations@14.2.5","MIT","","https://github.com/angular/angular" -"@angular/cdk@14.2.4","MIT","Copyright (c) 2022 Google LLC.","https://github.com/angular/components" -"@angular/cli@14.2.5","MIT","Copyright (c) 2017 Google, Inc.","https://github.com/angular/angular-cli" -"@angular/common@14.2.5","MIT","","https://github.com/angular/angular" -"@angular/compiler-cli@14.2.5","MIT","","https://github.com/angular/angular" -"@angular/compiler@14.2.5","MIT","","https://github.com/angular/angular" -"@angular/core@14.2.5","MIT","","https://github.com/angular/angular" -"@angular/forms@14.2.5","MIT","","https://github.com/angular/angular" -"@angular/language-service@14.2.5","MIT","","https://github.com/angular/angular" -"@angular/localize@14.2.5","MIT","","https://github.com/angular/angular" -"@angular/platform-browser-dynamic@14.2.5","MIT","","https://github.com/angular/angular" -"@angular/platform-browser@14.2.5","MIT","","https://github.com/angular/angular" -"@angular/platform-server@14.2.5","MIT","","https://github.com/angular/angular" -"@angular/router@14.2.5","MIT","","https://github.com/angular/angular" -"@angular/service-worker@14.2.5","MIT","","https://github.com/angular/angular" +"@angular-devkit/schematics@14.2.10","MIT","Copyright (c) 2017 Google, Inc.","https://github.com/angular/angular-cli" +"@angular-eslint/builder@14.4.0","MIT","Copyright (c) 2020 James Henry","https://github.com/angular-eslint/angular-eslint" +"@angular-eslint/bundled-angular-compiler@14.4.0","MIT","Copyright (c) 2020 James Henry","https://github.com/angular-eslint/angular-eslint" +"@angular-eslint/eslint-plugin-template@14.4.0","MIT","Copyright (c) 2020 James Henry","https://github.com/angular-eslint/angular-eslint" +"@angular-eslint/eslint-plugin@14.4.0","MIT","Copyright (c) 2020 James Henry","https://github.com/angular-eslint/angular-eslint" +"@angular-eslint/schematics@14.4.0","MIT","Copyright (c) 2020 James Henry","https://github.com/angular-eslint/angular-eslint" +"@angular-eslint/template-parser@14.4.0","MIT","Copyright (c) 2020 James Henry","https://github.com/angular-eslint/angular-eslint" +"@angular-eslint/utils@14.4.0","MIT","Copyright (c) 2020 James Henry","" +"@angular/animations@14.2.12","MIT","","https://github.com/angular/angular" +"@angular/cdk@14.2.7","MIT","Copyright (c) 2022 Google LLC.","https://github.com/angular/components" +"@angular/cli@14.2.10","MIT","Copyright (c) 2017 Google, Inc.","https://github.com/angular/angular-cli" +"@angular/common@14.2.12","MIT","","https://github.com/angular/angular" +"@angular/compiler-cli@14.2.12","MIT","","https://github.com/angular/angular" +"@angular/compiler@14.2.12","MIT","","https://github.com/angular/angular" +"@angular/core@14.2.12","MIT","","https://github.com/angular/angular" +"@angular/forms@14.2.12","MIT","","https://github.com/angular/angular" +"@angular/language-service@14.2.12","MIT","","https://github.com/angular/angular" +"@angular/localize@14.2.12","MIT","","https://github.com/angular/angular" +"@angular/platform-browser-dynamic@14.2.12","MIT","","https://github.com/angular/angular" +"@angular/platform-browser@14.2.12","MIT","","https://github.com/angular/angular" +"@angular/platform-server@14.2.12","MIT","","https://github.com/angular/angular" +"@angular/router@14.2.12","MIT","","https://github.com/angular/angular" +"@angular/service-worker@14.2.12","MIT","","https://github.com/angular/angular" "@assemblyscript/loader@0.10.1","Apache-2.0","","https://github.com/AssemblyScript/assemblyscript" "@babel/code-frame@7.18.6","MIT","Copyright (c) 2014-present Sebastian McKenzie and other contributors","https://github.com/babel/babel" "@babel/compat-data@7.19.3","MIT","Copyright (c) 2014-present Sebastian McKenzie and other contributors","https://github.com/babel/babel" @@ -143,19 +143,20 @@ "@babel/types@7.19.3","MIT","Copyright (c) 2014-present Sebastian McKenzie and other contributors","https://github.com/babel/babel" "@bcherny/json-schema-ref-parser@9.0.9","MIT","Copyright (c) 2015 James Messinger","https://github.com/APIDevTools/json-schema-ref-parser" "@bcoe/v8-coverage@0.2.3","MIT","Copyright © 2015-2017 Charles Samborski","https://github.com/demurgos/v8-coverage" -"@commitlint/cli@17.1.2","MIT","Copyright (c) 2016 - present Mario Nebl","https://github.com/conventional-changelog/commitlint" +"@commitlint/cli@17.3.0","MIT","Copyright (c) 2016 - present Mario Nebl","https://github.com/conventional-changelog/commitlint" +"@commitlint/config-conventional@17.3.0","MIT","Copyright (c) 2016 - present Mario Nebl","https://github.com/conventional-changelog/commitlint" "@commitlint/config-validator@17.1.0","MIT","Copyright (c) 2016 - present Mario Nebl","https://github.com/conventional-changelog/commitlint" -"@commitlint/ensure@17.0.0","MIT","Copyright (c) 2016 - present Mario Nebl","https://github.com/conventional-changelog/commitlint" +"@commitlint/ensure@17.3.0","MIT","Copyright (c) 2016 - present Mario Nebl","https://github.com/conventional-changelog/commitlint" "@commitlint/execute-rule@17.0.0","MIT","Copyright (c) 2016 - present Mario Nebl","https://github.com/conventional-changelog/commitlint" "@commitlint/format@17.0.0","MIT","Copyright (c) 2016 - present Mario Nebl","https://github.com/conventional-changelog/commitlint" -"@commitlint/is-ignored@17.1.0","MIT","Copyright (c) 2016 - present Mario Nebl","https://github.com/conventional-changelog/commitlint" -"@commitlint/lint@17.1.0","MIT","Copyright (c) 2016 - present Mario Nebl","https://github.com/conventional-changelog/commitlint" -"@commitlint/load@17.1.2","MIT","Copyright (c) 2016 - present Mario Nebl","https://github.com/conventional-changelog/commitlint" -"@commitlint/message@17.0.0","MIT","Copyright (c) 2016 - present Mario Nebl","https://github.com/conventional-changelog/commitlint" -"@commitlint/parse@17.0.0","MIT","Copyright (c) 2016 - present Mario Nebl","https://github.com/conventional-changelog/commitlint" -"@commitlint/read@17.1.0","MIT","Copyright (c) 2016 - present Mario Nebl","https://github.com/conventional-changelog/commitlint" -"@commitlint/resolve-extends@17.1.0","MIT","Copyright (c) 2016 - present Mario Nebl","https://github.com/conventional-changelog/commitlint" -"@commitlint/rules@17.0.0","MIT","Copyright (c) 2016 - present Mario Nebl","https://github.com/conventional-changelog/commitlint" +"@commitlint/is-ignored@17.2.0","MIT","Copyright (c) 2016 - present Mario Nebl","https://github.com/conventional-changelog/commitlint" +"@commitlint/lint@17.3.0","MIT","Copyright (c) 2016 - present Mario Nebl","https://github.com/conventional-changelog/commitlint" +"@commitlint/load@17.3.0","MIT","Copyright (c) 2016 - present Mario Nebl","https://github.com/conventional-changelog/commitlint" +"@commitlint/message@17.2.0","MIT","Copyright (c) 2016 - present Mario Nebl","https://github.com/conventional-changelog/commitlint" +"@commitlint/parse@17.2.0","MIT","Copyright (c) 2016 - present Mario Nebl","https://github.com/conventional-changelog/commitlint" +"@commitlint/read@17.2.0","MIT","Copyright (c) 2016 - present Mario Nebl","https://github.com/conventional-changelog/commitlint" +"@commitlint/resolve-extends@17.3.0","MIT","Copyright (c) 2016 - present Mario Nebl","https://github.com/conventional-changelog/commitlint" +"@commitlint/rules@17.3.0","MIT","Copyright (c) 2016 - present Mario Nebl","https://github.com/conventional-changelog/commitlint" "@commitlint/to-lines@17.0.0","MIT","Copyright (c) 2016 - present Mario Nebl","https://github.com/conventional-changelog/commitlint" "@commitlint/top-level@17.0.0","MIT","Copyright (c) 2016 - present Mario Nebl","https://github.com/conventional-changelog/commitlint" "@commitlint/types@17.0.0","MIT","Copyright (c) 2016 - present Mario Nebl","https://github.com/conventional-changelog/commitlint" @@ -163,54 +164,56 @@ "@compodoc/live-server@1.2.3","MIT","Copyright (c) 2012 Tapio Vierros","https://github.com/compodoc/live-server" "@compodoc/ngd-core@2.1.0","MIT","","https://github.com/compodoc/ngd" "@compodoc/ngd-transformer@2.1.0","MIT","","https://github.com/compodoc/ngd" -"@cspell/cspell-bundled-dicts@6.12.0","MIT","Copyright (c) 2017 Jason Dent","https://github.com/streetsidesoftware/cspell" -"@cspell/cspell-pipe@6.12.0","MIT","Copyright (c) 2017 Jason Dent","https://github.com/streetsidesoftware/cspell" -"@cspell/cspell-service-bus@6.12.0","MIT","Copyright (c) 2017 Jason Dent","https://github.com/streetsidesoftware/cspell" -"@cspell/cspell-types@6.12.0","MIT","Copyright (c) 2017 Jason Dent","https://github.com/streetsidesoftware/cspell" -"@cspell/dict-ada@2.0.1","MIT","Copyright (c) 2017-2020 Jason Dent ","https://github.com/streetsidesoftware/cspell-dicts" -"@cspell/dict-aws@2.0.0","MIT","Copyright (c) 2019 Daniel Schroeder . Copyright (c) 2017-2020 Jason Dent ","https://github.com/streetsidesoftware/cspell-dicts" -"@cspell/dict-bash@2.0.4","MIT","Copyright (c) 2017-2020 Jason Dent ","https://github.com/streetsidesoftware/cspell-dicts" -"@cspell/dict-companies@2.0.14","MIT","Copyright (c) 2017-2020 Jason Dent ","https://github.com/streetsidesoftware/cspell-dicts" -"@cspell/dict-cpp@3.2.1","MIT","Copyright (c) 2017-2020 Jason Dent ","https://github.com/streetsidesoftware/cspell-dicts" -"@cspell/dict-cryptocurrencies@2.0.0","MIT","Copyright (c) 2020 Shane Fontaine . Copyright (c) 2020 Jason Dent ","https://github.com/streetsidesoftware/cspell-dicts" -"@cspell/dict-csharp@3.0.1","MIT","Copyright (c) 2017-2020 Jason Dent ","https://github.com/streetsidesoftware/cspell-dicts" -"@cspell/dict-css@2.1.0","MIT","Copyright (c) 2017-2020 Jason Dent ","https://github.com/streetsidesoftware/cspell-dicts" -"@cspell/dict-dart@1.1.1","MIT","Copyright (c) 2017-2020 Jason Dent ","https://github.com/streetsidesoftware/cspell-dicts" +"@cspell/cspell-bundled-dicts@6.17.0","MIT","Copyright (c) 2017 Jason Dent","https://github.com/streetsidesoftware/cspell" +"@cspell/cspell-pipe@6.17.0","MIT","Copyright (c) 2017 Jason Dent","https://github.com/streetsidesoftware/cspell" +"@cspell/cspell-service-bus@6.17.0","MIT","Copyright (c) 2017 Jason Dent","https://github.com/streetsidesoftware/cspell" +"@cspell/cspell-types@6.17.0","MIT","Copyright (c) 2017 Jason Dent","https://github.com/streetsidesoftware/cspell" +"@cspell/dict-ada@4.0.0","MIT","Copyright (c) 2017-2020 Jason Dent ","https://github.com/streetsidesoftware/cspell-dicts" +"@cspell/dict-aws@3.0.0","MIT","Copyright (c) 2019 Daniel Schroeder . Copyright (c) 2017-2020 Jason Dent ","https://github.com/streetsidesoftware/cspell-dicts" +"@cspell/dict-bash@4.1.0","MIT","Copyright (c) 2017-2020 Jason Dent ","https://github.com/streetsidesoftware/cspell-dicts" +"@cspell/dict-companies@3.0.3","MIT","Copyright (c) 2017-2020 Jason Dent ","https://github.com/streetsidesoftware/cspell-dicts" +"@cspell/dict-cpp@4.0.0","MIT","Copyright (c) 2017-2020 Jason Dent ","https://github.com/streetsidesoftware/cspell-dicts" +"@cspell/dict-cryptocurrencies@3.0.1","MIT","Copyright (c) 2020 Shane Fontaine . Copyright (c) 2020 Jason Dent ","https://github.com/streetsidesoftware/cspell-dicts" +"@cspell/dict-csharp@4.0.2","MIT","Copyright (c) 2017-2020 Jason Dent ","https://github.com/streetsidesoftware/cspell-dicts" +"@cspell/dict-css@4.0.0","MIT","Copyright (c) 2017-2020 Jason Dent ","https://github.com/streetsidesoftware/cspell-dicts" +"@cspell/dict-dart@2.0.0","MIT","Copyright (c) 2017-2020 Jason Dent ","https://github.com/streetsidesoftware/cspell-dicts" "@cspell/dict-de-de@1.1.32","MIT","Copyright (c) 2017-2020 Jason Dent ","https://github.com/streetsidesoftware/cspell-dicts" -"@cspell/dict-django@2.0.0","MIT","Copyright (c) 2017-2020 Renaud Canarduck . Copyright (c) 2017-2020 Jason Dent ","https://github.com/streetsidesoftware/cspell-dicts" -"@cspell/dict-docker@1.1.1","MIT","Copyright (c) 2022 Jason Dent ","https://github.com/streetsidesoftware/cspell-dicts" -"@cspell/dict-dotnet@2.0.1","MIT","Copyright (c) 2017-2020 Jason Dent ","https://github.com/streetsidesoftware/cspell-dicts" -"@cspell/dict-elixir@2.0.1","MIT","Copyright (c) 2018 Todoroki (Ryo Konishi) . Copyright (c) 2017-2020 Jason Dent ","https://github.com/streetsidesoftware/cspell-dicts" +"@cspell/dict-django@4.0.0","MIT","Copyright (c) 2017-2020 Renaud Canarduck . Copyright (c) 2017-2020 Jason Dent ","https://github.com/streetsidesoftware/cspell-dicts" +"@cspell/dict-docker@1.1.3","MIT","Copyright (c) 2022 Jason Dent ","https://github.com/streetsidesoftware/cspell-dicts" +"@cspell/dict-dotnet@4.0.0","MIT","Copyright (c) 2017-2020 Jason Dent ","https://github.com/streetsidesoftware/cspell-dicts" +"@cspell/dict-elixir@4.0.0","MIT","Copyright (c) 2018 Todoroki (Ryo Konishi) . Copyright (c) 2017-2020 Jason Dent ","https://github.com/streetsidesoftware/cspell-dicts" "@cspell/dict-en-gb@1.1.33","MIT","Copyright (c) 2017-2020 Jason Dent ","https://github.com/streetsidesoftware/cspell-dicts" -"@cspell/dict-en_us@2.3.3","MIT","Copyright (c) 2017-2020 Jason Dent ","https://github.com/streetsidesoftware/cspell-dicts" -"@cspell/dict-filetypes@2.1.1","MIT","Copyright (c) 2017-2020 Jason Dent ","https://github.com/streetsidesoftware/cspell-dicts" -"@cspell/dict-fonts@2.1.0","MIT","Copyright (c) 2017-2020 Jason Dent ","https://github.com/streetsidesoftware/cspell-dicts" +"@cspell/dict-en_us@4.1.0","MIT","Copyright (c) 2017-2020 Jason Dent ","https://github.com/streetsidesoftware/cspell-dicts" +"@cspell/dict-filetypes@3.0.0","MIT","Copyright (c) 2017-2020 Jason Dent ","https://github.com/streetsidesoftware/cspell-dicts" +"@cspell/dict-fonts@3.0.0","MIT","Copyright (c) 2017-2020 Jason Dent ","https://github.com/streetsidesoftware/cspell-dicts" "@cspell/dict-fr-fr@2.1.1","MIT","Copyright (c) 2017-2020 Jason Dent ","https://github.com/streetsidesoftware/cspell-dicts" -"@cspell/dict-fullstack@2.0.6","MIT","Copyright (c) 2017-2020 Jason Dent ","https://github.com/streetsidesoftware/cspell-dicts" -"@cspell/dict-git@1.0.1","MIT","Copyright (c) 2017-2022 Jason Dent ","https://github.com/streetsidesoftware/cspell-dicts" -"@cspell/dict-golang@3.0.1","MIT","Copyright (c) 2017-2020 Jason Dent ","https://github.com/streetsidesoftware/cspell-dicts" -"@cspell/dict-haskell@2.0.1","MIT","Copyright (c) 2017-2020 Jason Dent ","https://github.com/streetsidesoftware/cspell-dicts" -"@cspell/dict-html-symbol-entities@3.0.0","MIT","Copyright (c) 2017-2020 Jason Dent ","https://github.com/streetsidesoftware/cspell-dicts" -"@cspell/dict-html@3.3.2","MIT","Copyright (c) 2017-2020 Jason Dent ","https://github.com/streetsidesoftware/cspell-dicts" -"@cspell/dict-java@3.0.7","MIT","Copyright (c) 2017-2020 Jason Dent ","https://github.com/streetsidesoftware/cspell-dicts" -"@cspell/dict-latex@2.0.9","MIT","Copyright (c) 2017-2020 Jason Dent ","https://github.com/streetsidesoftware/cspell-dicts" -"@cspell/dict-lorem-ipsum@2.0.1","MIT","Copyright (c) 2017-2020 Jason Dent ","https://github.com/streetsidesoftware/cspell-dicts" -"@cspell/dict-lua@2.0.0","MIT","Copyright (c) 2017-2020 Jason Dent ","https://github.com/streetsidesoftware/cspell-dicts" -"@cspell/dict-node@3.0.1","MIT","Copyright (c) 2017-2020 Jason Dent ","https://github.com/streetsidesoftware/cspell-dicts" -"@cspell/dict-npm@3.1.3","MIT","Copyright (c) 2017-2020 Jason Dent ","https://github.com/streetsidesoftware/cspell-dicts" -"@cspell/dict-php@2.0.0","MIT","Copyright (c) 2017-2020 Jason Dent ","https://github.com/streetsidesoftware/cspell-dicts" -"@cspell/dict-powershell@2.0.0","MIT","Copyright (c) 2017-2020 Jason Dent ","https://github.com/streetsidesoftware/cspell-dicts" -"@cspell/dict-public-licenses@1.0.6","MIT","Copyright (c) 2017-2020 Jason Dent ","https://github.com/streetsidesoftware/cspell-dicts" -"@cspell/dict-python@3.0.6","MIT","Copyright (c) 2017-2020 Jason Dent ","https://github.com/streetsidesoftware/cspell-dicts" -"@cspell/dict-r@1.0.3","MIT","Copyright (c) 2017-2020 Jason Dent , Matthew Toohey ","https://github.com/streetsidesoftware/cspell-dicts" -"@cspell/dict-ruby@2.0.2","MIT","Copyright (c) 2017-2020 Jason Dent ","https://github.com/streetsidesoftware/cspell-dicts" -"@cspell/dict-rust@2.0.1","MIT","Copyright (c) 2017-2020 Alexander Andreev . Copyright (c) 2017-2020 Jason Dent ","https://github.com/streetsidesoftware/cspell-dicts" -"@cspell/dict-scala@2.0.0","MIT","Copyright (c) 2017-2020 Jason Dent ","https://github.com/streetsidesoftware/cspell-dicts" -"@cspell/dict-software-terms@2.2.12","MIT","Copyright (c) 2017-2020 Jason Dent ","https://github.com/streetsidesoftware/cspell-dicts" -"@cspell/dict-sql@1.0.4","MIT","Copyright (c) 2017-2022 Jason Dent ","https://github.com/streetsidesoftware/cspell-dicts" -"@cspell/dict-swift@1.0.3","MIT","Copyright (c) 2017-2020 Jason Dent ","https://github.com/streetsidesoftware/cspell-dicts" -"@cspell/dict-typescript@2.0.2","MIT","Copyright (c) 2017-2020 Jason Dent ","https://github.com/streetsidesoftware/cspell-dicts" -"@cspell/dict-vue@2.0.2","MIT","Copyright (c) 2017-2020 Jason Dent ","https://github.com/streetsidesoftware/cspell-dicts" +"@cspell/dict-fullstack@3.0.0","MIT","Copyright (c) 2017-2020 Jason Dent ","https://github.com/streetsidesoftware/cspell-dicts" +"@cspell/dict-git@2.0.0","MIT","Copyright (c) 2017-2022 Jason Dent ","https://github.com/streetsidesoftware/cspell-dicts" +"@cspell/dict-golang@5.0.0","MIT","Copyright (c) 2017-2020 Jason Dent ","https://github.com/streetsidesoftware/cspell-dicts" +"@cspell/dict-haskell@4.0.0","MIT","Copyright (c) 2017-2020 Jason Dent ","https://github.com/streetsidesoftware/cspell-dicts" +"@cspell/dict-html-symbol-entities@4.0.0","MIT","Copyright (c) 2017-2020 Jason Dent ","https://github.com/streetsidesoftware/cspell-dicts" +"@cspell/dict-html@4.0.1","MIT","Copyright (c) 2017-2020 Jason Dent ","https://github.com/streetsidesoftware/cspell-dicts" +"@cspell/dict-java@5.0.2","MIT","Copyright (c) 2017-2020 Jason Dent ","https://github.com/streetsidesoftware/cspell-dicts" +"@cspell/dict-latex@3.0.0","MIT","Copyright (c) 2017-2020 Jason Dent ","https://github.com/streetsidesoftware/cspell-dicts" +"@cspell/dict-lorem-ipsum@3.0.0","MIT","Copyright (c) 2017-2020 Jason Dent ","https://github.com/streetsidesoftware/cspell-dicts" +"@cspell/dict-lua@3.0.0","MIT","Copyright (c) 2017-2020 Jason Dent ","https://github.com/streetsidesoftware/cspell-dicts" +"@cspell/dict-node@4.0.1","MIT","Copyright (c) 2017-2020 Jason Dent ","https://github.com/streetsidesoftware/cspell-dicts" +"@cspell/dict-npm@5.0.0","MIT","Copyright (c) 2017-2020 Jason Dent ","https://github.com/streetsidesoftware/cspell-dicts" +"@cspell/dict-php@3.0.3","MIT","Copyright (c) 2017-2020 Jason Dent ","https://github.com/streetsidesoftware/cspell-dicts" +"@cspell/dict-powershell@3.0.0","MIT","Copyright (c) 2017-2020 Jason Dent ","https://github.com/streetsidesoftware/cspell-dicts" +"@cspell/dict-public-licenses@2.0.0","MIT","Copyright (c) 2017-2020 Jason Dent ","https://github.com/streetsidesoftware/cspell-dicts" +"@cspell/dict-python@4.0.0","MIT","Copyright (c) 2017-2020 Jason Dent ","https://github.com/streetsidesoftware/cspell-dicts" +"@cspell/dict-r@2.0.0","MIT","Copyright (c) 2017-2020 Jason Dent , Matthew Toohey ","https://github.com/streetsidesoftware/cspell-dicts" +"@cspell/dict-ruby@3.0.0","MIT","Copyright (c) 2017-2020 Jason Dent ","https://github.com/streetsidesoftware/cspell-dicts" +"@cspell/dict-rust@3.0.0","MIT","Copyright (c) 2017-2020 Alexander Andreev . Copyright (c) 2017-2020 Jason Dent ","https://github.com/streetsidesoftware/cspell-dicts" +"@cspell/dict-scala@3.0.0","MIT","Copyright (c) 2017-2020 Jason Dent ","https://github.com/streetsidesoftware/cspell-dicts" +"@cspell/dict-software-terms@3.0.5","MIT","Copyright (c) 2017-2020 Jason Dent ","https://github.com/streetsidesoftware/cspell-dicts" +"@cspell/dict-sql@2.0.0","MIT","Copyright (c) 2017-2022 Jason Dent ","https://github.com/streetsidesoftware/cspell-dicts" +"@cspell/dict-svelte@1.0.0","MIT","Copyright (c) 2017-2022 Street Side Software ","https://github.com/streetsidesoftware/cspell-dicts" +"@cspell/dict-swift@2.0.0","MIT","Copyright (c) 2017-2020 Jason Dent ","https://github.com/streetsidesoftware/cspell-dicts" +"@cspell/dict-typescript@3.0.1","MIT","Copyright (c) 2017-2020 Jason Dent ","https://github.com/streetsidesoftware/cspell-dicts" +"@cspell/dict-vue@3.0.0","MIT","Copyright (c) 2017-2020 Jason Dent ","https://github.com/streetsidesoftware/cspell-dicts" +"@cspell/strong-weak-map@6.17.0","MIT","Copyright (c) 2017 Jason Dent","https://github.com/streetsidesoftware/cspell" "@cspotcode/source-map-support@0.8.1","MIT","Copyright (c) 2014 Evan Wallace","https://github.com/cspotcode/node-source-map-support" "@csstools/postcss-cascade-layers@1.1.1","CC0-1.0","","https://github.com/csstools/postcss-plugins" "@csstools/postcss-color-function@1.1.1","CC0-1.0","","https://github.com/csstools/postcss-plugins" @@ -228,20 +231,20 @@ "@csstools/postcss-unset-value@1.0.2","CC0-1.0","","https://github.com/csstools/postcss-plugins" "@csstools/selector-specificity@2.0.2","CC0-1.0","","https://github.com/csstools/postcss-plugins" "@discoveryjs/json-ext@0.5.7","MIT","Copyright (c) 2020 Roman Dvornov ","https://github.com/discoveryjs/json-ext" -"@es-joy/jsdoccomment@0.31.0","MIT","Copyright JS Foundation and other contributors, https://js.foundation. Copyright (c) 2021 Brett Zamir","https://github.com/es-joy/jsdoccomment" -"@eslint/eslintrc@1.3.2","MIT","Copyright OpenJS Foundation and other contributors, ","https://github.com/eslint/eslintrc" +"@es-joy/jsdoccomment@0.36.1","MIT","Copyright JS Foundation and other contributors, https://js.foundation. Copyright (c) 2021 Brett Zamir","https://github.com/es-joy/jsdoccomment" +"@eslint-community/eslint-utils@4.1.2","MIT","Copyright (c) 2018 Toru Nagashima","https://github.com/eslint-community/eslint-utils" +"@eslint/eslintrc@1.3.3","MIT","Copyright OpenJS Foundation and other contributors, ","https://github.com/eslint/eslintrc" "@foliojs-fork/fontkit@1.9.1","MIT","","https://github.com/foliojs-fork/fontkit" "@foliojs-fork/linebreak@1.1.1","MIT","Copyright (c) 2014-present Devon Govett","https://github.com/foliojs-fork/linebreaker" "@foliojs-fork/pdfkit@0.13.0","MIT","","https://github.com/foliojs-fork/pdfkit" "@foliojs-fork/restructure@2.0.2","MIT","Copyright (c) 2015-present Devon Govett","https://github.com/foliojs-fork/restructure" "@fortawesome/angular-fontawesome@0.11.1","MIT","Copyright (c) 2018 Fonticons, Inc. and contributors","https://github.com/FortAwesome/angular-fontawesome" -"@fortawesome/fontawesome-common-types@6.2.0","MIT","Copyright (c) 2022 Fonticons, Inc. (https://fontawesome.com). with Reserved Font Name: "Font Awesome".*","https://github.com/FortAwesome/Font-Awesome" -"@fortawesome/fontawesome-svg-core@6.2.0","MIT","Copyright (c) 2022 Fonticons, Inc. (https://fontawesome.com). with Reserved Font Name: "Font Awesome".*","https://github.com/FortAwesome/Font-Awesome" -"@fortawesome/free-solid-svg-icons@6.2.0","(CC-BY-4.0 AND MIT)","Copyright (c) 2022 Fonticons, Inc. (https://fontawesome.com). with Reserved Font Name: "Font Awesome".*","https://github.com/FortAwesome/Font-Awesome" +"@fortawesome/fontawesome-common-types@6.2.1","MIT","Copyright (c) 2022 Fonticons, Inc. (https://fontawesome.com). with Reserved Font Name: "Font Awesome".*","https://github.com/FortAwesome/Font-Awesome" +"@fortawesome/fontawesome-svg-core@6.2.1","MIT","Copyright (c) 2022 Fonticons, Inc. (https://fontawesome.com). with Reserved Font Name: "Font Awesome".*","https://github.com/FortAwesome/Font-Awesome" +"@fortawesome/free-solid-svg-icons@6.2.1","(CC-BY-4.0 AND MIT)","Copyright (c) 2022 Fonticons, Inc. (https://fontawesome.com). with Reserved Font Name: "Font Awesome".*","https://github.com/FortAwesome/Font-Awesome" "@gar/promisify@1.1.3","MIT","Copyright © 2020-2022 Michael Garvin","https://github.com/wraithgar/gar-promisify" -"@googlemaps/js-api-loader@1.14.3","Apache-2.0","","https://github.com/googlemaps/js-api-loader" -"@humanwhocodes/config-array@0.10.7","Apache-2.0","","https://github.com/humanwhocodes/config-array" -"@humanwhocodes/gitignore-to-minimatch@1.0.2","Apache-2.0","","https://github.com/humanwhocodes/gitignore-to-minimatch" +"@googlemaps/js-api-loader@1.15.1","Apache-2.0","","https://github.com/googlemaps/js-api-loader" +"@humanwhocodes/config-array@0.11.7","Apache-2.0","","https://github.com/humanwhocodes/config-array" "@humanwhocodes/module-importer@1.0.1","Apache-2.0","","https://github.com/humanwhocodes/module-importer" "@humanwhocodes/object-schema@1.2.1","BSD-3-Clause","Copyright (c) 2019, Human Who Codes. All rights reserved.","https://github.com/humanwhocodes/object-schema" "@hutson/parse-repository-url@3.0.2","Apache-2.0","","git+https://gitlab.com/hyper-expanse/open-source/parse-repository-url" @@ -289,11 +292,11 @@ "@ngrx/router-store@14.3.2","MIT","","https://github.com/ngrx/platform" "@ngrx/store-devtools@14.3.2","MIT","","https://github.com/ngrx/platform" "@ngrx/store@14.3.2","MIT","","https://github.com/ngrx/platform" -"@ngtools/webpack@14.2.5","MIT","Copyright (c) 2017 Google, Inc.","https://github.com/angular/angular-cli" -"@nguniversal/builders@14.2.0","MIT","","https://github.com/angular/universal" -"@nguniversal/common@14.2.0","MIT","","https://github.com/angular/universal" -"@nguniversal/express-engine@14.2.0","MIT","","https://github.com/angular/universal" -"@ngx-formly/core@5.12.7","MIT","","https://github.com/ngx-formly/ngx-formly" +"@ngtools/webpack@14.2.10","MIT","Copyright (c) 2017 Google, Inc.","https://github.com/angular/angular-cli" +"@nguniversal/builders@14.2.3","MIT","","https://github.com/angular/universal" +"@nguniversal/common@14.2.3","MIT","","https://github.com/angular/universal" +"@nguniversal/express-engine@14.2.3","MIT","","https://github.com/angular/universal" +"@ngx-formly/core@6.0.4","MIT","","https://github.com/ngx-formly/ngx-formly" "@ngx-translate/core@14.0.0","MIT","","https://github.com/ngx-translate/core" "@nodelib/fs.scandir@2.1.5","MIT","Copyright (c) Denis Malinochkin","https://github.com/nodelib/nodelib/tree/master/packages/fs/fs.scandir" "@nodelib/fs.stat@2.0.5","MIT","Copyright (c) Denis Malinochkin","https://github.com/nodelib/nodelib/tree/master/packages/fs/fs.stat" @@ -315,12 +318,11 @@ "@pm2/js-api@0.6.7","Apache*","","https://github.com/keymetrics/km.js" "@pm2/pm2-version-check@1.0.4","MIT","","" "@rx-angular/state@1.7.0","MIT","","https://github.com/rx-angular/rx-angular" -"@schematics/angular@14.2.5","MIT","Copyright (c) 2017 Google, Inc.","https://github.com/angular/angular-cli" -"@sentry/browser@7.14.1","BSD-3-Clause","Copyright (c) 2019, Sentry. All rights reserved.","https://github.com/getsentry/sentry-javascript" -"@sentry/core@7.14.1","BSD-3-Clause","Copyright (c) 2019, Sentry. All rights reserved.","https://github.com/getsentry/sentry-javascript" -"@sentry/hub@7.14.1","BSD-3-Clause","Copyright (c) 2019, Sentry. All rights reserved.","https://github.com/getsentry/sentry-javascript" -"@sentry/types@7.14.1","BSD-3-Clause","Copyright (c) 2019, Sentry. All rights reserved.","https://github.com/getsentry/sentry-javascript" -"@sentry/utils@7.14.1","BSD-3-Clause","Copyright (c) 2019, Sentry. All rights reserved.","https://github.com/getsentry/sentry-javascript" +"@schematics/angular@14.2.10","MIT","Copyright (c) 2017 Google, Inc.","https://github.com/angular/angular-cli" +"@sentry/browser@7.25.0","MIT","Copyright (c) 2019 Sentry (https://sentry.io) and individual contributors. All rights reserved.","https://github.com/getsentry/sentry-javascript" +"@sentry/core@7.25.0","MIT","Copyright (c) 2019 Sentry (https://sentry.io) and individual contributors. All rights reserved.","https://github.com/getsentry/sentry-javascript" +"@sentry/types@7.25.0","MIT","Copyright (c) 2019 Sentry (https://sentry.io) and individual contributors. All rights reserved.","https://github.com/getsentry/sentry-javascript" +"@sentry/utils@7.25.0","MIT","Copyright (c) 2019 Sentry (https://sentry.io) and individual contributors. All rights reserved.","https://github.com/getsentry/sentry-javascript" "@sinclair/typebox@0.24.44","MIT","Copyright (c) 2022 Haydn Paterson (sinclair) ","https://github.com/sinclairzx81/typebox" "@sinonjs/commons@1.8.3","BSD-3-Clause","Copyright (c) 2018, Sinon.JS. All rights reserved.","https://github.com/sinonjs/commons" "@sinonjs/fake-timers@9.1.2","BSD-3-Clause","Copyright (c) 2010-2014, Christian Johansen, christian@cjohansen.no. All rights reserved.","https://github.com/sinonjs/fake-timers" @@ -345,13 +347,13 @@ "@types/cookie@0.4.1","MIT","","https://github.com/DefinitelyTyped/DefinitelyTyped" "@types/cors@2.8.12","MIT","","https://github.com/DefinitelyTyped/DefinitelyTyped" "@types/eslint-scope@3.7.4","MIT","","https://github.com/DefinitelyTyped/DefinitelyTyped" -"@types/eslint@8.4.6","MIT","","https://github.com/DefinitelyTyped/DefinitelyTyped" +"@types/eslint@8.4.10","MIT","","https://github.com/DefinitelyTyped/DefinitelyTyped" "@types/estree@0.0.51","MIT","","https://github.com/DefinitelyTyped/DefinitelyTyped" "@types/estree@1.0.0","MIT","","https://github.com/DefinitelyTyped/DefinitelyTyped" "@types/express-serve-static-core@4.17.31","MIT","","https://github.com/DefinitelyTyped/DefinitelyTyped" "@types/express@4.17.14","MIT","","https://github.com/DefinitelyTyped/DefinitelyTyped" "@types/glob@7.2.0","MIT","","https://github.com/DefinitelyTyped/DefinitelyTyped" -"@types/google.maps@3.50.2","MIT","","https://github.com/DefinitelyTyped/DefinitelyTyped" +"@types/google.maps@3.51.0","MIT","","https://github.com/DefinitelyTyped/DefinitelyTyped" "@types/graceful-fs@4.1.5","MIT","","https://github.com/DefinitelyTyped/DefinitelyTyped" "@types/grecaptcha@3.0.4","MIT","","https://github.com/DefinitelyTyped/DefinitelyTyped" "@types/http-proxy@1.17.9","MIT","","https://github.com/DefinitelyTyped/DefinitelyTyped" @@ -367,8 +369,8 @@ "@types/mime@3.0.1","MIT","","https://github.com/DefinitelyTyped/DefinitelyTyped" "@types/minimatch@3.0.5","MIT","","https://github.com/DefinitelyTyped/DefinitelyTyped" "@types/minimist@1.2.2","MIT","","https://github.com/DefinitelyTyped/DefinitelyTyped" -"@types/node@14.18.31","MIT","","https://github.com/DefinitelyTyped/DefinitelyTyped" -"@types/node@16.11.64","MIT","","https://github.com/DefinitelyTyped/DefinitelyTyped" +"@types/node@14.18.34","MIT","","https://github.com/DefinitelyTyped/DefinitelyTyped" +"@types/node@16.18.8","MIT","","https://github.com/DefinitelyTyped/DefinitelyTyped" "@types/normalize-package-data@2.4.1","MIT","","https://github.com/DefinitelyTyped/DefinitelyTyped" "@types/parse-json@4.0.0","MIT","","https://github.com/DefinitelyTyped/DefinitelyTyped" "@types/parse5@6.0.3","MIT","","https://github.com/DefinitelyTyped/DefinitelyTyped" @@ -376,31 +378,37 @@ "@types/qs@6.9.7","MIT","","https://github.com/DefinitelyTyped/DefinitelyTyped" "@types/range-parser@1.2.4","MIT","","https://github.com/DefinitelyTyped/DefinitelyTyped" "@types/retry@0.12.0","MIT","","https://github.com/DefinitelyTyped/DefinitelyTyped" +"@types/semver@7.3.13","MIT","","https://github.com/DefinitelyTyped/DefinitelyTyped" "@types/serve-index@1.9.1","MIT","","https://github.com/DefinitelyTyped/DefinitelyTyped" "@types/serve-static@1.15.0","MIT","","https://github.com/DefinitelyTyped/DefinitelyTyped" "@types/sockjs@0.3.33","MIT","","https://github.com/DefinitelyTyped/DefinitelyTyped" "@types/stack-utils@2.0.1","MIT","","https://github.com/DefinitelyTyped/DefinitelyTyped" "@types/tough-cookie@4.0.2","MIT","","https://github.com/DefinitelyTyped/DefinitelyTyped" -"@types/uuid@8.3.4","MIT","","https://github.com/DefinitelyTyped/DefinitelyTyped" +"@types/uuid@9.0.0","MIT","","https://github.com/DefinitelyTyped/DefinitelyTyped" "@types/webpack@5.28.0","MIT","","https://github.com/DefinitelyTyped/DefinitelyTyped" "@types/ws@8.5.3","MIT","","https://github.com/DefinitelyTyped/DefinitelyTyped" "@types/yargs-parser@21.0.0","MIT","","https://github.com/DefinitelyTyped/DefinitelyTyped" "@types/yargs@17.0.13","MIT","","https://github.com/DefinitelyTyped/DefinitelyTyped" -"@typescript-eslint/eslint-plugin@5.39.0","MIT","Copyright (c) 2019 TypeScript ESLint and other contributors","https://github.com/typescript-eslint/typescript-eslint" +"@typescript-eslint/eslint-plugin@5.46.1","MIT","Copyright (c) 2019 typescript-eslint and other contributors","https://github.com/typescript-eslint/typescript-eslint" "@typescript-eslint/experimental-utils@5.39.0","MIT","Copyright (c) 2019 TypeScript ESLint and other contributors","https://github.com/typescript-eslint/typescript-eslint" -"@typescript-eslint/parser@5.39.0","BSD-2-Clause","","https://github.com/typescript-eslint/typescript-eslint" -"@typescript-eslint/scope-manager@5.37.0","MIT","Copyright (c) 2019 TypeScript ESLint and other contributors","https://github.com/typescript-eslint/typescript-eslint" +"@typescript-eslint/parser@5.46.1","BSD-2-Clause","","https://github.com/typescript-eslint/typescript-eslint" "@typescript-eslint/scope-manager@5.39.0","MIT","Copyright (c) 2019 TypeScript ESLint and other contributors","https://github.com/typescript-eslint/typescript-eslint" -"@typescript-eslint/type-utils@5.37.0","MIT","Copyright (c) 2021 TypeScript ESLint and other contributors","https://github.com/typescript-eslint/typescript-eslint" -"@typescript-eslint/type-utils@5.39.0","MIT","Copyright (c) 2021 TypeScript ESLint and other contributors","https://github.com/typescript-eslint/typescript-eslint" -"@typescript-eslint/types@5.37.0","MIT","Copyright (c) 2019 TypeScript ESLint and other contributors","https://github.com/typescript-eslint/typescript-eslint" +"@typescript-eslint/scope-manager@5.43.0","MIT","Copyright (c) 2019 TypeScript ESLint and other contributors","https://github.com/typescript-eslint/typescript-eslint" +"@typescript-eslint/scope-manager@5.46.1","MIT","Copyright (c) 2019 typescript-eslint and other contributors","https://github.com/typescript-eslint/typescript-eslint" +"@typescript-eslint/type-utils@5.43.0","MIT","Copyright (c) 2021 TypeScript ESLint and other contributors","https://github.com/typescript-eslint/typescript-eslint" +"@typescript-eslint/type-utils@5.46.1","MIT","Copyright (c) 2021 typescript-eslint and other contributors","https://github.com/typescript-eslint/typescript-eslint" "@typescript-eslint/types@5.39.0","MIT","Copyright (c) 2019 TypeScript ESLint and other contributors","https://github.com/typescript-eslint/typescript-eslint" -"@typescript-eslint/typescript-estree@5.37.0","BSD-2-Clause","","https://github.com/typescript-eslint/typescript-eslint" +"@typescript-eslint/types@5.43.0","MIT","Copyright (c) 2019 TypeScript ESLint and other contributors","https://github.com/typescript-eslint/typescript-eslint" +"@typescript-eslint/types@5.46.1","MIT","Copyright (c) 2019 typescript-eslint and other contributors","https://github.com/typescript-eslint/typescript-eslint" "@typescript-eslint/typescript-estree@5.39.0","BSD-2-Clause","","https://github.com/typescript-eslint/typescript-eslint" -"@typescript-eslint/utils@5.37.0","MIT","Copyright (c) 2019 TypeScript ESLint and other contributors","https://github.com/typescript-eslint/typescript-eslint" +"@typescript-eslint/typescript-estree@5.43.0","BSD-2-Clause","","https://github.com/typescript-eslint/typescript-eslint" +"@typescript-eslint/typescript-estree@5.46.1","BSD-2-Clause","","https://github.com/typescript-eslint/typescript-eslint" "@typescript-eslint/utils@5.39.0","MIT","Copyright (c) 2019 TypeScript ESLint and other contributors","https://github.com/typescript-eslint/typescript-eslint" -"@typescript-eslint/visitor-keys@5.37.0","MIT","Copyright (c) 2019 TypeScript ESLint and other contributors","https://github.com/typescript-eslint/typescript-eslint" +"@typescript-eslint/utils@5.43.0","MIT","Copyright (c) 2019 TypeScript ESLint and other contributors","https://github.com/typescript-eslint/typescript-eslint" +"@typescript-eslint/utils@5.46.1","MIT","Copyright (c) 2019 typescript-eslint and other contributors","https://github.com/typescript-eslint/typescript-eslint" "@typescript-eslint/visitor-keys@5.39.0","MIT","Copyright (c) 2019 TypeScript ESLint and other contributors","https://github.com/typescript-eslint/typescript-eslint" +"@typescript-eslint/visitor-keys@5.43.0","MIT","Copyright (c) 2019 TypeScript ESLint and other contributors","https://github.com/typescript-eslint/typescript-eslint" +"@typescript-eslint/visitor-keys@5.46.1","MIT","Copyright (c) 2019 typescript-eslint and other contributors","https://github.com/typescript-eslint/typescript-eslint" "@webassemblyjs/ast@1.11.1","MIT","Copyright (c) 2018 Sven Sauleau ","https://github.com/xtuc/webassemblyjs" "@webassemblyjs/floating-point-hex-parser@1.11.1","MIT","Copyright (c) 2017 Mauro Bringolf","https://github.com/xtuc/webassemblyjs" "@webassemblyjs/helper-api-error@1.11.1","MIT","Copyright (c) 2018 Sven Sauleau ","https://github.com/xtuc/webassemblyjs" @@ -447,7 +455,7 @@ "amp-message@0.1.2","MIT","","https://github.com/visionmedia/node-amp-message" "amp@0.3.1","MIT","","https://github.com/visionmedia/node-amp" "angular-oauth2-oidc@13.0.1","MIT","Copyright (c) 2017 Manfred Steyer","https://github.com/manfredsteyer/angular-oauth2-oidc" -"angulartics2@12.1.0","MIT","Copyright (c) 2021 angulartics","https://github.com/angulartics/angulartics2" +"angulartics2@12.2.0","MIT","Copyright (c) 2021 angulartics","https://github.com/angulartics/angulartics2" "ansi-colors@4.1.3","MIT","Copyright (c) 2015-present, Brian Woodward.","https://github.com/doowb/ansi-colors" "ansi-escapes@3.2.0","MIT","Copyright (c) Sindre Sorhus (sindresorhus.com)","https://github.com/sindresorhus/ansi-escapes" "ansi-escapes@4.3.2","MIT","Copyright (c) Sindre Sorhus (https://sindresorhus.com)","https://github.com/sindresorhus/ansi-escapes" @@ -468,13 +476,12 @@ "anymatch@3.1.2","ISC","Copyright (c) 2019 Elan Shanker, Paul Miller (https://paulmillr.com)","https://github.com/micromatch/anymatch" "apache-crypt@1.2.6","MIT","Copyright (c) Gevorg Harutyunyan","https://github.com/gevorg/apache-crypt" "apache-md5@1.1.8","MIT","Copyright (c) Gevorg Harutyunyan","https://github.com/gevorg/apache-md5" -"app-root-path@3.0.0","MIT","Copyright (c) 2014 Chris Morrell","https://github.com/inxilpro/node-app-root-path" "aproba@2.0.0","ISC","Copyright (c) 2015, Rebecca Turner ","https://github.com/iarna/aproba" "are-we-there-yet@3.0.1","ISC","Copyright npm, Inc.","https://github.com/npm/are-we-there-yet" "arg@4.1.3","MIT","Copyright (c) 2017-2019 Zeit, Inc.","https://github.com/zeit/arg" "argparse@1.0.10","MIT","Copyright (C) 2012 by Vitaly Puzrin","https://github.com/nodeca/argparse" "argparse@2.0.1","Python-2.0","Copyright (c) 1991 - 1995, Stichting Mathematisch Centrum Amsterdam,. The Netherlands. All rights reserved.","https://github.com/nodeca/argparse" -"aria-query@5.0.2","Apache-2.0","Copyright 2020 A11yance","https://github.com/A11yance/aria-query" +"aria-query@5.1.3","Apache-2.0","Copyright 2020 A11yance","https://github.com/A11yance/aria-query" "array-differ@3.0.0","MIT","Copyright (c) Sindre Sorhus (sindresorhus.com)","https://github.com/sindresorhus/array-differ" "array-flatten@1.1.1","MIT","Copyright (c) 2014 Blake Embrey (hello@blakeembrey.com)","https://github.com/blakeembrey/array-flatten" "array-flatten@2.1.2","MIT","Copyright (c) 2014 Blake Embrey (hello@blakeembrey.com)","https://github.com/blakeembrey/array-flatten" @@ -494,9 +501,10 @@ "async@3.2.4","MIT","Copyright (c) 2010-2018 Caolan McMahon","https://github.com/caolan/async" "asynckit@0.4.0","MIT","Copyright (c) 2016 Alex Indigo","https://github.com/alexindigo/asynckit" "at-least-node@1.0.0","ISC","","https://github.com/RyanZim/at-least-node" -"autoprefixer@10.4.12","MIT","Copyright 2013 Andrey Sitnik ","https://github.com/postcss/autoprefixer" +"autoprefixer@10.4.13","MIT","Copyright 2013 Andrey Sitnik ","https://github.com/postcss/autoprefixer" +"available-typed-arrays@1.0.5","MIT","Copyright (c) 2020 Inspect JS","https://github.com/inspect-js/available-typed-arrays" "axios@0.21.4","MIT","Copyright (c) 2014-present Matt Zabriskie","https://github.com/axios/axios" -"axobject-query@3.0.1","Apache-2.0","Copyright 2020 A11yance","https://github.com/A11yance/axobject-query" +"axobject-query@3.1.1","Apache-2.0","Copyright 2020 A11yance","https://github.com/A11yance/axobject-query" "babel-jest@28.1.3","MIT","Copyright (c) Facebook, Inc. and its affiliates.","https://github.com/facebook/jest" "babel-loader@8.2.5","MIT","Copyright (c) 2014-2019 Luís Couto ","https://github.com/babel/babel-loader" "babel-plugin-dynamic-import-node@2.3.3","MIT","Copyright (c) 2016 Airbnb","https://github.com/airbnb/babel-plugin-dynamic-import-node" @@ -522,7 +530,7 @@ "bl@4.1.0","MIT","Copyright (c) 2013-2019 bl contributors. ----------------------------------","https://github.com/rvagg/bl" "blessed@0.1.81","MIT","Copyright (c) 2013-2015, Christopher Jeffrey and contributors. https://github.com/chjj/","https://github.com/chjj/blessed" "bodec@0.1.0","MIT","Copyright (c) 2014 Tim Caswell","https://github.com/creationix/bodec" -"body-parser@1.20.0","MIT","Copyright (c) 2014 Jonathan Ong . Copyright (c) 2014-2015 Douglas Christopher Wilson ","https://github.com/expressjs/body-parser" +"body-parser@1.20.1","MIT","Copyright (c) 2014 Jonathan Ong . Copyright (c) 2014-2015 Douglas Christopher Wilson ","https://github.com/expressjs/body-parser" "bonjour-service@1.0.14","MIT","Copyright (https://github.com/onlxltd/bonjour-service) (c) 2021 ON LX Limited","https://github.com/onlxltd/bonjour-service" "boolbase@1.0.0","ISC","","https://github.com/fb55/boolbase" "bootstrap@4.6.2","MIT","Copyright (c) 2011-2022 Twitter, Inc.. Copyright (c) 2011-2022 The Bootstrap Authors","https://github.com/twbs/bootstrap" @@ -551,7 +559,6 @@ "bytes@3.1.2","MIT","Copyright (c) 2012-2014 TJ Holowaychuk . Copyright (c) 2015 Jed Watson ","https://github.com/visionmedia/bytes.js" "bytesish@0.4.4","(Apache-2.0 AND MIT)","","https://github.com/mikeal/bytesish" "cacache@16.1.2","ISC","Copyright (c) npm, Inc.","https://github.com/npm/cacache" -"cachedir@2.3.0","MIT","Copyright (c) 2013-2014, 2016, 2018 Linus Unnebäck","https://github.com/LinusU/node-cachedir" "call-bind@1.0.2","MIT","Copyright (c) 2020 Jordan Harband","https://github.com/ljharb/call-bind" "call-me-maybe@1.0.1","MIT","Copyright (c) 2015 Eric McCarthy","https://github.com/limulus/call-me-maybe" "callsite@1.0.0","MIT*","","" @@ -559,7 +566,7 @@ "camelcase-keys@6.2.2","MIT","Copyright (c) Sindre Sorhus (sindresorhus.com)","https://github.com/sindresorhus/camelcase-keys" "camelcase@5.3.1","MIT","Copyright (c) Sindre Sorhus (sindresorhus.com)","https://github.com/sindresorhus/camelcase" "camelcase@6.3.0","MIT","Copyright (c) Sindre Sorhus (https://sindresorhus.com)","https://github.com/sindresorhus/camelcase" -"caniuse-lite@1.0.30001416","CC-BY-4.0","","https://github.com/browserslist/caniuse-lite" +"caniuse-lite@1.0.30001439","CC-BY-4.0","","https://github.com/browserslist/caniuse-lite" "caseless@0.12.0","Apache-2.0","","https://github.com/mikeal/caseless" "chalk@1.1.3","MIT","Copyright (c) Sindre Sorhus (sindresorhus.com)","https://github.com/chalk/chalk" "chalk@2.4.2","MIT","Copyright (c) Sindre Sorhus (sindresorhus.com)","https://github.com/chalk/chalk" @@ -573,7 +580,7 @@ "chokidar@3.5.3","MIT","Copyright (c) 2012-2019 Paul Miller (https://paulmillr.com), Elan Shanker","https://github.com/paulmillr/chokidar" "chownr@2.0.0","ISC","Copyright (c) Isaac Z. Schlueter and Contributors","https://github.com/isaacs/chownr" "chrome-trace-event@1.0.3","MIT","Copyright (c) 2015 Joyent Inc. All rights reserved.","https://github.com/samccone/chrome-trace-event" -"ci-info@3.4.0","MIT","Copyright (c) 2016-2021 Thomas Watson Steen","https://github.com/watson/ci-info" +"ci-info@3.7.0","MIT","Copyright (c) 2016-2022 Thomas Watson Steen","https://github.com/watson/ci-info" "cjs-module-lexer@1.2.2","MIT","Copyright (C) 2018-2020 Guy Bedford","https://github.com/guybedford/cjs-module-lexer" "clean-regexp@1.0.0","MIT","Copyright (c) Sam Verschueren (github.com/SamVerschueren)","https://github.com/SamVerschueren/clean-regexp" "clean-stack@2.2.0","MIT","Copyright (c) Sindre Sorhus (sindresorhus.com)","https://github.com/sindresorhus/clean-stack" @@ -608,8 +615,6 @@ "commander@9.4.1","MIT","Copyright (c) 2011 TJ Holowaychuk ","https://github.com/tj/commander.js" "comment-json@4.2.3","MIT","Copyright (c) 2013 kaelzhang <>, contributors. http://kael.me/","https://github.com/kaelzhang/node-comment-json" "comment-parser@1.3.1","MIT","Copyright (c) 2014 Sergii Iavorskyi","https://github.com/yavorskiy/comment-parser" -"commitizen@4.2.5","MIT","","https://github.com/commitizen/cz-cli" -"commitlint-config-cz@0.13.3","MIT","Copyright (c) 2016 Whizark","https://github.com/whizark/commitlint-config-cz" "common-tags@1.8.2","MIT","Copyright © Declan de Wet","https://github.com/zspecza/common-tags" "commondir@1.0.1","MIT","Copyright (c) 2013 James Halliday (mail@substack.net)","https://github.com/substack/node-commondir" "compare-func@2.0.0","MIT","Copyright (c) 2015 Steve Mao","https://github.com/stevemao/compare-func" @@ -631,6 +636,7 @@ "conventional-changelog-cli@2.2.2","MIT","Copyright (c) 2015 Steve Mao (https://github.com/stevemao)","https://github.com/conventional-changelog/conventional-changelog" "conventional-changelog-codemirror@2.0.8","ISC","Copyright © [conventional-changelog team](https://github.com/conventional-changelog)","https://github.com/conventional-changelog/conventional-changelog" "conventional-changelog-conventionalcommits@4.6.3","ISC","Copyright © [conventional-changelog team](https://github.com/conventional-changelog)","https://github.com/conventional-changelog/conventional-changelog" +"conventional-changelog-conventionalcommits@5.0.0","ISC","Copyright © [conventional-changelog team](https://github.com/conventional-changelog)","https://github.com/conventional-changelog/conventional-changelog" "conventional-changelog-core@4.2.4","MIT","Copyright © [conventional-changelog team](https://github.com/conventional-changelog)","https://github.com/conventional-changelog/conventional-changelog" "conventional-changelog-ember@2.0.9","ISC","Copyright © [conventional-changelog team](https://github.com/conventional-changelog)","https://github.com/conventional-changelog/conventional-changelog" "conventional-changelog-eslint@3.0.9","ISC","Copyright © [conventional-changelog team](https://github.com/conventional-changelog)","https://github.com/conventional-changelog/conventional-changelog" @@ -640,7 +646,6 @@ "conventional-changelog-preset-loader@2.3.4","MIT","Copyright © [conventional-changelog team](https://github.com/conventional-changelog)","https://github.com/conventional-changelog/conventional-changelog" "conventional-changelog-writer@5.0.1","MIT","Copyright © [conventional-changelog team](https://github.com/conventional-changelog)","https://github.com/conventional-changelog/conventional-changelog" "conventional-changelog@3.1.25","MIT","Copyright © [conventional-changelog team](https://github.com/conventional-changelog)","https://github.com/conventional-changelog/conventional-changelog" -"conventional-commit-types@3.0.0","ISC","","https://github.com/commitizen/conventional-commit-types" "conventional-commits-filter@2.0.7","MIT","Copyright (c) 2015 Steve Mao (https://github.com/stevemao)","https://github.com/conventional-changelog/conventional-changelog" "conventional-commits-parser@3.2.4","MIT","Copyright © [conventional-changelog team](https://github.com/conventional-changelog)","https://github.com/conventional-changelog/conventional-changelog" "convert-source-map@1.8.0","MIT","Copyright 2013 Thorsten Lorenz. . All rights reserved.","https://github.com/thlorenz/convert-source-map" @@ -652,8 +657,9 @@ "core-js-compat@3.25.5","MIT","Copyright (c) 2014-2022 Denis Pushkarev","https://github.com/zloirock/core-js" "core-util-is@1.0.3","MIT","Copyright Node.js contributors. All rights reserved.","https://github.com/isaacs/core-util-is" "cors@2.8.5","MIT","Copyright (c) 2013 Troy Goode ","https://github.com/expressjs/cors" -"cosmiconfig-typescript-loader@4.1.1","MIT","Copyright (c) 2021 Alex Miller ","https://github.com/Codex-/cosmiconfig-typescript-loader" -"cosmiconfig@7.0.1","MIT","Copyright (c) 2015 David Clark","https://github.com/davidtheclark/cosmiconfig" +"cosmiconfig-typescript-loader@4.3.0","MIT","Copyright (c) 2021 Alex Miller ","https://github.com/Codex-/cosmiconfig-typescript-loader" +"cosmiconfig@7.1.0","MIT","Copyright (c) 2015 David Clark","https://github.com/davidtheclark/cosmiconfig" +"cosmiconfig@8.0.0","MIT","Copyright (c) 2015 David Clark","https://github.com/davidtheclark/cosmiconfig" "create-require@1.1.1","MIT","Copyright (c) 2020","https://github.com/nuxt-contrib/create-require" "critters@0.0.16","Apache-2.0","","https://github.com/GoogleChromeLabs/critters" "croner@4.1.97","MIT","Copyright (c) 2015-2021 Hexagon ","https://github.com/hexagon/croner" @@ -661,14 +667,14 @@ "cross-spawn@7.0.3","MIT","Copyright (c) 2018 Made With MOXY Lda ","https://github.com/moxystudio/node-cross-spawn" "crypto-js@4.1.1","MIT","Copyright (c) 2009-2013 Jeff Mott . Copyright (c) 2013-2016 Evan Vosberg","https://github.com/brix/crypto-js" "crypto-random-string@2.0.0","MIT","Copyright (c) Sindre Sorhus (sindresorhus.com)","https://github.com/sindresorhus/crypto-random-string" -"cspell-dictionary@6.12.0","MIT","Copyright (c) 2022 Jason Dent","https://github.com/streetsidesoftware/cspell" -"cspell-gitignore@6.12.0","MIT","Copyright (c) 2017 Jason Dent","https://github.com/streetsidesoftware/cspell" -"cspell-glob@6.12.0","MIT","Copyright (c) 2017 Jason Dent","https://github.com/streetsidesoftware/cspell" -"cspell-grammar@6.12.0","MIT","Copyright (c) 2017 Jason Dent","https://github.com/streetsidesoftware/cspell" -"cspell-io@6.12.0","MIT","Copyright (c) 2017 Jason Dent","https://github.com/streetsidesoftware/cspell" -"cspell-lib@6.12.0","MIT","Copyright (c) 2017 Jason Dent","https://github.com/streetsidesoftware/cspell" -"cspell-trie-lib@6.12.0","MIT","Copyright (c) 2017 Jason Dent","https://github.com/streetsidesoftware/cspell" -"cspell@6.12.0","MIT","Copyright (c) 2017 Jason Dent","https://github.com/streetsidesoftware/cspell" +"cspell-dictionary@6.17.0","MIT","Copyright (c) 2022 Jason Dent","https://github.com/streetsidesoftware/cspell" +"cspell-gitignore@6.17.0","MIT","Copyright (c) 2017 Jason Dent","https://github.com/streetsidesoftware/cspell" +"cspell-glob@6.17.0","MIT","Copyright (c) 2017 Jason Dent","https://github.com/streetsidesoftware/cspell" +"cspell-grammar@6.17.0","MIT","Copyright (c) 2017 Jason Dent","https://github.com/streetsidesoftware/cspell" +"cspell-io@6.17.0","MIT","Copyright (c) 2017 Jason Dent","https://github.com/streetsidesoftware/cspell" +"cspell-lib@6.17.0","MIT","Copyright (c) 2017 Jason Dent","https://github.com/streetsidesoftware/cspell" +"cspell-trie-lib@6.17.0","MIT","Copyright (c) 2017 Jason Dent","https://github.com/streetsidesoftware/cspell" +"cspell@6.17.0","MIT","Copyright (c) 2017 Jason Dent","https://github.com/streetsidesoftware/cspell" "css-blank-pseudo@3.0.3","CC0-1.0","","https://github.com/csstools/postcss-plugins" "css-functions-list@3.1.0","MIT","Copyright (c) Ivan Nikolić ","https://github.com/niksy/css-functions-list" "css-has-pseudo@3.0.4","CC0-1.0","","https://github.com/csstools/postcss-plugins" @@ -677,14 +683,13 @@ "css-select@4.3.0","BSD-2-Clause","Copyright (c) Felix Böhm. All rights reserved.","https://github.com/fb55/css-select" "css-select@5.1.0","BSD-2-Clause","Copyright (c) Felix Böhm. All rights reserved.","https://github.com/fb55/css-select" "css-what@6.1.0","BSD-2-Clause","Copyright (c) Felix Böhm. All rights reserved.","https://github.com/fb55/css-what" -"cssdb@7.0.1","CC0-1.0","","https://github.com/csstools/cssdb" +"cssdb@7.2.0","CC0-1.0","","https://github.com/csstools/cssdb" "cssesc@3.0.0","MIT","Copyright Mathias Bynens ","https://github.com/mathiasbynens/cssesc" "cssom@0.3.8","MIT","Copyright (c) Nikita Vasilyev","https://github.com/NV/CSSOM" "cssom@0.4.4","MIT","Copyright (c) Nikita Vasilyev","https://github.com/NV/CSSOM" "cssom@0.5.0","MIT","Copyright (c) Nikita Vasilyev","https://github.com/NV/CSSOM" "cssstyle@2.3.0","MIT","Copyright (c) Chad Walker","https://github.com/jsdom/cssstyle" "culvert@0.1.2","MIT","Copyright (c) 2014 Tim Caswell","https://github.com/creationix/culvert" -"cz-conventional-changelog@3.3.0","MIT","Copyright (c) 2015-2018 Commitizen Contributors","https://github.com/commitizen/cz-conventional-changelog" "cz-customizable@7.0.0","MIT","Copyright (c) 2016 Leonardo Correa","https://github.com/leoforfree/cz-customizable" "d@1.0.1","ISC","Copyright (c) 2013-2019, Mariusz Nowak, @medikoo, medikoo.com","https://github.com/medikoo/d" "dargs@7.0.0","MIT","Copyright (c) Sindre Sorhus (sindresorhus.com)","https://github.com/sindresorhus/dargs" @@ -694,6 +699,7 @@ "data-urls@3.0.2","MIT","Copyright © Domenic Denicola ","https://github.com/jsdom/data-urls" "date-fns@2.29.3","MIT","Copyright (c) 2021 Sasha Koss and Lesha Koss https://kossnocorp.mit-license.org","https://github.com/date-fns/date-fns" "dateformat@3.0.3","MIT","","https://github.com/felixge/node-dateformat" +"dayjs@1.11.6","MIT","Copyright (c) 2018-present, iamkun","https://github.com/iamkun/dayjs" "dayjs@1.8.36","MIT","Copyright (c) 2018-present, iamkun","https://github.com/iamkun/dayjs" "debug@2.6.9","MIT","Copyright (c) 2014 TJ Holowaychuk ","https://github.com/visionmedia/debug" "debug@3.2.7","MIT","Copyright (c) 2014 TJ Holowaychuk ","https://github.com/visionmedia/debug" @@ -706,6 +712,7 @@ "decimal.js@10.4.1","MIT","Copyright (c) 2022 Michael Mclaughlin","https://github.com/MikeMcl/decimal.js" "dedent@0.7.0","MIT","Copyright (c) 2015 Desmond Brand (dmnd@desmondbrand.com)","https://github.com/dmnd/dedent" "deep-equal@1.1.1","MIT","Copyright (c) 2012, 2013, 2014 James Halliday , 2009 Thomas Robinson <280north.com>","https://github.com/substack/node-deep-equal" +"deep-equal@2.1.0","MIT","Copyright (c) 2012, 2013, 2014 James Halliday , 2009 Thomas Robinson <280north.com>","https://github.com/inspect-js/node-deep-equal" "deep-is@0.1.4","MIT","Copyright (c) 2012, 2013 Thorsten Lorenz . Copyright (c) 2012 James Halliday . Copyright (c) 2009 Thomas Robinson <280north.com>","https://github.com/thlorenz/deep-is" "deepmerge@4.2.2","MIT","Copyright (c) 2012 James Halliday, Josh Duff, and other contributors","https://github.com/TehShrike/deepmerge" "default-gateway@6.0.3","BSD-2-Clause","Copyright (c) silverwind. All rights reserved.","https://github.com/silverwind/default-gateway" @@ -720,9 +727,7 @@ "dependency-graph@0.11.0","MIT","Copyright (C) 2013-2020 by Jim Riecken","https://github.com/jriecken/dependency-graph" "destroy@1.0.4","MIT","Copyright (c) 2014 Jonathan Ong me@jongleberry.com","https://github.com/stream-utils/destroy" "destroy@1.2.0","MIT","Copyright (c) 2014 Jonathan Ong me@jongleberry.com. Copyright (c) 2015-2022 Douglas Christopher Wilson doug@somethingdoug.com","https://github.com/stream-utils/destroy" -"detect-file@1.0.0","MIT","Copyright (c) 2016-2017, Brian Woodward.","https://github.com/doowb/detect-file" "detect-indent@5.0.0","MIT","Copyright (c) Sindre Sorhus (sindresorhus.com)","https://github.com/sindresorhus/detect-indent" -"detect-indent@6.1.0","MIT","Copyright (c) Sindre Sorhus (sindresorhus.com)","https://github.com/sindresorhus/detect-indent" "detect-newline@2.1.0","MIT","Copyright (c) Sindre Sorhus (sindresorhus.com)","https://github.com/sindresorhus/detect-newline" "detect-newline@3.1.0","MIT","Copyright (c) Sindre Sorhus (sindresorhus.com)","https://github.com/sindresorhus/detect-newline" "detect-node@2.1.0","MIT","Copyright (c) 2017 Ilya Kantor","https://github.com/iliakan/detect-node" @@ -768,7 +773,7 @@ "end-of-stream@1.4.4","MIT","Copyright (c) 2014 Mathias Buus","https://github.com/mafintosh/end-of-stream" "engine.io-client@6.2.2","MIT","Copyright (c) 2014-2015 Automattic ","https://github.com/socketio/engine.io-client" "engine.io-parser@5.0.4","MIT","Copyright (c) 2016 Guillermo Rauch (@rauchg)","https://github.com/socketio/engine.io-parser" -"engine.io@6.2.0","MIT","Copyright (c) 2014 Guillermo Rauch ","https://github.com/socketio/engine.io" +"engine.io@6.2.1","MIT","Copyright (c) 2014 Guillermo Rauch ","https://github.com/socketio/engine.io" "enhanced-resolve@5.10.0","MIT","Copyright JS Foundation and other contributors","https://github.com/webpack/enhanced-resolve" "enquirer@2.3.6","MIT","Copyright (c) 2016-present, Jon Schlinkert.","https://github.com/enquirer/enquirer" "entities@2.2.0","BSD-2-Clause","Copyright (c) Felix Böhm. All rights reserved.","https://github.com/fb55/entities" @@ -778,6 +783,7 @@ "errno@0.1.8","MIT","","https://github.com/rvagg/node-errno" "error-ex@1.3.2","MIT","Copyright (c) 2015 JD Ballard","https://github.com/qix-/node-error-ex" "es-abstract@1.20.3","MIT","Copyright (C) 2015 Jordan Harband","https://github.com/ljharb/es-abstract" +"es-get-iterator@1.1.2","MIT","Copyright (c) 2019 Jordan Harband","https://github.com/ljharb/es-get-iterator" "es-module-lexer@0.9.3","MIT","Copyright (C) 2018-2021 Guy Bedford","https://github.com/guybedford/es-module-lexer" "es-to-primitive@1.2.1","MIT","Copyright (c) 2015 Jordan Harband","https://github.com/ljharb/es-to-primitive" "es5-ext@0.10.62","ISC","Copyright (c) 2011-2022, Mariusz Nowak, @medikoo, medikoo.com","https://github.com/medikoo/es5-ext" @@ -803,12 +809,12 @@ "eslint-plugin-ban@1.6.0","ISC","Copyright (c) 2021 Rémi Thomas","https://github.com/remithomas/eslint-plugin-ban" "eslint-plugin-etc@2.0.2","MIT","Copyright (c) 2019-2021 Nicholas Jamieson and contributors","https://github.com/cartant/eslint-plugin-etc" "eslint-plugin-ish-custom-rules@0.0.1","UNKNOWN","","" -"eslint-plugin-jest@27.1.1","MIT","Copyright (c) 2018 Jonathan Kim","https://github.com/jest-community/eslint-plugin-jest" -"eslint-plugin-jsdoc@39.3.6","BSD-3-Clause","Copyright (c) 2018, Gajus Kuizinas (http://gajus.com/). All rights reserved.","https://github.com/gajus/eslint-plugin-jsdoc" +"eslint-plugin-jest@27.1.6","MIT","Copyright (c) 2018 Jonathan Kim","https://github.com/jest-community/eslint-plugin-jest" +"eslint-plugin-jsdoc@39.6.4","BSD-3-Clause","Copyright (c) 2018, Gajus Kuizinas (http://gajus.com/). All rights reserved.","https://github.com/gajus/eslint-plugin-jsdoc" "eslint-plugin-prettier@4.2.1","MIT","Copyright © 2017 Andres Suarez and Teddy Katz","https://github.com/prettier/eslint-plugin-prettier" "eslint-plugin-rxjs-angular@2.0.0","MIT","Copyright (c) 2019 Nicholas Jamieson","https://github.com/cartant/eslint-plugin-rxjs-angular" "eslint-plugin-rxjs@5.0.2","MIT","Copyright (c) 2019 Nicholas Jamieson and contributors","https://github.com/cartant/eslint-plugin-rxjs" -"eslint-plugin-unicorn@44.0.1","MIT","Copyright (c) Sindre Sorhus (https://sindresorhus.com)","https://github.com/sindresorhus/eslint-plugin-unicorn" +"eslint-plugin-unicorn@45.0.2","MIT","Copyright (c) Sindre Sorhus (https://sindresorhus.com)","https://github.com/sindresorhus/eslint-plugin-unicorn" "eslint-plugin-unused-imports@2.0.0","MIT","","https://github.com/sweepline/eslint-plugin-unused-imports" "eslint-rule-composer@0.3.0","MIT","Copyright © 2017 Teddy Katz","https://github.com/not-an-aardvark/eslint-rule-composer" "eslint-scope@5.1.1","BSD-2-Clause","Copyright JS Foundation and other contributors, https://js.foundation. Copyright (C) 2012-2013 Yusuke Suzuki (twitter: @Constellation) and other contributors.","https://github.com/eslint/eslint-scope" @@ -816,8 +822,8 @@ "eslint-utils@3.0.0","MIT","Copyright (c) 2018 Toru Nagashima","https://github.com/mysticatea/eslint-utils" "eslint-visitor-keys@2.1.0","Apache-2.0","","https://github.com/eslint/eslint-visitor-keys" "eslint-visitor-keys@3.3.0","Apache-2.0","","https://github.com/eslint/eslint-visitor-keys" -"eslint@8.24.0","MIT","Copyright OpenJS Foundation and other contributors, ","https://github.com/eslint/eslint" -"espree@9.4.0","BSD-2-Clause","Copyright (c) Open JS Foundation. All rights reserved.","https://github.com/eslint/espree" +"eslint@8.29.0","MIT","Copyright OpenJS Foundation and other contributors, ","https://github.com/eslint/eslint" +"espree@9.4.1","BSD-2-Clause","Copyright (c) Open JS Foundation. All rights reserved.","https://github.com/eslint/espree" "esprima@1.0.4","BSD","Copyright (C) 2012, 2011 [Ariya Hidayat](http://ariya.ofilabs.com/about). and other contributors.","https://github.com/ariya/esprima" "esprima@4.0.1","BSD-2-Clause","Copyright JS Foundation and other contributors, https://js.foundation/","https://github.com/jquery/esprima" "esquery@1.4.0","BSD-3-Clause","Copyright (c) 2013, Joel Feenstra. All rights reserved.","https://github.com/estools/esquery" @@ -841,12 +847,11 @@ "execa@5.1.1","MIT","Copyright (c) Sindre Sorhus (https://sindresorhus.com)","https://github.com/sindresorhus/execa" "execa@6.1.0","MIT","Copyright (c) Sindre Sorhus (https://sindresorhus.com)","https://github.com/sindresorhus/execa" "exit@0.1.2","MIT","Copyright (c) 2013 "Cowboy" Ben Alman","https://github.com/cowboy/node-exit" -"expand-tilde@2.0.2","MIT","Copyright (c) 2015-2016, Jon Schlinkert.","https://github.com/jonschlinkert/expand-tilde" "expect@28.1.3","MIT","Copyright (c) Facebook, Inc. and its affiliates.","https://github.com/facebook/jest" "expect@29.1.2","MIT","Copyright (c) Facebook, Inc. and its affiliates.","https://github.com/facebook/jest" "express-http-proxy@1.6.3","MIT","Copyright (c) 2013 villadora , contributors. http://kael.me/","https://github.com/villadora/express-http-proxy" "express-robots-txt@1.0.0","MIT","","https://github.com/modosc/express-robots-txt" -"express@4.18.1","MIT","Copyright (c) 2009-2014 TJ Holowaychuk . Copyright (c) 2013-2014 Roman Shtylman . Copyright (c) 2014-2015 Douglas Christopher Wilson ","https://github.com/expressjs/express" +"express@4.18.2","MIT","Copyright (c) 2009-2014 TJ Holowaychuk . Copyright (c) 2013-2014 Roman Shtylman . Copyright (c) 2014-2015 Douglas Christopher Wilson ","https://github.com/expressjs/express" "ext@1.7.0","ISC","Copyright (c) 2011-2022, Mariusz Nowak, @medikoo, medikoo.com","https://github.com/medikoo/es5-ext.git#ext" "external-editor@3.1.0","MIT","Copyright (c) 2016 Kevin Gravier","https://github.com/mrkmg/node-external-editor" "fancy-log@1.3.3","MIT","Copyright (c) 2014, 2015, 2018 Blaine Bublitz and Eric Schoffstall ","https://github.com/gulpjs/fancy-log" @@ -875,16 +880,14 @@ "finalhandler@1.2.0","MIT","Copyright (c) 2014-2022 Douglas Christopher Wilson ","https://github.com/pillarjs/finalhandler" "find-cache-dir@3.3.2","MIT","Copyright (c) Sindre Sorhus (https://sindresorhus.com)","https://github.com/avajs/find-cache-dir" "find-config@1.0.0","MIT","","https://github.com/shannonmoeller/find-config" -"find-node-modules@2.1.3","MIT","","https://github.com/callumacrae/find-node-modules" -"find-root@1.1.0","MIT","Copyright © 2017 jsdnxx","https://github.com/js-n/find-root" "find-up@2.1.0","MIT","Copyright (c) Sindre Sorhus (sindresorhus.com)","https://github.com/sindresorhus/find-up" "find-up@4.1.0","MIT","Copyright (c) Sindre Sorhus (sindresorhus.com)","https://github.com/sindresorhus/find-up" "find-up@5.0.0","MIT","Copyright (c) Sindre Sorhus (https://sindresorhus.com)","https://github.com/sindresorhus/find-up" "findit2@2.2.3","MIT","Copyright (c) 2014 Andrew Kelley. Copyright (c) 2014 James Halliday","https://github.com/andrewrk/node-findit" -"findup-sync@4.0.0","MIT","Copyright (c) 2013-2019 Ben Alman , Blaine Bublitz , and Eric Schoffstall ","https://github.com/gulpjs/findup-sync" "flat-cache@3.0.4","MIT","Copyright (c) 2015 Roy Riojas","https://github.com/royriojas/flat-cache" "flatted@3.2.7","ISC","Copyright (c) 2018-2020, Andrea Giammarchi, @WebReflection","https://github.com/WebReflection/flatted" "follow-redirects@1.15.2","MIT","Copyright 2014–present Olivier Lalonde , James Talmage , Ruben Verborgh","https://github.com/follow-redirects/follow-redirects" +"for-each@0.3.3","MIT","Copyright (c) 2012 Raynos.","https://github.com/Raynos/for-each" "form-data@3.0.1","MIT","Copyright (c) 2012 Felix Geisendörfer (felix@debuggable.com) and contributors","https://github.com/form-data/form-data" "form-data@4.0.0","MIT","Copyright (c) 2012 Felix Geisendörfer (felix@debuggable.com) and contributors","https://github.com/form-data/form-data" "forwarded@0.2.0","MIT","Copyright (c) 2014-2017 Douglas Christopher Wilson","https://github.com/jshttp/forwarded" @@ -903,7 +906,7 @@ "function.prototype.name@1.1.5","MIT","Copyright (c) 2016 Jordan Harband","https://github.com/es-shims/Function.prototype.name" "functions-have-names@1.2.3","MIT","Copyright (c) 2019 Jordan Harband","https://github.com/inspect-js/functions-have-names" "gauge@4.0.4","ISC","Copyright npm, Inc.","https://github.com/npm/gauge" -"gensequence@4.0.2","MIT","Copyright (c) 2016 Jason Dent","https://github.com/Jason3S/GenSequence" +"gensequence@4.0.3","MIT","Copyright (c) 2016 Jason Dent","https://github.com/Jason3S/GenSequence" "gensync@1.0.0-beta.2","MIT","Copyright 2018 Logan Smyth ","https://github.com/loganfsmyth/gensync" "get-assigned-identifiers@1.2.0","Apache-2.0","Copyright 2017 Renée Kooi ","https://github.com/goto-bus-stop/get-assigned-identifiers" "get-caller-file@2.0.5","ISC","","https://github.com/stefanpenner/get-caller-file" @@ -928,15 +931,15 @@ "glob@7.2.3","ISC","Copyright (c) Isaac Z. Schlueter and Contributors","https://github.com/isaacs/node-glob" "glob@8.0.3","ISC","Copyright (c) 2009-2022 Isaac Z. Schlueter and Contributors","https://github.com/isaacs/node-glob" "global-dirs@0.1.1","MIT","Copyright (c) Sindre Sorhus (sindresorhus.com)","https://github.com/sindresorhus/global-dirs" -"global-modules@1.0.0","MIT","Copyright (c) 2015-2017, Jon Schlinkert.","https://github.com/jonschlinkert/global-modules" "global-modules@2.0.0","MIT","Copyright (c) 2015-present, Jon Schlinkert.","https://github.com/jonschlinkert/global-modules" -"global-prefix@1.0.2","MIT","Copyright (c) 2015-2017, Jon Schlinkert.","https://github.com/jonschlinkert/global-prefix" "global-prefix@3.0.0","MIT","Copyright (c) 2015-present, Jon Schlinkert.","https://github.com/jonschlinkert/global-prefix" "globals@11.12.0","MIT","Copyright (c) Sindre Sorhus (sindresorhus.com)","https://github.com/sindresorhus/globals" "globals@13.17.0","MIT","Copyright (c) Sindre Sorhus (https://sindresorhus.com)","https://github.com/sindresorhus/globals" +"globals@13.19.0","MIT","Copyright (c) Sindre Sorhus (https://sindresorhus.com)","https://github.com/sindresorhus/globals" "globby@11.1.0","MIT","Copyright (c) Sindre Sorhus (sindresorhus.com)","https://github.com/sindresorhus/globby" "globby@13.1.2","MIT","Copyright (c) Sindre Sorhus (https://sindresorhus.com)","https://github.com/sindresorhus/globby" "globjoin@0.1.4","MIT","Copyright (c) 2016 amobiz","https://github.com/amobiz/globjoin" +"gopd@1.0.1","MIT","Copyright (c) 2022 Jordan Harband","https://github.com/ljharb/gopd" "graceful-fs@4.2.10","ISC","Copyright (c) 2011-2022 Isaac Z. Schlueter, Ben Noordhuis, and Contributors","https://github.com/isaacs/node-graceful-fs" "grapheme-splitter@1.0.4","MIT","Copyright (c) 2015 Orlin Georgiev","https://github.com/orling/grapheme-splitter" "guess-parser@0.4.22","MIT","Copyright (c) 2018 Minko Gechev and the Guess . contributors","https://github.com/guess-js/guess" @@ -955,7 +958,6 @@ "has@1.0.3","MIT","Copyright (c) 2013 Thiago de Arruda","https://github.com/tarruda/has" "hdr-histogram-js@2.0.3","BSD*","Copyright (c) 2016, Alexandre Victoor. All rights reserved.","https://github.com/HdrHistogram/HdrHistogramJS" "hdr-histogram-percentiles-obj@3.0.0","MIT","Copyright (c) 2016 Glen Keane","https://github.com/GlenTiki/hdr-histogram-percentiles-obj" -"homedir-polyfill@1.0.3","MIT","Copyright (c) 2016 Brian Woodward","https://github.com/doowb/homedir-polyfill" "hosted-git-info@2.8.9","ISC","Copyright (c) 2015, Rebecca Turner","https://github.com/npm/hosted-git-info" "hosted-git-info@4.1.0","ISC","Copyright (c) 2015, Rebecca Turner","https://github.com/npm/hosted-git-info" "hosted-git-info@5.1.0","ISC","Copyright (c) 2015, Rebecca Turner","https://github.com/npm/hosted-git-info" @@ -982,7 +984,7 @@ "human-signals@2.1.0","Apache-2.0","","https://github.com/ehmicky/human-signals" "human-signals@3.0.1","Apache-2.0","","https://github.com/ehmicky/human-signals" "humanize-ms@1.2.1","MIT","","https://github.com/node-modules/humanize-ms" -"husky@8.0.1","MIT","Copyright (c) 2021 typicode","https://github.com/typicode/husky" +"husky@8.0.2","MIT","Copyright (c) 2021 typicode","https://github.com/typicode/husky" "i18next@21.9.2","MIT","Copyright (c) 2022 i18next","https://github.com/i18next/i18next" "iconv-lite@0.4.24","MIT","Copyright (c) 2011 Alexander Shtuchkin","https://github.com/ashtuchkin/iconv-lite" "iconv-lite@0.6.3","MIT","Copyright (c) 2011 Alexander Shtuchkin","https://github.com/ashtuchkin/iconv-lite" @@ -990,6 +992,7 @@ "ieee754@1.2.1","BSD-3-Clause","Copyright 2008 Fair Oaks Labs, Inc.","https://github.com/feross/ieee754" "ignore-walk@5.0.1","ISC","Copyright (c) Isaac Z. Schlueter and Contributors","https://github.com/npm/ignore-walk" "ignore@5.2.0","MIT","Copyright (c) 2013 Kael Zhang , contributors. http://kael.me/","https://github.com/kaelzhang/node-ignore" +"ignore@5.2.1","MIT","Copyright (c) 2013 Kael Zhang , contributors. http://kael.me/","https://github.com/kaelzhang/node-ignore" "image-size@0.5.5","MIT","Copyright © 2017 Aditya Yadav, http://netroy.in","https://github.com/image-size/image-size" "immutable@3.8.2","MIT","Copyright (c) 2014-present, Facebook, Inc.","https://github.com/facebook/immutable-js" "immutable@4.1.0","MIT","Copyright (c) 2014-present, Lee Byron and other contributors.","https://github.com/immutable-js/immutable-js" @@ -1008,7 +1011,7 @@ "inquirer@8.2.4","MIT","Copyright (c) 2022 Simon Boudrias","https://github.com/SBoudrias/Inquirer.js" "inside@1.0.0","Public Domain","","https://github.com/aantthony/node-self" "internal-slot@1.0.3","MIT","Copyright (c) 2019 Jordan Harband","https://github.com/ljharb/internal-slot" -"intershop-pwa@3.1.0","UNLICENSED","Copyright (c) 2022 Intershop Communications AG, http://www.intershop.de","" +"intershop-pwa@3.2.0","UNLICENSED","Copyright (c) 2022 Intershop Communications AG, http://www.intershop.de","" "intershop-schematics@0.0.1","UNKNOWN","","https://github.com/intershop/intershop-pwa" "ip@1.1.8","MIT","Copyright Fedor Indutny, 2012.","https://github.com/indutny/node-ip" "ip@2.0.0","MIT","Copyright Fedor Indutny, 2012.","https://github.com/indutny/node-ip" @@ -1032,11 +1035,13 @@ "is-glob@4.0.3","MIT","Copyright (c) 2014-2017, Jon Schlinkert.","https://github.com/micromatch/is-glob" "is-interactive@1.0.0","MIT","Copyright (c) Sindre Sorhus (sindresorhus.com)","https://github.com/sindresorhus/is-interactive" "is-lambda@1.0.1","MIT","Copyright (c) 2016-2017 Thomas Watson Steen","https://github.com/watson/is-lambda" +"is-map@2.0.2","MIT","Copyright (c) 2019 Inspect JS","https://github.com/inspect-js/is-map" "is-negative-zero@2.0.2","MIT","Copyright (c) 2014 Jordan Harband","https://github.com/inspect-js/is-negative-zero" "is-number-like@1.0.8","ISC","Copyright (c) 2016, Vigour.io","https://github.com/vigour-io/is-number-like" "is-number-object@1.0.7","MIT","Copyright (c) 2015 Jordan Harband","https://github.com/inspect-js/is-number-object" "is-number@7.0.0","MIT","Copyright (c) 2014-present, Jon Schlinkert.","https://github.com/jonschlinkert/is-number" "is-obj@2.0.0","MIT","Copyright (c) Sindre Sorhus (sindresorhus.com)","https://github.com/sindresorhus/is-obj" +"is-path-inside@3.0.3","MIT","Copyright (c) Sindre Sorhus (sindresorhus.com)","https://github.com/sindresorhus/is-path-inside" "is-plain-obj@1.1.0","MIT","Copyright (c) Sindre Sorhus (sindresorhus.com)","https://github.com/sindresorhus/is-plain-obj" "is-plain-obj@3.0.0","MIT","Copyright (c) Sindre Sorhus (https://sindresorhus.com)","https://github.com/sindresorhus/is-plain-obj" "is-plain-object@2.0.4","MIT","Copyright (c) 2014-2017, Jon Schlinkert.","https://github.com/jonschlinkert/is-plain-object" @@ -1044,22 +1049,25 @@ "is-potential-custom-element-name@1.0.1","MIT","Copyright Mathias Bynens ","https://github.com/mathiasbynens/is-potential-custom-element-name" "is-promise@2.2.2","MIT","Copyright (c) 2014 Forbes Lindesay","https://github.com/then/is-promise" "is-regex@1.1.4","MIT","Copyright (c) 2014 Jordan Harband","https://github.com/inspect-js/is-regex" +"is-set@2.0.2","MIT","Copyright (c) 2019 Inspect JS","https://github.com/inspect-js/is-set" "is-shared-array-buffer@1.0.2","MIT","Copyright (c) 2021 Inspect JS","https://github.com/inspect-js/is-shared-array-buffer" "is-stream@2.0.1","MIT","Copyright (c) Sindre Sorhus (https://sindresorhus.com)","https://github.com/sindresorhus/is-stream" "is-stream@3.0.0","MIT","Copyright (c) Sindre Sorhus (https://sindresorhus.com)","https://github.com/sindresorhus/is-stream" "is-string@1.0.7","MIT","Copyright (c) 2015 Jordan Harband","https://github.com/ljharb/is-string" "is-symbol@1.0.4","MIT","Copyright (c) 2015 Jordan Harband","https://github.com/inspect-js/is-symbol" "is-text-path@1.0.1","MIT","Copyright (c) Sindre Sorhus (sindresorhus.com)","https://github.com/sindresorhus/is-text-path" +"is-typed-array@1.1.10","MIT","Copyright (c) 2015 Jordan Harband","https://github.com/inspect-js/is-typed-array" "is-typedarray@1.0.0","MIT","","https://github.com/hughsk/is-typedarray" "is-unicode-supported@0.1.0","MIT","Copyright (c) Sindre Sorhus (https://sindresorhus.com)","https://github.com/sindresorhus/is-unicode-supported" -"is-utf8@0.2.1","MIT","Copyright (C) 2014 Wei Fanzhe","https://github.com/wayfind/is-utf8" +"is-weakmap@2.0.1","MIT","Copyright (c) 2019 Inspect JS","https://github.com/inspect-js/is-weakmap" "is-weakref@1.0.2","MIT","Copyright (c) 2020 Inspect JS","https://github.com/inspect-js/is-weakref" +"is-weakset@2.0.2","MIT","Copyright (c) 2019 Inspect JS","https://github.com/inspect-js/is-weakset" "is-what@3.14.1","MIT","Copyright (c) 2018 Luca Ban - Mesqueeb","https://github.com/mesqueeb/is-what" -"is-windows@1.0.2","MIT","Copyright (c) 2015-2018, Jon Schlinkert.","https://github.com/jonschlinkert/is-windows" "is-wsl@1.1.0","MIT","Copyright (c) Sindre Sorhus (sindresorhus.com)","https://github.com/sindresorhus/is-wsl" "is-wsl@2.2.0","MIT","Copyright (c) Sindre Sorhus (sindresorhus.com)","https://github.com/sindresorhus/is-wsl" "isarray@0.0.1","MIT","Copyright (c) 2013 Julian Gruber <julian@juliangruber.com>","https://github.com/juliangruber/isarray" "isarray@1.0.0","MIT","Copyright (c) 2013 Julian Gruber <julian@juliangruber.com>","https://github.com/juliangruber/isarray" +"isarray@2.0.5","MIT","Copyright (c) 2013 Julian Gruber ","https://github.com/juliangruber/isarray" "isexe@2.0.0","ISC","Copyright (c) Isaac Z. Schlueter and Contributors","https://github.com/isaacs/isexe" "isobject@3.0.1","MIT","Copyright (c) 2014-2017, Jon Schlinkert.","https://github.com/jonschlinkert/isobject" "istanbul-lib-coverage@3.2.0","BSD-3-Clause","Copyright 2012-2015 Yahoo! Inc.. All rights reserved.","https://github.com/istanbuljs/istanbuljs" @@ -1125,6 +1133,7 @@ "jsdom@20.0.0","MIT","Copyright (c) 2010 Elijah Insua","https://github.com/jsdom/jsdom" "jsesc@0.5.0","MIT","Copyright Mathias Bynens ","https://github.com/mathiasbynens/jsesc" "jsesc@2.5.2","MIT","Copyright Mathias Bynens ","https://github.com/mathiasbynens/jsesc" +"jsesc@3.0.2","MIT","Copyright Mathias Bynens ","https://github.com/mathiasbynens/jsesc" "json-parse-better-errors@1.0.2","MIT","Copyright 2017 Kat Marchán","https://github.com/zkat/json-parse-better-errors" "json-parse-even-better-errors@2.3.1","MIT","Copyright 2017 Kat Marchán. Copyright npm, Inc.","https://github.com/npm/json-parse-even-better-errors" "json-schema-to-typescript@11.0.2","MIT","","https://github.com/bcherny/json-schema-to-typescript" @@ -1144,7 +1153,7 @@ "kind-of@6.0.3","MIT","Copyright (c) 2014-2017, Jon Schlinkert.","https://github.com/jonschlinkert/kind-of" "kleur@3.0.3","MIT","Copyright (c) Luke Edwards (lukeed.com)","https://github.com/lukeed/kleur" "klona@2.0.5","MIT","Copyright (c) Luke Edwards (lukeed.com)","https://github.com/lukeed/klona" -"known-css-properties@0.25.0","MIT","Copyright (c) 2017 Mavrix Technologies","https://github.com/known-css/known-css-properties" +"known-css-properties@0.26.0","MIT","Copyright (c) 2017 Mavrix Technologies","https://github.com/known-css/known-css-properties" "lazy@1.0.11","MIT","","https://github.com/pkrumins/node-lazy" "less-loader@11.0.0","MIT","Copyright JS Foundation and other contributors","https://github.com/webpack-contrib/less-loader" "less@4.1.3","Apache-2.0","","https://github.com/less/less.js" @@ -1152,35 +1161,41 @@ "levn@0.3.0","MIT","Copyright (c) George Zahariev","https://github.com/gkz/levn" "levn@0.4.1","MIT","Copyright (c) George Zahariev","https://github.com/gkz/levn" "license-webpack-plugin@4.0.2","ISC","Copyright (c) 2016, S K (xz64)","https://github.com/xz64/license-webpack-plugin" -"lilconfig@2.0.5","MIT","","https://github.com/antonk52/lilconfig" +"lilconfig@2.0.6","MIT","Copyright (c) 2022 Anton Kastritskiy","https://github.com/antonk52/lilconfig" "limiter@1.1.5","MIT","Copyright (C) 2011 by John Hurliman","https://github.com/jhurliman/node-rate-limiter" "lines-and-columns@1.2.4","MIT","Copyright (c) 2015 Brian Donovan","https://github.com/eventualbuddha/lines-and-columns" -"lint-staged@13.0.3","MIT","Copyright (c) 2016 Andrey Okonetchnikov","https://github.com/okonet/lint-staged" -"listr2@4.0.5","MIT","Copyright (c) Cenk Kilic (https://srcs.kilic.dev), Sam Verschueren (github.com/SamVerschueren)","https://github.com/cenk1cenk2/listr2" +"lint-staged@13.1.0","MIT","Copyright (c) 2016 Andrey Okonetchnikov","https://github.com/okonet/lint-staged" +"listr2@5.0.6","MIT","Copyright (c) Cenk Kilic (https://srcs.kilic.dev), Sam Verschueren (github.com/SamVerschueren)","https://github.com/cenk1cenk2/listr2" "load-json-file@4.0.0","MIT","Copyright (c) Sindre Sorhus (sindresorhus.com)","https://github.com/sindresorhus/load-json-file" "loader-runner@4.3.0","MIT","Copyright (c) Tobias Koppers @sokra","https://github.com/webpack/loader-runner" -"loader-utils@2.0.2","MIT","Copyright JS Foundation and other contributors","https://github.com/webpack/loader-utils" -"loader-utils@3.2.0","MIT","Copyright JS Foundation and other contributors","https://github.com/webpack/loader-utils" +"loader-utils@2.0.4","MIT","Copyright JS Foundation and other contributors","https://github.com/webpack/loader-utils" +"loader-utils@3.2.1","MIT","Copyright JS Foundation and other contributors","https://github.com/webpack/loader-utils" "localtunnel@2.0.2","MIT","Copyright (c) 2018 Roman Shtylman","https://github.com/localtunnel/localtunnel" "locate-path@2.0.0","MIT","Copyright (c) Sindre Sorhus (sindresorhus.com)","https://github.com/sindresorhus/locate-path" "locate-path@5.0.0","MIT","Copyright (c) Sindre Sorhus (sindresorhus.com)","https://github.com/sindresorhus/locate-path" "locate-path@6.0.0","MIT","Copyright (c) Sindre Sorhus (https://sindresorhus.com)","https://github.com/sindresorhus/locate-path" "lodash-es@4.17.21","MIT","Copyright OpenJS Foundation and other contributors ","https://github.com/lodash/lodash" -"lodash.clonedeep@4.5.0","MIT","Copyright jQuery Foundation and other contributors ","https://github.com/lodash/lodash" +"lodash.camelcase@4.3.0","MIT","Copyright jQuery Foundation and other contributors ","https://github.com/lodash/lodash" "lodash.debounce@4.0.8","MIT","Copyright jQuery Foundation and other contributors ","https://github.com/lodash/lodash" "lodash.isfinite@3.3.2","MIT","Copyright jQuery Foundation and other contributors ","https://github.com/lodash/lodash" +"lodash.isfunction@3.0.9","MIT","Copyright JS Foundation and other contributors ","https://github.com/lodash/lodash" "lodash.ismatch@4.4.0","MIT","Copyright jQuery Foundation and other contributors ","https://github.com/lodash/lodash" -"lodash.map@4.6.0","MIT","Copyright jQuery Foundation and other contributors ","https://github.com/lodash/lodash" +"lodash.isplainobject@4.0.6","MIT","Copyright jQuery Foundation and other contributors ","https://github.com/lodash/lodash" +"lodash.kebabcase@4.1.1","MIT","Copyright jQuery Foundation and other contributors ","https://github.com/lodash/lodash" "lodash.memoize@4.1.2","MIT","Copyright jQuery Foundation and other contributors ","https://github.com/lodash/lodash" "lodash.merge@4.6.2","MIT","Copyright OpenJS Foundation and other contributors ","https://github.com/lodash/lodash" +"lodash.mergewith@4.6.2","MIT","Copyright OpenJS Foundation and other contributors ","https://github.com/lodash/lodash" +"lodash.snakecase@4.1.1","MIT","Copyright jQuery Foundation and other contributors ","https://github.com/lodash/lodash" +"lodash.startcase@4.4.0","MIT","Copyright jQuery Foundation and other contributors ","https://github.com/lodash/lodash" "lodash.truncate@4.4.2","MIT","Copyright jQuery Foundation and other contributors ","https://github.com/lodash/lodash" +"lodash.uniq@4.5.0","MIT","Copyright jQuery Foundation and other contributors ","https://github.com/lodash/lodash" +"lodash.upperfirst@4.3.1","MIT","Copyright jQuery Foundation and other contributors ","https://github.com/lodash/lodash" "lodash@4.17.21","MIT","Copyright OpenJS Foundation and other contributors ","https://github.com/lodash/lodash" "log-driver@1.2.7","ISC","Copyright (c) 2014, Gregg Caines, gregg@caines.ca","https://github.com/cainus/logdriver" "log-symbols@4.1.0","MIT","Copyright (c) Sindre Sorhus (https://sindresorhus.com)","https://github.com/sindresorhus/log-symbols" "log-update@4.0.0","MIT","Copyright (c) Sindre Sorhus (sindresorhus.com)","https://github.com/sindresorhus/log-update" "loglevel-plugin-prefix@0.8.4","MIT","Copyright (c) 2017 Evgeniy Pavlov","https://github.com/kutuluk/loglevel-plugin-prefix" "loglevel@1.8.0","MIT","Copyright (c) 2013 Tim Perry","https://github.com/pimterry/loglevel" -"longest@2.0.1","MIT","Copyright (c) 2014-2017, Jon Schlinkert","https://github.com/jonschlinkert/longest" "lru-cache@5.1.1","ISC","Copyright (c) Isaac Z. Schlueter and Contributors","https://github.com/isaacs/node-lru-cache" "lru-cache@6.0.0","ISC","Copyright (c) Isaac Z. Schlueter and Contributors","https://github.com/isaacs/node-lru-cache" "lru-cache@7.14.0","ISC","Copyright (c) 2010-2022 Isaac Z. Schlueter and Contributors","https://github.com/isaacs/node-lru-cache" @@ -1201,7 +1216,7 @@ "marked@4.1.1","MIT","Copyright (c) 2018+, MarkedJS (https://github.com/markedjs/). Copyright (c) 2011-2018, Christopher Jeffrey (https://github.com/chjj/)*","https://github.com/markedjs/marked" "mathml-tag-names@2.1.3","MIT","Copyright (c) 2016 Titus Wormer ","https://github.com/wooorm/mathml-tag-names" "media-typer@0.3.0","MIT","Copyright (c) 2014 Douglas Christopher Wilson","https://github.com/jshttp/media-typer" -"memfs@3.4.7","Unlicense","","https://github.com/streamich/memfs" +"memfs@3.4.12","Unlicense","","https://github.com/streamich/memfs" "memoizee@0.4.15","ISC","Copyright (c) 2012-2018, Mariusz Nowak, @medikoo, medikoo.com","https://github.com/medikoo/memoizee" "memorystream@0.3.1","MIT","Copyright (C) 2011 Dmitry Nizovtsev","https://github.com/JSBizon/node-memorystream" "meow@8.1.2","MIT","Copyright (c) Sindre Sorhus (https://sindresorhus.com)","https://github.com/sindresorhus/meow" @@ -1210,7 +1225,6 @@ "merge-source-map@1.0.4","MIT","Copyright (c) keik ","https://github.com/keik/merge-source-map" "merge-stream@2.0.0","MIT","Copyright (c) Stephen Sugden (stephensugden.com)","https://github.com/grncdr/merge-stream" "merge2@1.4.1","MIT","Copyright (c) 2014-2020 Teambition","https://github.com/teambition/merge2" -"merge@2.1.1","MIT","","https://github.com/yeikos/js.merge" "methods@1.1.2","MIT","Copyright (c) 2013-2014 TJ Holowaychuk . Copyright (c) 2015-2016 Douglas Christopher Wilson ","https://github.com/jshttp/methods" "micromatch@4.0.5","MIT","Copyright (c) 2014-present, Jon Schlinkert.","https://github.com/micromatch/micromatch" "mime-db@1.52.0","MIT","Copyright (c) 2014 Jonathan Ong . Copyright (c) 2015-2022 Douglas Christopher Wilson ","https://github.com/jshttp/mime-db" @@ -1251,18 +1265,19 @@ "mute-stream@0.0.8","ISC","Copyright (c) Isaac Z. Schlueter and Contributors","https://github.com/isaacs/mute-stream" "mz@2.7.0","MIT","Copyright (c) 2014-2016 Jonathan Ong me@jongleberry.com and Contributors","https://github.com/normalize/mz" "nanoid@3.3.4","MIT","Copyright 2017 Andrey Sitnik ","https://github.com/ai/nanoid" +"natural-compare-lite@1.4.0","MIT","Copyright (c) 2012-2015 Lauri Rooden <lauri@rooden.ee> . [The MIT License](http://lauri.rooden.ee/mit-license.txt)","https://github.com/litejs/natural-compare-lite" "natural-compare@1.4.0","MIT","Copyright (c) 2012-2015 Lauri Rooden <lauri@rooden.ee> . [The MIT License](http://lauri.rooden.ee/mit-license.txt)","https://github.com/litejs/natural-compare-lite" "needle@2.4.0","MIT","Copyright (c) Fork, Ltd.","https://github.com/tomas/needle" -"needle@3.1.0","MIT","Copyright (c) Fork, Ltd.","https://github.com/tomas/needle" +"needle@3.2.0","MIT","Copyright (c) Fork, Ltd.","https://github.com/tomas/needle" "negotiator@0.6.3","MIT","Copyright (c) 2012-2014 Federico Romero. Copyright (c) 2012-2014 Isaac Z. Schlueter. Copyright (c) 2014-2015 Douglas Christopher Wilson","https://github.com/jshttp/negotiator" "neo-async@2.6.2","MIT","Copyright (c) 2014-2018 Suguru Motegi. Based on Async.js, Copyright Caolan McMahon","https://github.com/suguru03/neo-async" "netmask@2.0.2","MIT","Copyright (c) 2011 Olivier Poitrey ","https://github.com/rs/node-netmask" "next-tick@1.1.0","ISC","Copyright (c) 2012-2020, Mariusz Nowak, @medikoo, medikoo.com","https://github.com/medikoo/next-tick" -"ng-mocks@14.2.3","MIT","Copyright (c) 2017 Isaac Datlof","https://github.com/help-me-mom/ng-mocks" +"ng-mocks@14.5.0","MIT","Copyright (c) 2017 Isaac Datlof","https://github.com/help-me-mom/ng-mocks" "ng-morph@2.1.1","Apache-2.0","","https://github.com/TinkoffCreditSystems/ng-morph" "ng-recaptcha@10.0.0","MIT","Copyright (c) 2016 Ruslan Arkhipau","https://github.com/DethAriel/ng-recaptcha" -"ngx-infinite-scroll@14.0.0","MIT","","https://github.com/orizens/ngx-infinite-scroll" -"ngx-toastr@15.2.0","MIT","Copyright (c) Scott Cooper ","https://github.com/scttcper/ngx-toastr" +"ngx-infinite-scroll@14.0.1","MIT","","https://github.com/orizens/ngx-infinite-scroll" +"ngx-toastr@15.2.2","MIT","Copyright (c) Scott Cooper ","https://github.com/scttcper/ngx-toastr" "nice-try@1.0.5","MIT","Copyright (c) 2018 Tobias Reich","https://github.com/electerious/nice-try" "node-fetch@2.6.7","MIT","Copyright (c) 2016 David Frank","https://github.com/bitinn/node-fetch" "node-forge@1.3.1","(BSD-3-Clause OR GPL-2.0)","","https://github.com/digitalbazaar/forge" @@ -1335,7 +1350,6 @@ "parse-json@4.0.0","MIT","Copyright (c) Sindre Sorhus (sindresorhus.com)","https://github.com/sindresorhus/parse-json" "parse-json@5.2.0","MIT","Copyright (c) Sindre Sorhus (https://sindresorhus.com)","https://github.com/sindresorhus/parse-json" "parse-node-version@1.0.1","MIT","Copyright (c) 2018 Blaine Bublitz and Eric Schoffstall ","https://github.com/gulpjs/parse-node-version" -"parse-passwd@1.0.0","MIT","Copyright (c) 2016 Brian Woodward","https://github.com/doowb/parse-passwd" "parse5-html-rewriting-stream@6.0.1","MIT","Copyright (c) 2013-2019 Ivan Nikulin (ifaaan@gmail.com, https://github.com/inikulin)","https://github.com/inikulin/parse5" "parse5-htmlparser2-tree-adapter@6.0.1","MIT","Copyright (c) 2013-2019 Ivan Nikulin (ifaaan@gmail.com, https://github.com/inikulin)","https://github.com/inikulin/parse5" "parse5-htmlparser2-tree-adapter@7.0.0","MIT","Copyright (c) 2013-2019 Ivan Nikulin (ifaaan@gmail.com, https://github.com/inikulin)","https://github.com/inikulin/parse5" @@ -1343,6 +1357,7 @@ "parse5@5.1.1","MIT","Copyright (c) 2013-2019 Ivan Nikulin (ifaaan@gmail.com, https://github.com/inikulin)","https://github.com/inikulin/parse5" "parse5@6.0.1","MIT","Copyright (c) 2013-2019 Ivan Nikulin (ifaaan@gmail.com, https://github.com/inikulin)","https://github.com/inikulin/parse5" "parse5@7.1.1","MIT","Copyright (c) 2013-2019 Ivan Nikulin (ifaaan@gmail.com, https://github.com/inikulin)","https://github.com/inikulin/parse5" +"parse5@7.1.2","MIT","Copyright (c) 2013-2019 Ivan Nikulin (ifaaan@gmail.com, https://github.com/inikulin)","https://github.com/inikulin/parse5" "parseurl@1.3.3","MIT","Copyright (c) 2014 Jonathan Ong . Copyright (c) 2014-2017 Douglas Christopher Wilson ","https://github.com/pillarjs/parseurl" "path-browserify@1.0.1","MIT","Copyright (c) 2013 James Halliday","https://github.com/browserify/path-browserify" "path-exists@3.0.0","MIT","Copyright (c) Sindre Sorhus (sindresorhus.com)","https://github.com/sindresorhus/path-exists" @@ -1376,7 +1391,7 @@ "pm2-deploy@1.0.2","MIT","Copyright (c) 2010-2015","https://github.com/Unitech/pm2-deploy" "pm2-multimeter@0.1.2","MIT*","","https://github.com/Unitech/node-multimeter" "pm2-sysmonit@1.2.8","Apache*","","" -"pm2@5.2.0","AGPL-3.0","","https://github.com/Unitech/pm2" +"pm2@5.2.2","AGPL-3.0","","https://github.com/Unitech/pm2" "png-js@1.0.0","MIT*","Copyright (c) 2017 Devon Govett","https://github.com/devongovett/png.js" "popper.js@1.16.1","MIT","","https://github.com/FezVrasta/popper.js" "portscanner@2.2.0","MIT","Copyright (c) 2011 Brandon Ace Alexander","https://github.com/baalexander/node-portscanner" @@ -1386,7 +1401,7 @@ "postcss-color-hex-alpha@8.0.4","MIT","Copyright © PostCSS","https://github.com/csstools/postcss-plugins" "postcss-color-rebeccapurple@7.1.1","CC0-1.0","","https://github.com/csstools/postcss-plugins" "postcss-custom-media@8.0.2","MIT","Copyright © PostCSS","https://github.com/csstools/postcss-plugins" -"postcss-custom-properties@12.1.9","MIT","Copyright © PostCSS","https://github.com/csstools/postcss-plugins" +"postcss-custom-properties@12.1.11","MIT","Copyright © PostCSS","https://github.com/csstools/postcss-plugins" "postcss-custom-selectors@6.0.3","MIT","Copyright © PostCSS","https://github.com/csstools/postcss-plugins" "postcss-dir-pseudo-class@6.0.5","CC0-1.0","","https://github.com/csstools/postcss-plugins" "postcss-double-position-gradients@3.1.2","CC0-1.0","","https://github.com/csstools/postcss-plugins" @@ -1419,10 +1434,11 @@ "postcss-safe-parser@6.0.0","MIT","Copyright 2013 Andrey Sitnik ","https://github.com/postcss/postcss-safe-parser" "postcss-scss@4.0.5","MIT","Copyright 2013 Andrey Sitnik ","https://github.com/postcss/postcss-scss" "postcss-selector-not@6.0.1","MIT","Copyright (c) 2017 Maxime Thirouin","https://github.com/csstools/postcss-plugins" -"postcss-selector-parser@6.0.10","MIT","Copyright (c) Ben Briggs (http://beneb.info)","https://github.com/postcss/postcss-selector-parser" +"postcss-selector-parser@6.0.11","MIT","Copyright (c) Ben Briggs (http://beneb.info)","https://github.com/postcss/postcss-selector-parser" "postcss-sorting@7.0.1","MIT","Copyright 2015-present Aleks Hudochenkov ","https://github.com/hudochenkov/postcss-sorting" "postcss-value-parser@4.2.0","MIT","Copyright (c) Bogdan Chadkin ","https://github.com/TrySound/postcss-value-parser" "postcss@8.4.16","MIT","Copyright 2013 Andrey Sitnik ","https://github.com/postcss/postcss" +"postcss@8.4.20","MIT","Copyright 2013 Andrey Sitnik ","https://github.com/postcss/postcss" "prelude-ls@1.1.2","MIT","Copyright (c) George Zahariev","https://github.com/gkz/prelude-ls" "prelude-ls@1.2.1","MIT","Copyright (c) George Zahariev","https://github.com/gkz/prelude-ls" "prettier-linter-helpers@1.0.0","MIT","Copyright © 2017 Andres Suarez and Teddy Katz","https://github.com/prettier/prettier-linter-helpers" @@ -1448,7 +1464,7 @@ "purgecss-webpack-plugin@5.0.0","MIT","","https://github.com/FullHuman/purgecss" "purgecss@5.0.0","MIT","","https://github.com/FullHuman/purgecss" "q@1.5.1","MIT","Copyright 2009–2017 Kristopher Michael Kowal. All rights reserved.. Permission is hereby granted, free of charge, to any person obtaining a copy. of this software and associated documentation files (the "Software"), to. deal in the Software without restriction, including without limitation the. rights to use, copy, modify, merge, publish, distribute, sublicense, and/or. sell copies of the Software, and to permit persons to whom the Software is. furnished to do so, subject to the following conditions:","https://github.com/kriskowal/q" -"qs@6.10.3","BSD-3-Clause","Copyright (c) 2014, Nathan LaFreniere and other [contributors](https://github.com/ljharb/qs/graphs/contributors). All rights reserved.","https://github.com/ljharb/qs" +"qs@6.11.0","BSD-3-Clause","Copyright (c) 2014, Nathan LaFreniere and other [contributors](https://github.com/ljharb/qs/graphs/contributors). All rights reserved.","https://github.com/ljharb/qs" "qs@6.2.3","BSD-3-Clause","Copyright (c) 2014 Nathan LaFreniere and other contributors.. All rights reserved.","https://github.com/ljharb/qs" "querystringify@2.2.0","MIT","Copyright (c) 2015 Unshift.io, Arnout Kazemier, the Contributors.","https://github.com/unshiftio/querystringify" "queue-microtask@1.2.3","MIT","Copyright (c) Feross Aboukhadijeh","https://github.com/feross/queue-microtask" @@ -1490,7 +1506,6 @@ "requireindex@1.2.0","MIT","","https://github.com/stephenhandley/requireindex" "requires-port@1.0.0","MIT","Copyright (c) 2015 Unshift.io, Arnout Kazemier, the Contributors.","https://github.com/unshiftio/requires-port" "resolve-cwd@3.0.0","MIT","Copyright (c) Sindre Sorhus (sindresorhus.com)","https://github.com/sindresorhus/resolve-cwd" -"resolve-dir@1.0.1","MIT","Copyright (c) 2015-2016, Jon Schlinkert","https://github.com/jonschlinkert/resolve-dir" "resolve-from@4.0.0","MIT","Copyright (c) Sindre Sorhus (sindresorhus.com)","https://github.com/sindresorhus/resolve-from" "resolve-from@5.0.0","MIT","Copyright (c) Sindre Sorhus (sindresorhus.com)","https://github.com/sindresorhus/resolve-from" "resolve-global@1.0.0","MIT","Copyright (c) Sindre Sorhus (sindresorhus.com)","https://github.com/sindresorhus/resolve-global" @@ -1535,6 +1550,7 @@ "semver@6.3.0","ISC","Copyright (c) Isaac Z. Schlueter and Contributors","https://github.com/npm/node-semver" "semver@7.2.3","ISC","Copyright (c) Isaac Z. Schlueter and Contributors","https://github.com/npm/node-semver" "semver@7.3.7","ISC","Copyright (c) Isaac Z. Schlueter and Contributors","https://github.com/npm/node-semver" +"semver@7.3.8","ISC","Copyright (c) Isaac Z. Schlueter and Contributors","https://github.com/npm/node-semver" "send@0.16.2","MIT","Copyright (c) 2012 TJ Holowaychuk. Copyright (c) 2014-2016 Douglas Christopher Wilson","https://github.com/pillarjs/send" "send@0.18.0","MIT","Copyright (c) 2012 TJ Holowaychuk. Copyright (c) 2014-2022 Douglas Christopher Wilson","https://github.com/pillarjs/send" "serialize-javascript@6.0.0","BSD-3-Clause","Copyright 2014 Yahoo! Inc.. All rights reserved.","https://github.com/yahoo/serialize-javascript" @@ -1574,7 +1590,6 @@ "source-map-js@1.0.2","BSD-3-Clause","","https://github.com/7rulnik/source-map-js" "source-map-loader@4.0.0","MIT","Copyright JS Foundation and other contributors","https://github.com/webpack-contrib/source-map-loader" "source-map-support@0.5.13","MIT","Copyright (c) 2014 Evan Wallace","https://github.com/evanw/node-source-map-support" -"source-map-support@0.5.19","MIT","Copyright (c) 2014 Evan Wallace","https://github.com/evanw/node-source-map-support" "source-map-support@0.5.21","MIT","Copyright (c) 2014 Evan Wallace","https://github.com/evanw/node-source-map-support" "source-map@0.1.43","BSD","","https://github.com/mozilla/source-map" "source-map@0.5.7","BSD-3-Clause","","https://github.com/mozilla/source-map" @@ -1626,16 +1641,15 @@ "strip-indent@3.0.0","MIT","Copyright (c) Sindre Sorhus (sindresorhus.com)","https://github.com/sindresorhus/strip-indent" "strip-json-comments@3.1.1","MIT","Copyright (c) Sindre Sorhus (https://sindresorhus.com)","https://github.com/sindresorhus/strip-json-comments" "style-search@0.1.0","ISC","Copyright (c) 2016, David Clark","https://github.com/davidtheclark/style-search" -"stylelint-config-prettier@9.0.3","MIT","Copyright (c) Shannon Moeller (shannonmoeller.com). Copyright (c) Hugo Dias (https://hugodias.me). Copyright (c) 2017 Simon Lydell","https://github.com/prettier/stylelint-config-prettier" +"stylelint-config-prettier@9.0.4","MIT","Copyright (c) Shannon Moeller (shannonmoeller.com). Copyright (c) Hugo Dias (https://hugodias.me). Copyright (c) 2017 Simon Lydell","https://github.com/prettier/stylelint-config-prettier" "stylelint-config-recess-order@3.0.0","ISC","Copyright Jeff Nelson","https://github.com/stormwarning/stylelint-config-recess-order" -"stylelint-config-recommended-scss@7.0.0","MIT","Copyright (c) 2016 Krister Kari","https://github.com/stylelint-scss/stylelint-config-recommended-scss" -"stylelint-config-recommended@8.0.0","MIT","Copyright (c) 2018 - present stylelint","https://github.com/stylelint/stylelint-config-recommended" +"stylelint-config-recommended-scss@8.0.0","MIT","Copyright (c) 2016 Krister Kari","https://github.com/stylelint-scss/stylelint-config-recommended-scss" "stylelint-config-recommended@9.0.0","MIT","Copyright (c) 2018 - present stylelint","https://github.com/stylelint/stylelint-config-recommended" -"stylelint-config-standard@28.0.0","MIT","Copyright (c) 2015 - present stylelint authors","https://github.com/stylelint/stylelint-config-standard" +"stylelint-config-standard@29.0.0","MIT","Copyright (c) 2015 - present stylelint authors","https://github.com/stylelint/stylelint-config-standard" "stylelint-order@5.0.0","MIT","Copyright 2016–present Aleks Hudochenkov ","https://github.com/hudochenkov/stylelint-order" "stylelint-prettier@2.0.0","MIT","Copyright © 2018 Ben Scott","https://github.com/prettier/stylelint-prettier" "stylelint-scss@4.3.0","MIT","Copyright (c) 2016 Krister Kari","https://github.com/stylelint-scss/stylelint-scss" -"stylelint@14.13.0","MIT","Copyright (c) 2015 - present Maxime Thirouin, David Clark & Richard Hallows","https://github.com/stylelint/stylelint" +"stylelint@14.16.0","MIT","Copyright (c) 2015 - present Maxime Thirouin, David Clark & Richard Hallows","https://github.com/stylelint/stylelint" "stylus-loader@7.0.0","MIT","Copyright JS Foundation and other contributors","https://github.com/webpack-contrib/stylus-loader" "stylus@0.59.0","MIT","Copyright (c) Automattic ","https://github.com/stylus/stylus" "supports-color@2.0.0","MIT","Copyright (c) Sindre Sorhus (sindresorhus.com)","https://github.com/chalk/supports-color" @@ -1645,12 +1659,12 @@ "supports-hyperlinks@2.3.0","MIT","Copyright (c) James Talmage (github.com/jamestalmage)","https://github.com/jamestalmage/supports-hyperlinks" "supports-preserve-symlinks-flag@1.0.0","MIT","Copyright (c) 2022 Inspect JS","https://github.com/inspect-js/node-supports-preserve-symlinks-flag" "svg-tags@1.0.0","MIT","Copyright (c) 2014 Athan Reines.","https://github.com/element-io/svg-tags" -"swiper@8.4.2","MIT","Copyright (c) 2019 Vladimir Kharlampidi","https://github.com/nolimits4web/Swiper" +"swiper@8.4.5","MIT","Copyright (c) 2019 Vladimir Kharlampidi","https://github.com/nolimits4web/Swiper" "symbol-observable@1.0.1","MIT","Copyright (c) Sindre Sorhus (sindresorhus.com). Copyright (c) Ben Lesh ","https://github.com/blesh/symbol-observable" "symbol-observable@4.0.0","MIT","Copyright (c) 2021 Sindre Sorhus (sindresorhus.com). Copyright (c) 2021 Ben Lesh ","https://github.com/blesh/symbol-observable" "symbol-tree@3.2.4","MIT","Copyright (c) 2015 Joris van der Wel","https://github.com/jsdom/js-symbol-tree" "systeminformation@5.12.6","MIT","Copyright (c) 2014-2021 Sebastian Hildebrandt","https://github.com/sebhildebrandt/systeminformation" -"table@6.8.0","BSD-3-Clause","Copyright (c) 2018, Gajus Kuizinas (http://gajus.com/). All rights reserved.","https://github.com/gajus/table" +"table@6.8.1","BSD-3-Clause","Copyright (c) 2018, Gajus Kuizinas (http://gajus.com/). All rights reserved.","https://github.com/gajus/table" "tapable@2.2.1","MIT","Copyright JS Foundation and other contributors","https://github.com/webpack/tapable" "tar@6.1.11","ISC","Copyright (c) Isaac Z. Schlueter and Contributors","https://github.com/npm/node-tar" "tdigest@0.1.2","MIT","Copyright (c) 2015 Will Welch","https://github.com/welch/tdigest" @@ -1697,6 +1711,7 @@ "tslib@1.14.1","0BSD","Copyright (c) Microsoft Corporation.","https://github.com/Microsoft/tslib" "tslib@1.9.3","Apache-2.0","","https://github.com/Microsoft/tslib" "tslib@2.4.0","0BSD","Copyright (c) Microsoft Corporation.","https://github.com/Microsoft/tslib" +"tslib@2.4.1","0BSD","Copyright (c) Microsoft Corporation.","https://github.com/Microsoft/tslib" "tsutils-etc@1.4.1","MIT","Copyright (c) 2019 Nicholas Jamieson","https://github.com/cartant/tsutils-etc" "tsutils@3.21.0","MIT","Copyright (c) 2017 Klaus Meinhardt","https://github.com/ajafff/tsutils" "tv4@1.3.0","Public Domain,MIT","","https://github.com/geraintluff/tv4" @@ -1750,7 +1765,7 @@ "vary@1.1.2","MIT","Copyright (c) 2014-2017 Douglas Christopher Wilson","https://github.com/jshttp/vary" "vizion@2.2.1","Apache-2.0","Copyright 2016 Keymetrics, Inc.","https://github.com/keymetrics/vizion" "vm2@3.9.11","MIT","Copyright (c) 2014-2022 Patrik Simek and contributors","https://github.com/patriksimek/vm2" -"vscode-languageserver-textdocument@1.0.7","MIT","Copyright (c) Microsoft Corporation","https://github.com/Microsoft/vscode-languageserver-node" +"vscode-languageserver-textdocument@1.0.8","MIT","Copyright (c) Microsoft Corporation","https://github.com/Microsoft/vscode-languageserver-node" "vscode-uri@3.0.6","MIT","Copyright (c) Microsoft","https://github.com/microsoft/vscode-uri" "w3c-hr-time@1.0.2","MIT","Copyright (c) 2017 Tiancheng "Timothy" Gu and other contributors","https://github.com/jsdom/w3c-hr-time" "w3c-xmlserializer@2.0.0","MIT","Copyright © 2016 Sebastian Mayr","https://github.com/jsdom/w3c-xmlserializer" @@ -1781,6 +1796,8 @@ "whatwg-url@5.0.0","MIT","Copyright (c) 2015–2016 Sebastian Mayr","https://github.com/jsdom/whatwg-url" "whatwg-url@8.7.0","MIT","Copyright (c) 2015–2016 Sebastian Mayr","https://github.com/jsdom/whatwg-url" "which-boxed-primitive@1.0.2","MIT","Copyright (c) 2019 Jordan Harband","https://github.com/inspect-js/which-boxed-primitive" +"which-collection@1.0.1","MIT","Copyright (c) 2019 Inspect JS","https://github.com/inspect-js/which-collection" +"which-typed-array@1.1.9","MIT","Copyright (c) 2015 Jordan Harband","https://github.com/inspect-js/which-typed-array" "which@1.3.1","ISC","Copyright (c) Isaac Z. Schlueter and Contributors","https://github.com/isaacs/node-which" "which@2.0.2","ISC","Copyright (c) Isaac Z. Schlueter and Contributors","https://github.com/isaacs/node-which" "wide-align@1.1.5","ISC","Copyright (c) 2015, Rebecca Turner ","https://github.com/iarna/wide-align" diff --git a/CHANGELOG.md b/CHANGELOG.md index 619e4d630d..478ff62461 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,59 @@ kb_everyone # Changelog +## [3.2.0](https://github.com/intershop/intershop-pwa/releases/tag/3.2.0) (2022-12-22) + +**required Intershop Commerce Management version: 7.10.38.11-LTS** + +**required/tested Node.js version: 16.16.0 LTS (including npm 8.11.0)** + +### Features + +- use ICM '/token' REST endpoint for authentication (#1156) ([4e02efd](https://github.com/intershop/intershop-pwa/commit/4e02efd)) +- introduce additional components for product variation select display (#1317) ([ddd6e2e](https://github.com/intershop/intershop-pwa/commit/ddd6e2e)) +- introduce variation attribute mapping with extended /variations call (#1317) ([d9e4b17](https://github.com/intershop/intershop-pwa/commit/d9e4b17)) +- provide and combine metrics of theme instances (#1340) ([88b4206](https://github.com/intershop/intershop-pwa/commit/88b4206)) +- route to product detail page if only one search result was found (#1316) ([9964ebd](https://github.com/intershop/intershop-pwa/commit/9964ebd)) +- save desired delivery date at basket items (#1325) ([f8fba29](https://github.com/intershop/intershop-pwa/commit/f8fba29)) +- add zebra striped table style for attributes in product details tab (#1326) ([7999f02](https://github.com/intershop/intershop-pwa/commit/7999f02)) +- customer address update in the My Account section (#1315) ([a98d391](https://github.com/intershop/intershop-pwa/commit/a98d391)) + +### Bug Fixes + +- apply ordered-import rule on multiline import statements (#1344) ([5887c41](https://github.com/intershop/intershop-pwa/commit/5887c41)) +- prevent outdated basket information on /basket route (#1342) ([1a730f8](https://github.com/intershop/intershop-pwa/commit/1a730f8)) +- remove check for variations or variation master data for variation product data handling ([41e674f](https://github.com/intershop/intershop-pwa/commit/41e674f)) +- error when submitting consecutive search after submitting search from search suggestion (#1332) ([9d063a9](https://github.com/intershop/intershop-pwa/commit/9d063a9)) +- add SSR hybrid backend configuration option for kubernetes deployments (#1320) ([3e293fa](https://github.com/intershop/intershop-pwa/commit/3e293fa)) +- prevent budget bar arrow and product label ribbons to be shown in the subcategories layer (#1329) ([7f4d34c](https://github.com/intershop/intershop-pwa/commit/7f4d34c)) +- replace the product label image sprite, use CSS only styling and localized HTML text (#1319) ([7f63863](https://github.com/intershop/intershop-pwa/commit/7f63863)) +- replace the empty cart image with pure CSS and remove the image file (#1319) ([eebebec](https://github.com/intershop/intershop-pwa/commit/eebebec)) +- remove active_catalog.png and budget-bar-indicator.png and replace with CSS styling (#1319) ([9aeafaf](https://github.com/intershop/intershop-pwa/commit/9aeafaf)) +- display company input fields for anonymous b2b users on checkout edit address form (#1312) ([23a9257](https://github.com/intershop/intershop-pwa/commit/23a9257)) +- change and display an anonymous user`s email during the checkout process (#1312) ([b2c265b](https://github.com/intershop/intershop-pwa/commit/b2c265b)) +- validate email during the checkout process of an anonymous user (#1312) ([5ba41ec](https://github.com/intershop/intershop-pwa/commit/5ba41ec)) +- apiToken cookie should be shared between same host (#1321) ([80d5f9d](https://github.com/intershop/intershop-pwa/commit/80d5f9d)) +- add promotion scope to the validation of the first basket checkout step (#1322) ([8da52d6](https://github.com/intershop/intershop-pwa/commit/8da52d6)) +- keep the footer at the bottom of the page (#1318) ([feb1648](https://github.com/intershop/intershop-pwa/commit/feb1648)) +- 'npm install' not working with PWA release zip file (#1314) ([33be4cc](https://github.com/intershop/intershop-pwa/commit/33be4cc)) +- display login modal after authentication token has expired (#1280, #1311) ([025818c](https://github.com/intershop/intershop-pwa/commit/025818c)) +- add matrix parameter spgid to promotion REST calls (#1310) ([1cae0d8](https://github.com/intershop/intershop-pwa/commit/1cae0d8)) + +### Documentation + +- improve authentication documentation (#1156) ([cf89956](https://github.com/intershop/intershop-pwa/commit/cf89956)) +- extend PurgeCSS integration documentation (#1313) ([d4db9d6](https://github.com/intershop/intershop-pwa/commit/d4db9d6)) + +### BREAKING CHANGES + +- PWA uses the ICM `/token` REST endpoint to retrieve user token, every anonymous user will get a anonymous user token, every identity provider has to configure the `oAuthService` with information about the token endpoint, before expiration the given token should be refreshed. +- Changed the rendering of the `ProductVariationSelectComponent` and introduced additional product variation select rendering components (see [Migrations / 3.1 to 3.2](https://github.com/intershop/intershop-pwa/blob/develop/docs/guides/migrations.md#31-to-32) for more details). +- `ProductsService` was changed to use an `extended=true` details and variations call. `VariationAttribute` model was cleaned up and extended (see [Migrations / 3.1 to 3.2](https://github.com/intershop/intershop-pwa/blob/develop/docs/guides/migrations.md#31-to-32) for more details). +- The product label ribbon image is replaced with localized text and CSS styling. +- The empty cart image is removed and replaced with localized text and CSS styling. +- The `active_catalog.png` and `budget-bar-indicator.png` are removed and replaced with CSS styling. +- Formly has been upgraded from version 5 to 6 (see [Migrations / 3.1 to 3.2](https://github.com/intershop/intershop-pwa/blob/develop/docs/guides/migrations.md#31-to-32) for more details). + ## [3.1.0](https://github.com/intershop/intershop-pwa/releases/tag/3.1.0) (2022-10-17) **required Intershop Commerce Management version: 7.10.38.11-LTS** diff --git a/docs/guides/migrations.md b/docs/guides/migrations.md index 94111ab110..5d9cb0f742 100644 --- a/docs/guides/migrations.md +++ b/docs/guides/migrations.md @@ -14,7 +14,7 @@ Together with that an inline style of the `main-container` was moved to the glob Formly has been upgraded from version 5 to 6. Find more information in the [Formly Upgrade Guide](https://github.com/ngx-formly/ngx-formly/blob/main/UPGRADE-6.0.md). -We still use deprecated form properties like 'templateOptions' and 'expressionProperties' for compatibility reasons but we are going to replace them in the next major release. +We still use deprecated form properties like `templateOptions` and `expressionProperties` for compatibility reasons but we are going to replace them in the next major release. The two small black triangle images `active_catalog.png` (header: when hovering a catalog) and `budget-bar-indicator.png` (my account: budget bar) are removed and replaced by CSS styling. The image for an empty basket `empty-cart.png` is removed and replaced with CSS styling. @@ -22,7 +22,7 @@ The sprite image `product_sprite.png` is removed and replaced with localized tex After entering a desired delivery date on the checkout shipping page and after submitting the order, the desired delivery date will be saved at all basket items if necessary. In case of large baskets (> 20 items) this might cause long response times. -You can keep the existing behavior by modifying the _updateBasketItemsDesiredDeliveryDate()_ method of the basket service to always return an empty array without doing anything. +You can keep the existing behavior by modifying the `updateBasketItemsDesiredDeliveryDate()` method of the basket service to always return an empty array without doing anything. The `ProductsService` was changed to use `extended=true` REST calls for product details and variations to fetch variation attributes with additional `attributeType` and `metaData` information that can be used to control the rendering of different variation select types. The added `VariationAttributeMapper` maps the additional information in a backwards compatible way. diff --git a/package-lock.json b/package-lock.json index e805c597cc..efb427e1b1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "intershop-pwa", - "version": "3.1.0", + "version": "3.2.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "intershop-pwa", - "version": "3.1.0", + "version": "3.2.0", "hasInstallScript": true, "license": "Intershop Standard Software End User License Agreement Intershop 7", "dependencies": { diff --git a/package.json b/package.json index 6bf10f639a..5b8219d733 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "intershop-pwa", "description": "Intershop Progressive Web App", - "version": "3.1.0", + "version": "3.2.0", "license": "Intershop Standard Software End User License Agreement Intershop 7", "keywords": [ "intershop" @@ -255,4 +255,4 @@ }, "active-themes": "b2b,b2c" } -} \ No newline at end of file +}