From 4934b66ecd5b0bc3216e1316c52fb2a0ddd72a5e Mon Sep 17 00:00:00 2001 From: Danilo Hoffmann Date: Wed, 22 Apr 2020 21:13:38 +0200 Subject: [PATCH] docs: import and update documentation from PWA Guide (#125) --- .github/workflows/test.yml | 30 + .prettierignore | 6 +- .vscode/extensions.json | 5 +- README.md | 90 +-- docs/README.md | 62 ++- docs/check-dead-links.js | 109 ++++ docs/check-documentation-overview.js | 39 ++ docs/check-kb-labels.js | 58 ++ docs/check-sentence-newline.js | 26 + docs/concepts/cms-integration.md | 99 ++++ docs/concepts/cms-integration.png | Bin 0 -> 14236 bytes docs/concepts/configuration.md | 210 +++++++ ...-angular-browsersideapp-build-activity.jpg | Bin 0 -> 20230 bytes ...oyment-angular-browsersideapp-sequence.jpg | Bin 0 -> 32520 bytes ...t-angular-serversideapp-build-activity.jpg | Bin 0 -> 41415 bytes ...t-angular-serversiderendering-sequence.jpg | Bin 0 -> 31344 bytes docs/concepts/deployment-scenarios.md | 38 ++ .../concepts/hybrid-approach-architecture.svg | 411 ++++++++++++++ docs/concepts/hybrid-approach.md | 82 +++ docs/concepts/localization.md | 274 ++++++++++ docs/concepts/logging.md | 31 ++ docs/concepts/project-structure.md | 118 ++++ docs/concepts/search-engine-optimization.md | 42 ++ docs/concepts/software-architecture.md | 32 ++ docs/concepts/state-management.md | 194 +++++++ docs/{ => concepts}/state-management.svg | 0 docs/concepts/styling-behavior.md | 39 ++ docs/concepts/testing-test-pyramid.jpg | Bin 0 -> 23759 bytes docs/concepts/testing.md | 110 ++++ docs/concepts/url-rewriting.md | 39 ++ docs/customizations.md | 111 ---- docs/guides/angular-change-detection.md | 95 ++++ docs/guides/angular-component-development.md | 128 +++++ docs/guides/code-documentation.md | 151 +++++ docs/guides/continuous-integration.md | 78 +++ docs/guides/customizations.md | 146 +++++ docs/guides/data-handling-with-mappers.md | 62 +++ docs/guides/development.md | 121 ++++ docs/guides/forms-checkbox.png | Bin 0 -> 3022 bytes docs/guides/forms-formfeedback.png | Bin 0 -> 5944 bytes docs/guides/forms-input.png | Bin 0 -> 3031 bytes docs/guides/forms-select.png | Bin 0 -> 3256 bytes docs/guides/forms-textarea.png | Bin 0 -> 4610 bytes docs/guides/forms.md | 247 +++++++++ docs/guides/getting-started.md | 114 ++++ docs/guides/google-tag-manager.md | 20 + .../hybrid-approach-icm-url-rewriting.md | 81 +++ docs/{ => guides}/migrations.md | 17 +- docs/guides/mocking-rest-calls.md | 26 + docs/guides/multiple-themes.md | 43 ++ .../propagating-environment-variables.md | 55 ++ docs/guides/sentry-error-monitoring.md | 21 + docs/guides/ssr-startup.md | 49 ++ docs/guides/state-management.md | 38 ++ docs/guides/testing-cypress.md | 55 ++ docs/guides/testing-jest.md | 515 ++++++++++++++++++ docs/project-structure.md | 218 -------- docs/search-engine-optimization.md | 34 -- docs/state-management.md | 194 ------- package.json | 12 +- .../services/products/products.service.ts | 1 + 61 files changed, 4113 insertions(+), 663 deletions(-) create mode 100644 docs/check-dead-links.js create mode 100644 docs/check-documentation-overview.js create mode 100644 docs/check-kb-labels.js create mode 100644 docs/check-sentence-newline.js create mode 100644 docs/concepts/cms-integration.md create mode 100644 docs/concepts/cms-integration.png create mode 100644 docs/concepts/configuration.md create mode 100644 docs/concepts/deployment-angular-browsersideapp-build-activity.jpg create mode 100644 docs/concepts/deployment-angular-browsersideapp-sequence.jpg create mode 100644 docs/concepts/deployment-angular-serversideapp-build-activity.jpg create mode 100644 docs/concepts/deployment-angular-serversiderendering-sequence.jpg create mode 100644 docs/concepts/deployment-scenarios.md create mode 100644 docs/concepts/hybrid-approach-architecture.svg create mode 100644 docs/concepts/hybrid-approach.md create mode 100644 docs/concepts/localization.md create mode 100644 docs/concepts/logging.md create mode 100644 docs/concepts/project-structure.md create mode 100644 docs/concepts/search-engine-optimization.md create mode 100644 docs/concepts/software-architecture.md create mode 100644 docs/concepts/state-management.md rename docs/{ => concepts}/state-management.svg (100%) create mode 100644 docs/concepts/styling-behavior.md create mode 100644 docs/concepts/testing-test-pyramid.jpg create mode 100644 docs/concepts/testing.md create mode 100644 docs/concepts/url-rewriting.md delete mode 100644 docs/customizations.md create mode 100644 docs/guides/angular-change-detection.md create mode 100644 docs/guides/angular-component-development.md create mode 100644 docs/guides/code-documentation.md create mode 100644 docs/guides/continuous-integration.md create mode 100644 docs/guides/customizations.md create mode 100644 docs/guides/data-handling-with-mappers.md create mode 100644 docs/guides/development.md create mode 100644 docs/guides/forms-checkbox.png create mode 100644 docs/guides/forms-formfeedback.png create mode 100644 docs/guides/forms-input.png create mode 100644 docs/guides/forms-select.png create mode 100644 docs/guides/forms-textarea.png create mode 100644 docs/guides/forms.md create mode 100644 docs/guides/getting-started.md create mode 100644 docs/guides/google-tag-manager.md create mode 100644 docs/guides/hybrid-approach-icm-url-rewriting.md rename docs/{ => guides}/migrations.md (84%) create mode 100644 docs/guides/mocking-rest-calls.md create mode 100644 docs/guides/multiple-themes.md create mode 100644 docs/guides/propagating-environment-variables.md create mode 100644 docs/guides/sentry-error-monitoring.md create mode 100644 docs/guides/ssr-startup.md create mode 100644 docs/guides/state-management.md create mode 100644 docs/guides/testing-cypress.md create mode 100644 docs/guides/testing-jest.md delete mode 100644 docs/project-structure.md delete mode 100644 docs/search-engine-optimization.md delete mode 100644 docs/state-management.md diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d346649c04..28c8a7821c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -116,6 +116,36 @@ jobs: name: dist path: dist + Docs: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v1 + - name: Use Node.js 12 + uses: actions/setup-node@v1 + with: + node-version: 12 + + - name: Install root dependencies + uses: bahmutov/npm-install@v1 + + - name: Check KB Labels + run: node docs/check-kb-labels + + - name: Check Documentation Overview + run: node docs/check-documentation-overview + + - name: Check Newline After Every Sentence + run: node docs/check-sentence-newline + + - name: Check Formatting + run: | + npx prettier --write docs/**/*.* + bash ./scripts/ci-test-no-changes.sh 'you probably committed unformatted documentation' + + - name: Check Dead Links + run: node docs/check-dead-links + Universal: needs: [Build] runs-on: ubuntu-latest diff --git a/.prettierignore b/.prettierignore index e07d231f3e..40a31fa9f7 100644 --- a/.prettierignore +++ b/.prettierignore @@ -15,6 +15,9 @@ *.hbs *.mp4 *.js.map +*.properties +*.crt +*.key .*ignore .gitattributes .editorconfig @@ -27,7 +30,8 @@ browserslist /builds /cache /dist -/docs +/docs/**/*.ditamap +/docs/theme /out-tsc /coverage /junit.xml diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 6f67400834..4bc7453a6c 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -21,7 +21,10 @@ "hex-ci.stylelint-plus", "ms-vscode.vscode-typescript-tslint-plugin", // testing - "andys8.jest-snippets" + "andys8.jest-snippets", + // documentation + "streetsidesoftware.code-spell-checker", + "yzhang.markdown-all-in-one" ], // List of extensions recommended by VS Code that should not be recommended for users of this workspace. "unwantedRecommendations": ["eg2.tslint"] diff --git a/README.md b/README.md index 4ba8a23d77..faffdd11ea 100644 --- a/README.md +++ b/README.md @@ -12,97 +12,11 @@ More information on the PWA can be found [here](https://www.intershop.com/en/pro In order to contribute, please have a look at our [Contribution Guidelines](./CONTRIBUTING.md) -## Getting Started - ---- - -Before working with this project, download and install [Node.js](https://nodejs.org) with the included npm package manager. Currently Node.js 12.x LTS with the corresponding npm is required. - -This project was generated with [Angular CLI](https://github.com/angular/angular-cli) and follows the Angular CLI style guide and naming coventions. - --- -After having cloned the project from the Git repository, open a command line in the project folder and run `npm install`. - -The project uses Angular CLI which has to be installed globally. Run `npm install -g @angular/cli` once to globally install Angular CLI on your development machine. Use `ng serve --open` to start up the development server and open the progressive web app in your browser. - -The project can alternatively be run in production mode with `npm start`. - -## Customization - -Before customizing the PWA for your specific needs, have a look at our [Customization Guide](./docs/customizations.md) and also have a look at the current [PWA Guide](https://support.intershop.de/kb/index.php?c=Search&qoff=0&qtext=guide+progressive+web+app) first. - -## Development Server - -Run `ng serve` or `ng s` for a development server that is configured by default via `environment.ts` to use mocked responses instead of actual REST calls. - -Running `ng serve --configuration production` or `ng s -c production` starts a server that will communicate by default with the Intershop Commerce Management of our public demo via REST API (see the used `environment.prod.ts` for the configuration). - -The project is also configured to support the usage of an own local environment file `environment.local.ts` that can be configured according to the development environment, e.g. with a different icmBaseURL or different configuration options (see the `environment.model.ts`). This file will be ignored by Git so the developer-specific setting will not be commited. To use this local environment configuration, the server should be started with `ng s -c local`. - -Once the server is running, navigate to `http://localhost:4200/` in your browser to see the application. The app will automatically reload if you change any of the source files. - -Running `ng serve --port 4300` will start the server on a different port than the default 4200 port, e.g., if one wants to run multiple instances in paralell for comparison. - -Running `ng serve --open` will automatically open a new browser tab with the started application. The different start options can be combined. - -> DO NOT USE webpack-dev-server IN PRODUCTION! - -## Deployment - -Deployments are generated to the `dist` folder of the project. - -Use `npm run build` to generate the preferred angular universal enabled version. On the server the `dist/server.js` script has to be executed with `node`. - -Alternatively, you can use `ng build --prod` to get an application using browser rendering. All the files under `dist/browser` have to be served statically. The server has to be configured for fallback routing, -see [Server Configuration in Angular Docs](https://angular.io/guide/deployment#server-configuration). - -For a production setup we recommend building the docker image supplied with the `Dockerfile` in the root folder of the project. Build it with `docker build -t my_pwa .`. To run the PWA with multiple channels and [Google PageSpeed](https://developers.google.com/speed/pagespeed/insights/) optimizations, you can use the nginx docker image supplied in the sub folder [nginx](./nginx). - -We provide templates for [Kubernetes Deployments](./schematics/src/kubernetes-deployment) and [DevOps](./schematics/src/azure-pipeline) for Microsoft Azure. - -## Progressive Web App (PWA) - -To run the project as a Progressive Web App with an enabled [Service Worker](https://angular.io/guide/service-worker-getting-started), use `npm run start` to build and serve the application. After that open `http://localhost:4200` in your browser and test it or run a PWA Audit. Currently only `localhost` or `127.0.0.1` will work with the service worker since it requires `https` communication on any other domain. - -## Running Unit Tests - -Run `npm test` to start an on the fly test running environment to execute the unit tests via [Jest](https://facebook.github.io/jest/) once. To run Jest in watch mode with interactive interface, run `npm run test:watch`. - -## Running End-to-End Tests - -Run `npm run e2e` to execute the end-to-end tests via [cypress](https://www.cypress.io/). -You have to start your development or production server first as cypress will instruct you. - -## Code Style - -Use `npm run lint` to run a static code analysis. - -For development make sure the used IDE or Editor follows the [EditorConfig](http://editorconfig.org/) configuration of the project and uses [Prettier](https://prettier.io/) to help maintain consistent coding styles (see `.editorconfig` and `.prettierrc.json`). - -Use `npm run format` to perform a formatting run on the code base with Prettier. - -## Pre-Commit Check - -`npm run check` is a combination task of `lint`, `format` and `test` that runs some of the checks that will also be performed in Continuous Integration on the whole code base. Do not overuse it as the run might take a long time. - -Prefer using `npx lint-staged` to perform a manual quick evaluation of staged files. This also happens automatically when committing files. It is also possible to bypass verification on commit, following the suggestions of the versioning control tool of your choice. - -## Documentation - -Project documentation can be found in the [Documentation Folder](./docs). - -The project is also configured to use [Compodoc](https://compodoc.github.io/website) as API documentation tool. The output folder for the documentation is set to `\docs\compodoc`. To generate the code documentation, run `npm run docs`. To generate and serve the documentation at http://localhost:8080, run `npm run docs:serve`. To serve the documentation while watching for source changes, run `npm run docs:watch`. - -## Code Scaffolding - -With the integrated `intershop-schematics` this project provides the functionality to generate different code artifacts according to our style guide and project structure. `ng generate` will use our custom schematics by default, e.g. run `ng generate component component-name` in the shared folder to generate a new shared component. `ng generate --help` gives an overview of available Intershop-specific schematics. - -The Angular CLI default schematics are still available and working. However, they need to be prefixed to use them, e.g. `ng generate @schematics/angular:guard`. A list of the available Angular CLI schematics can be fetched with `ng generate @schematics/angular: --help`. - -## Further Help +## Getting Started -To get more help on the Angular CLI, use `ng help` or check out the [Angular CLI Documentation](https://github.com/angular/angular-cli/wiki). +Head over to our documentation section for a [Quick Start Guide](./docs/guides/getting-started.md). ## License diff --git a/docs/README.md b/docs/README.md index cefe0c3813..65f0ace8af 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,11 +1,61 @@ -# Documentation + -## [Project Structure](./project-structure.md) +# Documentation Overview -## [State Management](./state-management.md) +## Developers -## [Customizations](./customizations.md) +- [Getting Started](./guides/getting-started.md) -## [Migrations](./migrations.md) +### Architecture -## [Search Engine Optimization](./search-engine-optimization.md) \ No newline at end of file +- [Concept - Software Architecture](./concepts/software-architecture.md) +- [Concept - Project Structure](./concepts/project-structure.md) +- [Concept - State Management](./concepts/state-management.md) + - [Guide - State Management](./guides/state-management.md) +- [Concept - CMS Integration](./concepts/cms-integration.md) +- [Concept - Configuration](./concepts/configuration.md) + - [Guide - Propagating Environment Variables](./guides/propagating-environment-variables.md) +- [Concept - Localization](./concepts/localization.md) +- [Concept - SEO](./concepts/search-engine-optimization.md) +- [Guide - Forms](./guides/forms.md) + +### Developing + +- [Guide - Development Environment](./guides/development.md) +- [Concept - Styling](./concepts/styling-behavior.md) +- [Concept - Testing](./concepts/testing.md) + - [Guide - Testing with Jest](./guides/testing-jest.md) + - [Guide - Testing with Cypress](./guides/testing-cypress.md) +- [Guide - Code Documentation](./guides/code-documentation.md) +- [Guide - Angular Component Development](./guides/angular-component-development.md) +- [Guide - Angular Change Detection](./guides/angular-change-detection.md) +- [Guide - Data Handling with Mappers](./guides/data-handling-with-mappers.md) + +### Customization + +- [Guide - Customizations](./guides/customizations.md) + - [Guide - Multiple Themes](./guides/multiple-themes.md) +- [Guide - Migration](./guides/migrations.md) +- [Concept - URL Rewriting](./concepts/url-rewriting.md) +- [Guide - Mocking REST API Calls](./guides/mocking-rest-calls.md) + +## Operations + +### Setup + +- [Concept - Deployment Scenarios](./concepts/deployment-scenarios.md) + - [Guide - SSR Parameters](./guides/ssr-startup.md) +- [Concept - Hybrid Approach](./concepts/hybrid-approach.md) + - [Guide - Hybrid Approach and ICM URL Rewriting](./guides/hybrid-approach-icm-url-rewriting.md) +- [Guide - CI](./guides/continuous-integration.md) +- [Guide - Google Tag Manager](./guides/google-tag-manager.md) + +### Monitoring + +- [Concept - Logging](./concepts/logging.md) +- [Guide - Client-Side Error Monitoring with Sentry](./guides/sentry-error-monitoring.md) diff --git a/docs/check-dead-links.js b/docs/check-dead-links.js new file mode 100644 index 0000000000..394b1f34e8 --- /dev/null +++ b/docs/check-dead-links.js @@ -0,0 +1,109 @@ +const fs = require('fs'); +const path = require('path'); +const async = require('async'); +const { promisify } = require('util'); +const glob = promisify(require('glob')); + +async function checkExternalLinkError(link) { + console.log('check', link); + const client = link.startsWith('https') ? require('https') : require('http'); + return new Promise(resolve => { + const req = client.request(link, res => { + isError = res.statusCode >= 400; + if (isError) { + console.warn('found dead link to', link); + } + resolve(isError); + }); + + req.on('error', () => { + console.warn('found dead link to', link); + resolve(true); + }); + req.end(); + }); +} + +function sleep(milliseconds) { + return new Promise(resolve => setTimeout(resolve, milliseconds)); +} + +function getLineInfoOfString(data, str) { + var perLine = data.split('\n'); + for (let line = 0; line < perLine.length; line++) { + const index = perLine[line].indexOf(str); + if (index > 0) { + return ':' + (line + 1) + ':' + (index + 1); + } + } + return ''; +} + +glob('**/*.md') + .then(files => { + const externalLinks = []; + let isError = false; + + files + .filter(file => !file.includes('node_modules')) + .forEach(file => { + const content = fs.readFileSync(file, { encoding: 'utf-8' }); + const match = content.match(/\[.*?\](\(|:\s+)[^\s]*\)?/g); + if (match) { + match.forEach(link => { + const linkTo = /\](\(?\)|$|#)/.exec(link)[2]; + + if (linkTo) { + // link is not document-internal + if (linkTo.startsWith('http')) { + externalLinks.push(linkTo); + } else { + const normalized = path.normalize(path.join(path.dirname(file), linkTo)); + if (!fs.existsSync(normalized)) { + console.warn(file + getLineInfoOfString(content, linkTo) + ': found dead link to "' + linkTo + '"'); + isError = true; + } + } + } + }); + } + }); + if (isError) { + throw new Error('found dead internal links'); + } + return externalLinks; + }) + .then(externalLinks => { + if (process.argv.length > 2 && process.argv[2] === 'fast') { + console.warn('skipping external link check'); + return; + } + + const filtered = externalLinks + .map(link => link.replace(/\/$/, '')) + .filter((val, idx, arr) => arr.indexOf(val) === idx) + .filter( + link => + !link.includes('github.com') && + !link.includes('repository.intershop.de') && + !link.includes('support.intershop.com') && + !link.includes('github.com/intershop/intershop-pwa/commit') + ) + .sort(); + + async.eachSeries( + filtered, + async link => { + const isError = await checkExternalLinkError(link); + if (isError) throw new Error('found dead external link to', link); + await sleep(1000); + }, + err => { + if (err) throw err; + } + ); + }) + .catch(err => { + console.error(err.message); + process.exit(1); + }); diff --git a/docs/check-documentation-overview.js b/docs/check-documentation-overview.js new file mode 100644 index 0000000000..4b78f1e40a --- /dev/null +++ b/docs/check-documentation-overview.js @@ -0,0 +1,39 @@ +const fs = require('fs'); +const path = require('path'); +const glob = require('glob'); + +const overviewContent = fs.readFileSync('docs/README.md', { encoding: 'utf-8' }); +const match = overviewContent.match(/\[.*?\](\(|:\s+)[^\s]*\)?/g); +const links = []; + +if (match) { + match.forEach(link => { + const linkTo = /\](\(?\)|$|#)/.exec(link)[2]; + + if (linkTo) { + // link is not document-internal + if (!linkTo.startsWith('http')) { + const basename = path.normalize(path.join(process.cwd(), 'docs', linkTo)); + links.push(basename); + } + } + }); +} + +const files = glob.sync('docs/*/*.md'); +let isError = false; + +files.forEach(file => { + const fullPath = path.join(process.cwd(), file); + // console.log('######'); + // console.log(files); + // console.log(fullPath); + if (!links.includes(fullPath)) { + console.warn('document not linked in overview docs/README.md: .' + file.substr(4)); + isError = true; + } +}); + +if (isError) { + process.exit(1); +} diff --git a/docs/check-kb-labels.js b/docs/check-kb-labels.js new file mode 100644 index 0000000000..c93c93bd53 --- /dev/null +++ b/docs/check-kb-labels.js @@ -0,0 +1,58 @@ +const fs = require('fs'); +const path = require('path'); + +let files = process.argv.splice(2); + +if (files.length === 0) { + const glob = require('glob'); + files = glob.sync('docs/**/*.md'); +} + +let isError = false; + +function getDefaultLabel(file) { + const parsedPath = path.parse(file); + if (parsedPath.dir.endsWith(path.join('docs', 'concepts'))) { + return ` + +`; + } else if (parsedPath.dir.endsWith(path.join('docs', 'guides'))) { + return ` + +`; + } +} + +files.forEach(file => { + console.log('at', file); + const fileContent = fs.readFileSync(file, { encoding: 'utf-8' }); + if (!fileContent.startsWith(' + +# CMS Integration + +## Introduction + +The Intershop REST API contains resources reflecting the aspects of Intershop's integrated Content Management System (CMS), i.e. +Pagelets, Includes, Pages. + +Calling the `/cms` resource will list the available CMS sub resources for the different CMS artifacts. + +```json +{ + "elements": [ + { + "type": "Link", + "uri": "inSPIRED-inTRONICS-Site/-/cms/viewcontexts", + "title": "viewcontexts" + }, + { + "type": "Link", + "uri": "inSPIRED-inTRONICS-Site/-/cms/includes", + "title": "includes" + }, + { + "type": "Link", + "uri": "inSPIRED-inTRONICS-Site/-/cms/pagelets", + "title": "pagelets" + }, + { + "type": "Link", + "uri": "inSPIRED-inTRONICS-Site/-/cms/pages", + "title": "pages" + }, + { + "type": "Link", + "uri": "inSPIRED-inTRONICS-Site/-/cms/pagetree", + "title": "pagetree" + } + ], + "type": "ResourceCollection" +} +``` + +With this API, a client can retrieve a composition of involved CMS objects (e.g. include, component, slot, component and so on). +It is the client's responsibility to interpret and "render" such a composition tree. +In the PWA this is done by mapping each element onto an Angular specific render component. + +![CMS Integration Overview](cms-integration.png) + +## Angular CMS Components + +A CMS render component in Angular has to fulfill the following requirements: + +- It is declared in the `CMSModule`. +- The component must have an input for the assigned pagelet. +- It is added to the `CMSModule` as an `entryComponent` (required, so a factory is generated as it is not referenced directly). +- A mapping has to be provided in the `CMSModule` to map the `definitionQualifiedName` of the ICM realm to the PWA render component. +- It needs to implement the `CMSComponent` interface. + +```typescript +providers: [ + { + provide: CMS_COMPONENT, + useValue: { + definitionQualifiedName: 'app_sf_customer_cm:component.custom.inventory.pagelet2-Component', + class: CMSInventoryComponent, + }, + multi: true, + }, +]; +``` + +When using `ng generate` with PWA custom schematics, you can apply all those changes described automatically. +For example, the following code block creates a new Angular component named `cms-inventory` and registers it with the `CMSModule`. + +```bash +$ ng generate cms inventory --definitionQualifiedName app_sf_customer_cm:component.custom.inventory.pagelet2-Component +CREATE src/app/cms/components/cms-inventory/cms-inventory.component.ts (386 bytes) +CREATE src/app/cms/components/cms-inventory/cms-inventory.component.html (32 bytes) +CREATE src/app/cms/components/cms-inventory/cms-inventory.component.spec.ts (795 bytes) +UPDATE src/app/cms/cms.module.ts (4956 bytes) +``` + +> **Visual Studio Code Integration** +> For Visual Studio Code there is an extension that offers comfortable usage options for the schematics, see [Angular Schematics](https://marketplace.visualstudio.com/items?itemName=cyrilletuzi.angular-schematics). + +## Integration with an External CMS + +Since the Intershop PWA can integrate any other REST API in addition to the ICM REST API, it should not be a problem to integrate an external 3rd party CMS that provides an own REST API, instead of using the integrated ICM CMS. +Even combinations would be possible. + +In case an external API has to be integrated, the native Angular `httpClient` must to be used for the REST calls instead of the PWAs `apiService`. +In addition, the mapping of content to the according places in the PWA needs to be handled in a way fitting to the external CMS. diff --git a/docs/concepts/cms-integration.png b/docs/concepts/cms-integration.png new file mode 100644 index 0000000000000000000000000000000000000000..ec2e67d9c1321fedb1ad3da4e6780a77873b76e4 GIT binary patch literal 14236 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 kSA5UxKm08=(%Po-fVsz)gz|*Jci + +# Configuration + +In a complex application like the Intershop Progressive Web App, there are multiple ways and kinds of configuration. +The complexity increases if you consider that the communication with Intershop Commerce Management has to be coordinated as well. +In addition, the PWA, when run with Angular Universal, consists of a server-side and a client-side application. + +## Ways of Configuring Angular Applications + +> :warning: If available, always prefer configuration via system environment variables and running the PWA with Universal Rendering. + +### Angular CLI Environments + +The standard way of configuring an Angular Application can be done by managing multiple environment files that are part of the project's source tree, usually located in _src/environments._ To choose one configuration, you have to supply the parameter when building the Angular Application. +The file _angular.json_ defines how the correct environment file is swapped in for the corresponding environment. +See [Angular 2: Application Settings using the CLI Environment Option](http://tattoocoder.com/angular-cli-using-the-environment-option/) for further information. + +Properties supplied with environment files should not be accessed directly in artifacts other than modules. +Instead, you need to provide them via `InjectionToken`s to be used in components, pipes or services. +The `InjectionToken` can be used to access a certain property later on: + +```typescript +export const PROPERTY = new InjectionToken('property'); + +@NgModule({ + providers: [{ provide: PROPERTY, useValue: environment.property }], +}) +export class SomeModule {} +``` + +**Property consumer** + +```typescript +import { Inject } from '@angular/core' +import { PROPERTY } from '../injection-keys' + +... + +constructor(@Inject(PROPERTY) private property: string) +``` + +It is good practice to never write those properties at runtime. + +As can be seen here, only build-time and deploy-time configuration parameter can be supplied this way. + +### Node.js Environment Variables + +When running the application in Angular Universal mode within a _Node.js_ environment, we can additionally access the process environment variables via _process.env._ This method provides a way to configure the application at deploy time, e.g., when using docker images. +Configuration can then be consumed and passed to the client side via means of state transfer. + +### NgRx Configuration State + +Previous ways were mainly handling deployment- or build-time-related means to configure an Angular application. +All further configuration that has some kind of runtime flexibility, especially configuration that is retrieved via REST calls from the ICM, has to be handled in the NgRx store and to be used throughout the application with selectors. +Effects and actions should be used to manipulate those settings. + +### URL Parameters + +A configuration effect (NgRx) for listening to route parameters when initially visiting the page has been composed. +This provides the most flexible way of configuring the application at runtime. + +## Different Levels of Configuration Settings + +### Build Settings + +One example for a build time configuration is the property `serviceWorker`, which is managed in the _environment.ts_ and used to activate the `ServiceWorker` module. +Another example of such a build setting is the property `production` as multiple debug modules are only compiled into the application when running in development mode. + +In general, properties available at build time can only be supplied by Angular CLI environments (see above). + +### Deployment Settings + +Deployment settings do not influence the build process and therefore can be set in more flexible manners. +The main criteria of this category is the fact that deployment settings do not change during runtime. +The most common way of supplying them can be implemented by using Angular CLI environment files and `InjectionToken`s for distribution throughout the application's code. + +An example for this kind of settings are breakpoint settings for the different device classes of the application touchpoints. + +### Runtime Settings + +The most flexible kind of settings, which can also change when the application runs, are runtime settings. +Angular CLI environment files cannot provide a way to handle those. +Only the NgRx store can do that. +Therefore only NgRx means should be used to supply them. +Nevertheless, default values can be provided by environment files and can later be overridden by system environment variables. + +Everything managed in the NgRx state is accumulated on the server side and sent to the client side with the initial HTML response. +The reason for this is that this is the most common deployment scenario of PWAs (see [Concept - Deployment Scenarios](./deployment-scenarios.md)). + +## Multi-Site Handling + +Since version 0.9 of the PWA there are means to dynamically configure ICM channel and application to determine the correct REST endpoint for each incoming top level request. +Nevertheless, you can still configure it in a static way for each PWA deployment via Angluar CLI environments. + +### Setting the Base URL + +At first, the PWA has to be connected with the corresponding ICM. +This can be done by modifying environment files or by setting the environment variable `ICM_BASE_URL` for the process running the _Node.js_ server. +The latter is the preferred way. + +Independent of where and how you deploy the Angular Universal application, be it in a docker container or plain, running on Azure, with or without service orchestrator, setting the base URL provides the most flexible way of configuring the PWA. +Refer to the documentation for mechanisms of your environment on how to set and pass environment variables. + +### Static Setting for Channels + +Use the properties `icmChannel` and `icmApplication` in the Angular CLI environment or the environment variables `ICM_CHANNEL` and `ICM_APPLICATION` to statically direct one deployment to a specific REST endpoint of the ICM. + +### Dynamic Setting of Channels + +To set ICM channels and applications dynamically, you have to use URL rewriting in a reverse proxy running in front of the PWA instances. +The values have to be provided as URL parameters (not to be confused with query parameters). + +**nginx URL rewrite snippet** + +```text +rewrite ^(.*)$ "$1;channel=inSPIRED-inTRONICS_Business-Site;application=-" break; +``` + +The above example configuration snippet shows a [Nginx](https://en.wikipedia.org/wiki/Nginx) rewrite rule on how to map an incoming top level request URL to an internal worker process, e.g., _Node.js_. +It shows both the PWA parameters `channel`, `application` and their fixed example values. +The parameters of each incoming request are then read and transferred to the NgRx store to be used for the composition of the initial HTML response on the server side. +Afterwards they are propagated to the client side and re-used for subsequent REST requests. + +In the source code of the project we provide an extended [Nginx](https://en.wikipedia.org/wiki/Nginx) docker image for easy configuration of multiple channels via sub-domains. +Refer to our Gitlab CI configuration (file _.gitlab-ci.yml)_ for a usage example. + +## Feature Toggles + +To activate additional functionality, we use the concept of feature toggles throughout the application. +For instance, there is no general distinction between B2B and B2C applications. +Each setup can define specific features at any time. +Of course, the ICM server must supply appropriate REST resources to leverage functionality. + +### Configuring Features + +The configuration of features can be done statically by the Angular CLI environment property `features` (string array) or the environment parameter `FEATURES` (comma-separated string list). +To configure it dynamically, use the PWA URL parameter `features` (comma-separated string list) during URL rewriting in the reverse proxy. + +### Programmatically Switching Features + +Various means to activate and deactivate functionality based on feature toggles are supplied. + +**Guard** + +```typescript +const routes: Routes = [ + { + path: 'quote', + loadChildren: ..., + canActivate: [FeatureToggleGuard], + data: { feature: 'quoting' }, + }, +... +``` + +Add the Guard as `CanActivate` to the routing definition. +Additionally, you have to supply a `data` field called `feature`, containing a string that determines for which feature the route should be active. +If the feature is deactivated, the user is sent to the error page on accessing. + +**Directive** + +```html + ... +``` + +**Service** + +```typescript +@Injectable({ providedIn: 'root' }) +export class SomeService { + constructor(private featureToggleService: FeatureToggleService) {} +... + if (this.featureToggleService.enabled('quoting')) { +... +} +``` + +## Setting Default Locale + +You can set the default locale statically by modifying the order of the provided locales in the Angular CLI environment files. +The first locale is always chosen as the default one. +To dynamically set the default locale, use the URL parameter `lang` when rewriting the URL in the reverse proxy (see Dynamic Setting of Channels). + +## Extend Locales + +To add other languages except English, German or French, you have to create a new json-mapping-file with all translations, e.g., _./src/assets/i18n/nl_NL.json_). +Add the locale in the file _./src/environment/environments.ts_. +Additionally, for Angular's built-in components, e.g., currency-pipe, you have to register locale data similar to `localeDe` and `localeFr` with `registerLocaleData(localeNl)` in _./src/app/core/configuration.module.ts._ + +```typescript +... +import localeNl from '@angular/common/locales/nl'; +... +export class ConfigurationModule { + constructor(@Inject(LOCALE_ID) lang: string, translateService: TranslateService) { + registerLocaleData(localeNl); + ... + } +} +``` + +> ### Configuration REST Resource +> +> We are currently planning to implement a Configuration REST resource in the ICM so that all necessary runtime configuration can be defined in the ICM Backoffice and consumed by each PWA deployment. diff --git a/docs/concepts/deployment-angular-browsersideapp-build-activity.jpg b/docs/concepts/deployment-angular-browsersideapp-build-activity.jpg new file mode 100644 index 0000000000000000000000000000000000000000..24b2777a75deec55e084abbfe60f8cf79dde01c2 GIT binary patch literal 20230 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 literal 0 HcmV?d00001 diff --git a/docs/concepts/deployment-angular-browsersideapp-sequence.jpg b/docs/concepts/deployment-angular-browsersideapp-sequence.jpg new file mode 100644 index 0000000000000000000000000000000000000000..3223d0c31987d9c4d9e965db1f9839a198cbef2d GIT binary patch literal 32520 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 E1qz*fZvX%Q literal 0 HcmV?d00001 diff --git a/docs/concepts/deployment-angular-serversideapp-build-activity.jpg b/docs/concepts/deployment-angular-serversideapp-build-activity.jpg new file mode 100644 index 0000000000000000000000000000000000000000..5321dceeefa608054855792542489727d74bf2e0 GIT binary patch literal 41415 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 literal 0 HcmV?d00001 diff --git a/docs/concepts/deployment-angular-serversiderendering-sequence.jpg b/docs/concepts/deployment-angular-serversiderendering-sequence.jpg new file mode 100644 index 0000000000000000000000000000000000000000..50582da68c8a57d4ffc61b53f8df12c19056fe80 GIT binary patch literal 31344 zcmeFZ2UJu|);8KiiIPErD4|J`qacVt6S`?~&XSsp1d*s9Ifn)s8W1EWQOQ9-B}d6f z6v-J85G3Dr-VvBJ@7(px%r|%4|K2HFP@e8{YM(k)yPo~*r_TA<`BwljL=G$mKtlrn z&`>{s^GSdV00aG3`SlwERWPwJe-#{TY%FYC96UT+99&#H0wMxD{OkC*xP&Bx*NKRU zNr>?XNXbZv$xwCTUl&39btVQT4(dc=d|Z6g(SNd>e*lo+pq-)XW1ukt&`HoRNYKt( z05?&lqKw7(_3N+Y8V)uFCKfvGuaB*X0RT))baZ@Noa@*)7+AQd+d@ZuihT`-g!Bd( z!(AO+Yj;FaBrW=c{S6J{HkVZ`R74# z@l7qvGU^~R|6n+fMNk74keOA6YKi>PfZvAvI!bb3*2U2Y009OX>JBhS0Fr>SLciZ1 z{3rh|Z&BEBR8?RpI_JOr{;1nVPuzZsuK)y8sc8|o<8bnxET#qZy7m zWgqf@HBSXutqj6@+g+IY2PC5UXayI%E@m;L)V$Vg$HiCmhwE(MW;f-d_#^M)WCGnS z-TJhQVF|;ZruST0Qy0$C*OmZU4=zso->P71?dBax9<>Rnb~Qepkv|9MwiW%pHtDNN zD^g73$vhllO{<0#@@oRB%2pHVLgJ+5q*_N}3EmSNI}zlzr8y|WV=D@xSmGJ^`T6-2 zTcZ~vIy+BgM*70?sChes$IoaTK>OYy-FAFPsI@1#p@oGJD`59KPc5J;&8?k$pwLb| za@bbjS3y%8ZO5krmgtD*PQSG?4~ixoElyY88WU1(YR691>KS)8qsccl4^S#vvVT_! z`0tnhPwWi=?Kj%^r_AZIKG!DC0j|$5jyJ#fy7C$|PfwqXy~}?%wl9%t{#{t}ZPZ)}xP~H`scn#}*M@;zN zFaICd8;mw#_57w)Qo)mNB;rhWCc5*~*XX^eu2JKHKk%OjY<>{`1u7~a(t}w*($J2M zjz~ZCAEiz0xOQ57?C$n7W3%Rgl|{=5@2be4{HF1QH+3KUY)CI$^UxnT4zaHiPJN^|7FY7f0hD&2-bNQsJp~er*%82U) z^5)F(!sf{id&9WuDH@T5U?5yOhz7dbX(QPvV@Y5qYcPR5aT*_Dp}iE6zp7`qe^brz zV<#~3!`u0_0@6gWym5%SonR)e5VaMY)WjpW>(7?T%LN;Q@*aYRmGuXa&ixz8Q>Cy& z$^5!nKMGX<+P0n7m(*&_$sd+_R3t>)Azm~$KGER*Y)|uI*dH^^8#Bds`%LFK+MmFH zr&eeIk_jEvpY}1VYakmj(ZL2bUeBd_shTlYs9WThXvgK)$ZcdCw)UntK+n$YyUW_1 ziq=y@j+f|KuPW<)*1CDI7B#*mv@H4D+Fd*#q4mS5K$c6XS$vKN2&`m_IU)$PxI^tx z>{Vv@NHw+Eg+nYlQ~t@P9I^>un0+GuUQQPWBdK7n(@EJ1Olj-+r(;M@JA zD0$*@fR|Zs36KF3RGq?E&fih4rnxDhWz@$ZdXip0mK=%bvQW8)IY^R;PkAB`9tP1& z>y<5I{ZYwrUUXC=gs3*5;(i1Ap)=A{1OVz^>4Kw>mXfz#V(91` zqz#E_U1F35L?j~BZUl8?b!Nsg1j04R$t>gcqw7&s@%n>uxjE%M9hxo0q%+slqgB~d zzH7e1(gXnh9vXhIeF*@Nc?A$_FTE&asYu<>)?kl}Tbj)SGG*q+2b-ol2hS)fKEop! z4eJfs*U8M!0Xt(`=*8yXYNh^^ZJc^3bq)x!xP9eeeZ4ZS@d~!vWShHZZ^cBqaq$sM zq`TRQhp~ZSFkc1|zvO?^-=m)?)yGGGP8?5)Ic(oOlV>?Fy~Y#Q%b>l$OdGfqSvzl~ zXzo^SBU;!bL|w0Uc+F7ftkTccowcCEr|NofNMxck82I)FV|q}sd%Hqr?@FR{rfOTf z!)7A$P#yCZYY|sQwEZ8KJ^k^@oT8`U`1#3}Bd!RNnW-1w6Hg`@7{LmJyc=$R-ZotWZdmO|y9@Gp_R5j=BMMO@^lLT~aKy8wT~94_;RU0y9@&<)Hs@+EJlcQo=S}L;1vLV3N<`Znk;fSDti?X8j>W_ zKuBOkhhqsT72CaLL9_4T*k<75O{S`WBh|BsRbY{xw1T5nQ1|q z7U5>!KJ&TN%Y2)L%#H;Yuhi1QZK9Z4ggB=v`PM@ip`ROiQmB_Cw7w>E29l7Fmj$Xd3n~Pu$Cu{* zU>gS6s3bg73ky+}q2ewy(bCzIfuw|^P^|&GWfPkjr_ov;Y>(|BRVNIli&68GF-b>V>$041CInk}B$b$EP9#KH!LSs(N5!b!MTO5^`XL$H_jgfdfHEg`2e@Ch5 zyN?-Thu@QwOaFh>t6mqrMLwb_#EPpi{KUP+Ci zaOUGxDjP5POzXf>nG>Z5wnD}hhH3u|#sL5bikFr4e1AmRW+wWqHpZNlMxhp{KMGK{ zbgI--RI@{q62xWzs*80kxVR30Xno|z=Un$h)eNtJ|#d5_Z^*E5M< zjjbZKOfX#zWQp`th;+S=W&dTb!fNG)anH4Z?U+aR?`yozy>WK?vdXYqk2l`1YTeh* z+mP%*QSLoBknz&go0nd%w>|q^-_0*xZ3*EqTUOqQp~!XBjk^7?6^3?oW%i1cGA2FY zaxgU#sAu6hVL0Nzle-eGPN`)z8z%*lkzhwwdl+ikl1?b$Ln0Sw@Dxkf9ZHxJmJ;dZ z;u+PfRnV&*dFc!65%9q1FKxuO&NbyJVGuI}1uqZqs>?5vkh$l8>&iYq+{?P9(I*Ts zVsRG3u6$!>1tg&dmF>hZxlE?i_K3JCJ2Vw4*uYsz)SulLEVNU@^|pd^K3m4)11rwh zvqax9>j@KM_Gd=KZ{L9(&OVi+qq;xSWdG?IDR zGnejv+HRp`ccgyARSxlPfdFYT)Yy9E^hhjZD{6=1-(eXn%~ z6c!!m1UiJz`pcd3Z?SY+4-ikL>kznKd1{!HU*x9lnH~3ansPA@bc7j}Y0eKRwyhx? zJ__cD9Ejq%YAf;!-D(^FFU0qRJasHu8MEEbEL;zbuwylQdJRYU9!HJ5pmE>z7v#5* zPfYL>q~fEIZ!GnYx%3P+^f8{7ozVQDB=rj-@)QGq-0$c<$>wrV5a; z1|khy)JND<ZXa`*Z!&lleojvnrs*F`FFMX};i;msMFF@$jnI10|G2$pa_a0avZnV7iBcyB)Z zlPTvn@&Et;YCoE#Ja&10?TjKeydY8UeW1Z+7v`;>+S2Gnce~r6%EgLSk~ulVyaT1K z9%cb|J&W@DK@$Zh<;tm=hp@6QB4l4q3?bwo4)@i2VonM-+ir6te#+yX^rANqN@3B4$~~A` zDRV&^!>X%dydgBy1-_doT9rgRN2wQ=&)KODb)sUI=PfLNsI#*aT8z5P4}?t&c+8r< z2(`@h1=E&fdOkgkVVaoW^?#r{rPM5)1w3@%M$TEqvuEY?yK^w)&xAzEfW4^ao}{}~ z0-J2aydW#JmjN`Vdidqbq6TDV|EGs|st1?tD%R1ycb0>9}`8{S(LDXgr@ zcYWLmnzxe#|C|6kL*JGp?kj@o{MTJMlK}>-uhMzo#7|FEeaf;&Tq! z%Z4EEL>WpOej#{3yPUzjV+EC24qqq1$8Ba)hD|ICE9);Pw>oNuo5{AQ8PnxpRs;}c zRw-Noer50g;zE=;1JR)W}!W@lvrOT$rlAg<1$)aMR4@YHeQ zX3*TQMb3r|B=oefvZq0#)6mk0y%vkvRs%Xn_T($WB5@2K1vRg8i!96SY3Vd!Xaym= zO&bED^1fpSZ7HH$Hw31`7Lv2t<%LXnP)^xnv1IUg=I*#ZW4cI|>QNSZb*e<%)Qcg9 zD#S!&XSTyV`GKr}#<0YZL8}r83~F56rivD4*=g#CZiyRmv(T_sg(%MzCJCWJTlENm zZt;lp7l!)#;nCqRJ25RQD!JkKs+V(l>SN5ZErQeH1;fI&rilLW_-Goz2}N7s4tTb< zt&kg;YntL_&YG`Cz3~Psf5#2HCfzBJAU>aZH8dfM;y23>ZE_&tW&)iGf#|xqPBsVJB1@R9TKWJ9 z2{kgggb3oy5~FA!m(cTaiL5?h}5??g&G0%YtK85)}4aGCnYU zwWF6g(S}tpJ?GEYBr9+L6%Au@KrAGDxYHcyT2OmZ<&5D;?)+BGnY+GP33F4tiZXyL z#Gv0T7ZDyIJ`Bn{``B$8_Lz&lMLf7)ya>X`aZhQzm((^H)RNNsLl&v96Te^?PU1bAvAroW+$>CNE9FNR&?u8~Q?%5#$dKy7rfgc$ zZS*x0NOD0Ia&iV1=C4rDmRjdi>dHA_{l{ad$OJy=l57Mh!C!sQv==h*xEGP^t8@>fh8g1%ga^o&1Pw0g8wKP~ zA&Vrm0{QyzI8#~TX}{nqhsjuPrW$om1$#Xc?t1j>dg#mF=Ar)n9bo?pdSKv3{;;us ztu(B^QR$%eVMGYTyk>L@_{}BR0E;nEs*qzVv5z;pxs2zB3_*!HU|jS4+CZKVqwwdCGD*S-6B!Gh^tcZ+Tj%XFp6h$(y;q zaBoe7&@tDMH+x8}em12~$fLbF4oTk(xM5q9pW*jB@zb47?%U}h{^>I0m1A9UlOT7& zLDL(Pgf*j1Fo(Jg#m%@HJ$k!d`9;2gW5nw?f|?ti%1p^YF7%WnPMbqdzNQ+eo&%hu za%Rf}U+$`FEP#ss5jG2Ne`WUtr-1w&a$L-2$&exe8HrBqc3wxr@Y z)#puU%T`&i0r&;fy(6bgq3;4nZKJo6%&&7UP;R8(4SEYr&C3d58e-{)&s`M!StOtg zzJ;e}w+v?&wD3>Q{Z0XVo(x5%wwAN!a}DD$noa6!E7u_yVv{s&Wni!}Z7*D=iJllc z0ImR4(JRZ~ao6iQmOnBv6UF9Tq&0$NtFi}@NnIt`7eFTcAo29eNBL{x_Oh2pYHDK?9 z6a_j$`#5(f9->yd%ZxJ$nwqt{v@R^1Wv(rMxQqbNi-5WpO#E}L%K?iutj)} zNJo7XHcg%<+TKOHL`5nZ8pKeI_!1wL1({Q|dTABM1=2f&`Wy+Xg?mA=UoW5GZ)%#n z(|?ZKYMv!En{lysDZn9&P^`Bqvznr1L4c26M;mj)>*~3EurwsoPx)rG(0DSplsaQ- z`GmhLOdi?Fl7>;MFl1S{fk5Nend*eeq&rt|G^CcRkv1erdw70{&je@2&jH_1{dG~$ zE`nhGH!nm6&H}yR(NgTre3${8NJi6+Cy&XH)Nk!}UbO;HCmfP_4Q?n7F(V$N zMZXR9)7Xx*cWh6|Ax(}-2kNDFtddI!LMDziIP-a^1YTY?^$*;`#TU?heOSs0g{Zh9 z(=;|d>AJklnI$3EEpp&$JUK%{nBfdn*;;<4^<>{+S0Yuh?oqvl<+U8QcY2dM2GR9( ziRhm8;9Cw`4k{~Y{m_gDZ`X4&>Z>N*92tE5DuBNJ_xb^3cV@!w@Pyd#vmBV!_$P4R zE)Vcy8aK}-5-)?@6fKcWA1G!7KO+rQWsbMD*+ZavVuuj7v{4jP!B|2VDBEVPTdmz|lvrKEF3T>BoEF3{rwniKN4$HUdGnz8Tm7+>&!hEToFeFxRdOn z^m>SKPl9r$p2!X#{iDxQ;}p1RpJw@t9uJ>1YEY9;XrV!wqa4a|NvN|_6G>pkngysd zS&Y45JA+4>XR*A&J3^xJ`fwMEyon*^jc_&{er2l)z4XzClJPaqkEyCZWhW_qORRpK zC&G2)k4bV6OrU%yew6wBWy>`QUpUS0ip1l2 ztV1)xu0F!g;R+K^Poy-a2d-x%w!C_;daPL`@OD8VcmVy;rYSp zRP|@S8Akg1PQu?68UWz(DA_9oQ@g2sdT!Kn)a{<-|K>n-FLH}L%A{PnWy6Vw$IP=K zv|>_QpF`O)1=`J5dX+IpzZHDK<(=8w`Z{Bs+6UOMleG}{qLsUN|Awvu94m1HT3t#X zVAChd-m?QHfw9&7?gFuTR!7}(pNyet!w*G%eShCaf&jq3e>8mPhI7G6A)OE(c<#xX zVlJ=#(_TE)ipgUVE?YfB$6l`Eoi?@25{TsbEyl>|q$)SH&+zdx7VaE!0{={y5wlND zMfcNb%tB2}NV*WWvc;AfAD@y~8LyXU(r{C-jDgF2Qtn#an_5JQ^poakMr{kv4v)2cgycUTB~Y z62kgKgP8>#k5)j)Q_0J~7#c)=Wvc#X+|36AJ^V7SPwPE&X^Q3=;>EMslV+r>mQQfm zV~hFRCsjm40ufJ-f~2Tbq@W=h|;)u*%BKZ4#G{G2>aNVpg zL#y&rxiszhWg}=0&?GM{!+u+Z?@R0Z$(p+#3xJ85co(M8T)7i+0NLpk+w3lG1)z#~ z#oyk4eR(x!vecMIci#Q59nV~!c#`QtC@=zI)Dkq~g668)FsF3p-T}ohbxU%~p>K&Q zmTv3mJyYp<7Ws9po&Cvu{5w3IhcN?bfQoYfR>Gr&lXok<(XXr^5tPC**M*;j8+E*5 z7)Q2f>HQz_^!-LOgL?Os_1Jn>%U3ewP+ zv)d8_+eLNP!(PAqgkF(jvD@Ja&Ph#{5 z#x(;epS)~F(xYw)56?{1r^j_WtQnJx!HDJf)WzaS6-e+|s}jeJ(^N=yT5* zni9bz$iIPwn_Bx=yzv$Sf4DCZNwaEDI9cSaTMC^e|bOVN#DXIXI+GkU_c<=o^I= zYJrdmnr&}`!;muM9224zU}AID_EQZC|4N=1ay3{X)VD00?m3;++WzFB|CO5X)sxW}T%$0DpsqUi}X7oKv-o`E*)BQSRTbs(g0kSRs*hsaW zI!)oYq%93k0-LS4L<%pIRpmyDkgBx>HU>2V@mO(L5#AP-6{vqg{l7swF|nc0(4Gn7EmW8JqmnY%~G>#ae%!I}NXCuV*qN7JCoHD7WL5PtZMcVzA3qnnQo%dy78 z0f3CP9}laet53%LH=1>dvFhF4FZEEt*Z}Ch#UuV-#AbG3_~q0|AEqvBf#O?+7zc!&1DfABa2&VY zvVC~u%lHJ?DU=Mi+qbaCO*@Wk0JYk7%UNRx9349LtpkaN?XX9{~*~L9M1b0t8 zC?y_Tf}aD9jk=-dfC=;RKSuw*H;c)Rty2t(72iHSju(*=F!GE;2^Xp zOlMx{G~?_$+LaLb?=AV8u;R*aL4{UMXEz;g9iIbMtkJaSa>sshzIxBPBq`_QW}fiw z7FJ@?!b$teC&1N*DypcmttWS;(%l$2w)WwVhx9kJ+#HH%xXepmaabsp{W1W*D(FX1 zb1HEnCEP6y{9p^*j&&SsEJ>WUwhhlBDfZCuIW6QybR38bRPv^hzsu#js~BL8>twJb zSu2T~G~0AfaWN4p&iPzfnvC5AumEq@&f$a`@OzhxH4O=9Lm+V(VOg=87lxt?{ttjT zv@4OT8c#&iryKKg=`k_QiEqNf!kksacZUNT?3``IhjZB`dVZBY`<@S)nB4g_f)r%o zu&(lw1yVx945EDo9M4ezX`u3YI^xptA2TJI;q$9`g$O;FC72SAZ zgfk+FMqCII1zU#R)u9!32~kg%8K5O`pfQ6lCl1}*puE95#HF6Z-P~Fpq}~mr6-;Qr z&N>U|EO~niEV9-5=8JJSi3w01+johG<55(iNqs#zq!Hqe66y8OhrgW<$@N|*a_n2o z{p!syFD_)-Ykc%!()G|r_j<>*&q_<>>jz2t(K?+HL6S%9NGjVA(Qn+AY;WMM1in9_ zeYV?!TfH|VCS?w7?q2VBMub0RrPJ74St^N$OWJ=RmS8yPxIDE*DEJ0${}DyuhH@6gEh)sFB7 z`HU^aoMmn>X2#w-5M5~S6$V~A25oQW1~u=@mW{fK8I8s4Y7uZb*;V?WB=Wb}t&_W_ zkJEg_&g=>$C-~Pi6HgO*nF}H-eRtx{0Y{-)l`2hrf8Ams0C~InP-|bIq3LGKONm|& zqHoonT`2)<^jqKdTc__~x0h~A-U=(T%+0P$H+UeTi%YuwJcw0dpVlGHXF?xWvas~4 zF(Gftdu}?1ai0kLk_A{F zFxho)G;zEireHG>zA1Z@Aa3=Xj`7w}Xujf7eBr4gnkO2DIQA_0Ibhf)q0#O;@F?w8 z{0D84vmZ%GtV`{3RBImBN<&Y~^r!9&?+K=F2nhrye!jUa<7zD@{NyVYo>Oh=X8vaI zL^yLFxJ*5C01>td%iw(KDmUew53^mvD?n(H>JIk}w>+)`@>IM(p%GAqLwD3X30dRf zHmOM>N5W%DjTDViQl39ABOdfZm&cd^j*wl;2#fTz(Q7*3*E6+~5c*kq?c1cXu@?18 zY~}<3gT5hf`i=>(Y7;tVp0_9}S}|x&nNJ^O;AHs{=HHpuDQ4FZ>n|SDCPWRxFpXB< z@S)T;iQm1IeOIjX>SE=z-*&8rL+cDtoPPu)B%&Z>er~yRv*2O>yfQG5G&Uy{iOL{Y zSa@4l+{3XOvSK2L$9-bm-g6Q-bwpuOXdYQrMcLM~S;9`sTdpY%f@(%%Ezh`Q1ZqUr zFUJd;m@U;O;AktDna+OiBPVyXpu}n{FzM5cJLf6Y3Eb27Re(3_K7Jbenze;i;iwMykkFT#)TX@J@H=6)n#L(!@AX zTHFO@2ZZe->-Vp0Zs4ql<`9T~G9sM&M#*UC>p$2)J8>j$Rf}w52Jlyr7?3IwC_f6u1W#3+xV8*CB61>JcQ9ODqG@FH=HIZui`WXy~#Y2t-I zhzOY0vuaoIL_GA~_V4uq_4hwQ<;tu~mV|l-C^wG`S0bxr*`u9z0!4#bqRki-92To{ z=B?p13Dhgp3yNx#&ot1+48ZmX?230J>P2A!Y=u##gk=1|Y+4mA9q?F)K2)^OKp9y! zM6{Z39ez9RTWsb7;uJHzFo?Fhj>$5Rc*5u8az!$u)HFBX%@uCiCYu zdTc#=2@Um;q1iPVwYMxNg$mwzTC8~C$xDD@m(}jXYhLeNy56+W))h{z7VA{P&?Na$ zkW0?MTI~)aT6wtL9Z_T_#1hYy)Vxla45r)zUA5jwp{$@m&wU=w*B^>^4+nhV4|zV^ zwxo(--085VX$03Vyx)v`Hgq$3@L;$hSf}%T8r3P%uih_+#WGs`VAvsCr&B{2|LjA1 zqJW_z)%%wlUX73{N0WC@l%G-~OS4XTUoEU=0<;n4nU)jpNH-^LT(#SnXoT!oo7DDT zC0Lq>J<2`@yfRA(51?#4P*k)|+;ybudGB)L)6L@+^k#A0`PqJ6F z-C?yD>waekRsyD)+dfzPLbE0I`&8E#r)6+@prO5CerM1TpNa7Do&9c{U+z^$gbIA3 z#V+eJnU*oh%`mSu)H>lyUgTyXyi|GoPyuTJlVIx;cSLs}HT%gVlJNLp$1~+`W?$=T z#FB3H)~fne-S9KH%Cw4_5a?F&a1%1bEKP+HO>`1PaF@Kn((~QS*16w7fjKPTS}852 zB`Em0ZrCX(FAWJ%Y9M7!w^|%Ldr4#Al4Sn1tn4Hs`IP$ONVM{1O1!O|ECUUM!^_Mf zZKvlL+575)Z&k+EvZ#}cq^H!SLvK;U8Stl7s#~R1Rs~x^{A9Ovhc9($Ot_DYLP6cL z=PofwgA{5P-Mbz(k@dZ;N_u0&pi0?n8|JLZe&sNPC%yT@S=iFt;GOFU=r(sqn2?Ic zJp(emc@#2WO?_9%D)zGPaG2*N{TN{l#=UCZ_Oy*C!l8YyH0g+$R#3aFSWIlH063$& zLXy?oSwxq~yrw2E93jni;vpsn9VXp5UP#V9HubG!!N_ctn8;jFG!@TX=B^aT7~x+D zgQjOZ3>&Krl-Vq-0CG`UWv7JxlDbhPiH9hK6!CjL;*6q@9ez!k2S?r`kR@RoR;oq7 zzE~~x_o23C2KuZU1xKK$zp0Flg*QEGth?jOLj^%{J#+#)>0R$VQQ-u*?_2mwlw0{% z#Ko6hDP!`V^a%e%gz^8XfB^!{QV+{ownj*^n9T#P*ECr&AEhw$#M*T$45fe3TTI?> zzh`0yp&ZrV6N+jpwtT#RAdaz!Om4|V^y;%%1TJ}&z`zpZ%ux`2R2I_Og67UfzCLYs zXrXLwR%%GnKsU%Po~9`)4BC(sm1n476KvBA&b*^1Y*8J>!7s#W#>B6N{q=v9koyhm zx%4jyJSn6Pzy7VgB=1+-#qtO7A56U8EzWcpFK)d)2W+Hbcjsr8S$j;NJnQCSEMdhz zrX+FCE(t!Ne~JZJ2q7s+y&w`)-aJ#rxK=2F0cg{v8lUleSLp;aKXB1@1w`{e9 z%(xtG&k}=j$slSuk^AMmRB=~O(KYR>6e3ZEj7AyW>u&WjniNGWAw=s?*`6fsjV5E8 zrY#Fk^(=nAA@$)B3DD5CW-cqcnI0RWx>Q=4S5B!cvS&|S{-9jo7R%5HrP}-Wop0H~ zVkkKANBI|&Qom0{t@8h5RQ)Rqk6%z)S1jd$!u8Ny#F}W(RGRoo-_hH~Ld=9c5@Fs< zIck^#ckDKyWoh+>J#3e40RR{mdXe9&kNHJpeAO{rrp3|zlv_h>IJhW@rmkN(kS7PL zF?a=+b-+CPJuWM5x3Pxu;zXN*i89k`22u~2W&4k)*sT%<6=JphJ z2A!=Cw&KSU8gENK=8d=t8;u2B*%!oEgu!JD<(e&_aLbC5)}{5vz*5y#E9*>k5}aaY zZ;UCiYJNsI=&*V^5t1BeP{0PF2FeYp+cHYjM;e2!uqNn5S0~;74R7y)xLmqN{CYpj>o9+K>ifrP-Qk2Dir)Bz=Hy`}IeRQp$KEEQX-I+Xb z6#aBj$5;|jF$t$^tPu#OiFjGWE@5*&IK6CR;!R{CsF|HK%m*E*!KKKko;Bzv1n=xE zbBX{}xJoKEwJ|r$c!+JYI(RRBd3L(dNXoQN7yui4{PXzNhA~!}FBkt?ePpZo)ZW@(nT$>|mL=qdlZ~w#H`zAQ@J1_A_~0i9mtW#uU~A(a5E+PYn1Qu?GfKOVdSp z+YF@#=ytkuep}bi0aoSiI$lw|sTSX#a0STAw~f1g$KJYN1o0vg?{ma`vw!j0b!n;t zWhfI0Ntd?zOKkdAtshnk^Xzvj=sv(Sa{IzXve29eKq2Xj(=va03PW*2*3MuE5QU_9 z_PaU^{4lw6c&NZTrp9oc-rCk<%N%jrI^B0Fb4aD)rKzR&_GtJY0RA^Ww+{M}i!wGk z^n{$gwUE5#?(Af2c(Yq&X{ttYl0flVp+fRAulhj!i`LuWil;g0GKzB~1YvH_ zo)I$2eRb3x!79WsT%zO}X_okIhYeK7*ii0^e%s)u@3^q1ZL-2)pPitOqv@7b?lLbe zJBy~ivH-;o3Je&hTQf3t*mQN_weY#E9KLuKZb`9VC=QiisVy6rzQckTuTSQ8+Iyld zGTHZ_N(SfvgNrxdhitD~p95yj0WuHp3KqHBKU!KIO7a~E1QcK8M$xY3E9m5k9y=l*Y_%M50t3)3`Z+mIb#_Nwjeq8ANX5zHQ;ng(JI)%yJ>J*dN z1*4z;BxrqkHM9L63ByrQxJIb=Q|0DC!OEp-dG_bdyMgRM@1mbEusJW*X3dAb5Kcte zAYlq%dShuS02+SY(%rZsD|vNejWsw5bHY9OOSW>Ybe%AZ z_(32-kA(Wfb0nx`c;{>0yhu32ONJMUN%=YhA+#NX((SK`TW`Df$SFdHB^)CyD~NJl2pu(^CyK3_ugOQ4B2D*jc^`{ zy}2|){K9U&mjGOZeu{BTCQQ4w+06b?=bbs@r>D(-wiLPc$yn$hPUfo8R0DO`fJh( z?dh`g!w7KWrYozN0F#tO)_`2Ff&f#G!*`Av-|JN|r{{n@K|s+z<^#DTKU&frN^&0w zn4>0V{!h+qU)IGLIeWK?!_S{bvnL6Air`u`}dx z9-sW}gqmc3DFif*cj2BGZcpTx(4qFj|2VouMRw&Z?5 z2iIL24uUJfRNUU((1+P}g4G4S3u;XI{qEL30Th1`NB{M6jKEsTf-Qni8R;K%esD1f z=cR=i(mjqM#@7$q@2n=SkZWoYwyo;LI|yLP}?O^8dB z&AEC}usMv)dbNd)PP!wGjS0w*9HJfH$EdC_3<1-qWM`P3l^5n{&^Wq=zbo5lw8PmW zFmPzn;4HDP3y|*Q7GZ_-!xHHskTBUrCZ@@oPrp1X3(mbwmq|wH%2dRjH?sxnwEO0g zuL@3v$;r>TQ@5;Ue36TbL+C{hW4kAP9i-GkBjkMEGI9!|Axz-ywtm&gE=>%FsDq#+ zs6q+SBQn>#FL=Xm`OS{pU;N4M|H=HW{5yw1>!dgI4YZPe@VT$-JaI9Bik7P;5ay`3SI!r;m@$y3EM15}WTS+4mwq zrdH%GY}n=-?(C%xN^4hGr+G5grwDt zjps9@TW=lXvT?RsUWi+<-IaMw5VxmZZBKjTMCH%4P)JM@uO1pgx_=N=Dw^NBP61U1 zwmO?G46JE~HT5w}Cl%tI1!#1#>>iSHHP322zDZ^zm4;=b(77DD6b}~R^>^3--`cZ@ zTgmtlOH^`=_8MQ1t(XaAkEXyXuws{+w7()Fb}(y#|E!GO8Sa=^nrup=`(xv6x%E#7 zOpADE1V}+bTK$zVQ+rOe+B6JiYdO41f{>3(+VdpR>SBp4uZ5B?VGcVDY;U&m+j2(s zSHR+;|z$D%yOX+2r;`%d;Mx6>o!kVvcC>yw@RClXu)F^WuEAo}k{#Uy%zs-zx1 z4-7(unnyHS#-_Y-g48vW*{8_$QyC#Wdx&ILX_XF$8a0vr9IJ~k29E45E&U+ zj(X=P`Sca#A|6Unb=eL7CWZNrcv7Ooi+G+ROD)J<|5?*uAOD=8LrWVwK6ojSd7d8i zdc>XBV!v+pa{x)zdqVd$`jXlqcLu$ftb5Vu)QG6|teJ1YUO0?976vfXCT>C=AhP;i z`~zh@`$@<9)CD;V^2i*7zZ%Iopv718eiuE>RyyNWE89p>Vx>TI9=nzK+{=9zhh=I* zSg?atMJ`EFy!ifpISmW^BNr33LPu9|O|{m~azwK`lvzo+0o!maKJQD|?J`F3ZqEHO zwA}e6{zF5ttR?6E?MS8>vExDqteup|P{nmlt=Xj@se`Tcj&xu9a2kCS{%_3nUF{DU z;ukop58>hYfS{he$rctwMlI^|eix>Z+#Y$Rj#9>*1JIPRH(GP6mqOUyJyHgLdGw`4 zMlnV49I)xmC2$U~@q0cJ_gRse_>~$dgvAb|A6gLn;)OD6)CL)D%RM__JaPZwQXt!) zUX9hlIpFzP=nXbKdvftQtiWekU^6V`H;7Me%3BsgdC zWIA<9qm*Hb>EqN6)YJ}ykAWspBKy9_Etphl5nkI72QSkZ9Ew zkVS1vEi$f9L3mUyqZxerY(Hj(MpEY;R{hgU_20Drm-~NF{}CE*P=4RPA~Ae|?{WNx z#LychF`T@V7>;qxZ(d3aZ(T|Z4<1&>{6%8eX1OhD?K^0+rSZ||))y7Dr{BLl@|g8b zs(W`1DBU=w3=ZzhoLgP|r4NyD_)Vi>75@_d{b3qS!$l*1i2p8av%P5LFA~EG+f0iu zZsji$!>x~x{xB{0&BdMkAu+t@fWK{p*tU(v!wT@xh|@r;Y3CZxF%9&Xd!r`phpOP$RcRLUE535kl1 z%Cz7k;o~FUV~TEETihsg%NQ371Q)7#X#Hf3PX5aSWXQ>`sA5%y{>}Fz@W?>%#6I-5+#+=P=p0&Cxq5 z4|MJFIO5e@#yVT?_bi6o!BZw7 z?i0%!ocz}Jl^S+C>K-i5$hjJx!I&_>ydT*Lu%fMu6c>|Gcj6h+865s(HlnEkttI#& zgF&j)R4e8Wg37g*EybG#i5-eASWmDL*?j~Ryn zpCYDVH;-RTXC>`gZHJsaygqytD;79@(|3Qi_?R7dGCswJl18$O<<>mLY$u?4MrvX> z^j+n>lqO-tkbp2{HD#gq0vJq+dIb?IdBEw*`=LbmYEChyZS47|LxW|K z!{Q(QfDIVJrm44ndbi9w4Te9#U)^1KJd|tux3nOXrG+TlScVa12w5Tq zGxjlrvM*;C6;kOGiWs4on2{}ENGOdlNU3BQ1~Fu-ELjd-ZHTPBkMld2_t*J+-s5xL z{yFzQbItR4p7}h_ec#V@U-xx=zu&1~;F}O{X_V`t!IS=SuaFS-_6ALazE;?g$qmLY zf8mDB3SPoIEBVgSCvksmWoHHfucb5H(QB*as^sGDjTr;Z#Nk!`Gx>04Bs2@l=5tjy zz+qv9nG=;se5i^&`Pd-KmotOf{N{I6-s9=}?O}MUN#l=oL#C}BX`Q}KP8za{mVewN z@bM#}O=&~8VD&Wr!@&!U2X0+C$(2kpO|n6mRarI*#Qb?Ssk8@M@tvje=rN7y6q6OQ z$mO9#o506#=j}+tG?;G?T6xbzqwL-3aJS$sGY+sOy0+}OKJ%!-@n1YH+8+#HAE`j1 zc3$pHOM3X59yPT1;sZ;Ve1A~Zu>3q@)M6!JE^NEf$OG!B<<57g9rFxa;CGhHi}Vsg zbpPYt$9kcfMqHsYyw6$ucRZq6y?SgvA?~bsCqj3s{>p{W;+D&d221cfrW}|i3g@4* zoh#HB{1CEI<5+u%@`MwB>}nxi!bR*peg4kk_j-yEzkS6rCyWzc{46r**GVl$$$5-i zsE4$A`pa{oITx^%0u7LeG8KS*6qQl?qE4UD3en|VJhM}dSB=7~b{v_?oWtd$3MsWm z39amhXifd<0(nyBu6%dc%0D6DY!3UYnyO)Tuz$S>3QhtNJy!Qt0oP5|H@WsX=qd_- zaE5xm0~iq-5CFFOY-k`(z3_kX*?fcNIWE|o?{_74?`x9H-01Tgy~&sa1dCyR?ecIbR|}zU|G*PdOm)_V#4x^Jlj44yM>r| znNi5hO5WBnmZKJ|7mRd41cJ;cyiVq;mWr2<&dY1c=89ZJHARTP@fa^+f_a0pf3kGa zh{Q!H%E_~IU(C~39K9dN_t&r>lpZjIiYX-!87Z~onw*j;KDc%~K(ARR?%WS+>4G=E z`L!*YLqn9FcjU`fR)g~=j-2dX(g}ashha1)WxlEf1aiC_-Ma%>%Uyt~q^vCM*kWDH ztXAx^g$(7+^&o*Y-GXiQx~M|{IBTFm(CMr7Lej8sh?#`t@}V7eyvCT5tTFPbht!te z%bZqK$3832tJV_=wQK{53YE%rMGsmOy$|J_8cxq{t|sP;Km+xH&17{cvH)iR355z4 zXk3;}?SjOGq}q6JGc4x<&4x>)&2Zy^{R^Vll{q44Pus9;xMD-LC)!*qE4Qt)XWrEJLd%|Tpao(U{sAo%B< zL^m+nO4@U`a-n#Zmt`#%^@csaH7C*}N>v`yuIAvn4Ew-mz^^?e$eiK-F=ytY%H&^w zTzx;Ml&YQ~-9;j=m_I`GxV1ltPq+l>dqQi4bw&wp5Q+^p0}<$-$|pFcB%3U^tbEgJ zZ|gWwnv!v5hRmaSO?uj*g!iJ4V0)-U9c0x6TBXhM6|7grunZK&g zxppmpz}uLSI7r1Uw}2k2&o)|HOG~FOVB8b7H2HS1xpI}Ymc8`{qd=fD@xcl>8X>Fc zNv=+8#)um@R4D^NQoG+#V~Fw}Bbe=%zX`JltOf7J#_QVgO4W-x{B5ME+5yq&VAcJt0G(NHZ)nbp}y0=E0S)6ozj)t)9HS2(04MjKeZzI zYm2-+_exHZ1bUApEM0zUZnCGJ$LN&|A6iMX4^(+sG;7?q6jo_i5s@I_ zUJIPh3Rz<1$#egx=*nlpMMAtg|1}r8{OOW}(3L~8DY&VFq_?J+4tOF>Y0j9D@0`$k zHO~|yv(T(mA-{Z^XBP`JhPr>(oV?m;Tk*ZDvS(M`No9#GUgMQox~E=BXu{}9!{G{q zzAnpi_%uWL)A`*PQ#LeNoiL5MSsr31;!tAU8G5OyDd_0$WAo$e`upIh)$-9_SIOaA zrreB&+Y;RlScutFDQ+u{j7q5yu}+tZwW5I(rN!bsZ%LVI@U~nHlvLvhK?6L%jkX>Y ztm$AfSB?PWd*~(6T>Gq%f&y3f_hRP7Ijsz_G=zxkYl{Ic9xtEX?sk5xT=i5c#(qyv zd?x4e=aj~rjuTWBe~S_g*T(8w5eCqfdMf6(%~3itX6tS_7@hA-S}wU91$1Qc8Gvh`b^%c!C^|G zvEz}V4)J6YY{SPg_Z_Cq_Htl&i1hViW$XO`7(ZtT+$4nT^1T5_;k6!Z@1Rj$bI_yw z4D{!=505wn$dy|XX&|LVZvB8oRPLG4ammwaeVEXwg!2O%&+wy@`hQE{;6stD>~x08 zH%$kH!EDF-`M5G$E5n^>`vqt+S#7yq_T$H>Q)hK|B9BQjXp0HDRs6<2S7Qjz4J}+i z96rT4LkI~~fZAFfyu?1{5{p~uo=QPNi2eQ@e8Wcn;c))e8u-%-Rbro>Z*8k{oDFO^15s9&9&n6V#LJ7uN^S4Q;Rr zTcXSOoV3qPv@3UDb&PaFPTky9RF-A$5wEU*Iq{4;apctg@T^USJsg@BHDd@RsFni+ zz1-rQKFq9Z^N1p(Vu@|g9!5-1tN{@cA3jG$K70WHh^u(p71{z`zb=0CWtYrx>t8Ut z%t5?@K6#(FZYxps!(yV<1P6o7D1|Xt8qjqtR129j(b#IKZkAA^pD;!YtW_(q0woE@)$l=NZjbFkM{H8e zV^e~3Tr9l&(@s9pwQYwwUsD__573g2b8y%;cQ>TDy^_A`nsP{}%eD}QcLwCiymU&b zcV2%r%(}dIwp&u}iV=9b>Va_I3-MR2XJ9-ACJkU!sW^jlN#4Zu$Ntep z|1V^iHVqm7;-O+hLOI~IY4*oV(wg4f@H?aAMQL4Tk& zg&MX_k;-UG&fFPwoF;kS-c;-HNoZ79(CtF9OP({aY+sV<4w0H;aRR6OK<45kH6pSO zgGAyAsR=#;Z~Bhf*=zIKjB2Ve2sa{6$)Zv3!8XNy>>TfXboQr`K+&Rkh@2jeMCCzU|x zm#T=5iw>0vsW0p-NT=!jSBk_z50$wW-VcH8EAgC&ZUY6~#B`M6F(orel|{XBF9!sD z-0h>g$3S6P=Ya+N%Kfr&{!~!$87XfVE(HM44WbmKzAbm<5w2uoZ$fqV8heQ-q0pkB zP!JT0rMsn;mvZmmeKQ_$@opE(`^Me5PJXW`iJ8Bp?od!0ifKmrQ9z|02JpR=P%mGf zUaC8Pa$IFE)AuS8Z`#6``uHbQ$C!S7)%-7! z>VKU5Hwc^T5l^~Oq92__el7Le^Rb>Ry@!o$cgZ;w?|mf=_l|qT)CIjRkN|W!gZYGR zOd|NA=1p6c44p6bg$h{1k1D-|h=Zd&Zf!SHExxPFtCH4~h6}ECi2KSXCu{9!!Y9>{ z`M$9Ec2R*(Pb|XCn(yJc{ld2pjQlg{;n|s;PG5DeP6o6S8i8!y9F7)q>gi9Le3awE zr;6@poz@l*J=(>1@PPmyE5Al9@%v`f8eF#P<+UN$)b1gWC)H?K%FAe!3TsBgTk$DQ zlm||Qxl^5lm+kcf?c6hNoV8d)5T`Zntuz%@6#Q)hLByY>7;A>D560#rC()wnERr2K zEZ?tbRss_3hn9)M!;f%KNVDD1p4jr-j+}fcGNOsr3|5$Gx=DlD<(d#kFS2|sI@a*X%SvC*wHNi zh@)%oi0am%{Ob=AmeVfXnZYuuCZ4|Yb&hsEoBrvOMTqvh!|&d`Bx9d8m&_?P6i;U8 z)RVGFL!KGkJYnqMi5>N*-6tg%)qU0s9-Dv0QKrh|7jVbk8@b@eMzj7CW>xi$Ia>TXjQ@5z6rTMBr WwE25Pz69EOX0AJqpJr_T-t%vK=vk@& literal 0 HcmV?d00001 diff --git a/docs/concepts/deployment-scenarios.md b/docs/concepts/deployment-scenarios.md new file mode 100644 index 0000000000..5ffb046ac4 --- /dev/null +++ b/docs/concepts/deployment-scenarios.md @@ -0,0 +1,38 @@ + + +# Deployment Scenarios + +## Simple Browser-Side Rendering + +> :warning: This is suitable for demo servers to have a fast build chain. We do not recommend this setup for production use. + +The application is built completely with Angular CLI: + +![Angular-BrowserSideApp-Build-Activity](deployment-angular-browsersideapp-build-activity.jpg 'Angular-BrowserSideApp-Build-Activity') + +The resulting files can be statically served using any HTTP server that is capable of doing that. +The initial page response from the browser is minimal and the application gets composed and rendered on the client side. + +![Angular-BrowserSideApp-Sequence](deployment-angular-browsersideapp-sequence.jpg 'Angular-BrowserSideApp-Sequence') + +Of course, this can have a significant impact on the client side if no efficient rendering power is available. +Search engine crawlers might also not be able to execute JavaScript and therefor might only see the initial minimal response. + +## Browser-Side Rendering with On-Demand Server-Side Pre-Rendering (Angular Universal) + +> We recommend using this approach for production use. You can use the supplied Dockerfile to build it. + +The application consists of two parts, the server-side and the client-side application: + +![Angular-BrowserSideApp-Sequence](deployment-angular-serversideapp-build-activity.jpg 'Angular-BrowserSideApp-Sequence') + +The resulting distribution has to be executed in a node environment. +The _server.js_ executable handles client requests and pre-renders the content of the page. +The resulting response to the browser is mainly prepared and can be displayed quickly on the client side. + +![Angular-ServerSideRendering-Sequence](deployment-angular-serversiderendering-sequence.jpg 'Angular-ServerSideRendering-Sequence') diff --git a/docs/concepts/hybrid-approach-architecture.svg b/docs/concepts/hybrid-approach-architecture.svg new file mode 100644 index 0000000000..3c65df2969 --- /dev/null +++ b/docs/concepts/hybrid-approach-architecture.svg @@ -0,0 +1,411 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + nginx + Client(Angular) + + + express.js(node.js) + + Universal(Angular) + SSR + + + ICM + + + mappingtable + + + + diff --git a/docs/concepts/hybrid-approach.md b/docs/concepts/hybrid-approach.md new file mode 100644 index 0000000000..0c06e38247 --- /dev/null +++ b/docs/concepts/hybrid-approach.md @@ -0,0 +1,82 @@ + + +# Hybrid Approach + +This document describes how to run PWA and ICM in hybrid mode, so that pages from the Intershop Progressive Web App and pages from the classic storefront can be run in parallel. + +A possible scenario would be to have the shopping experience with all its SEO optimizations handled by the PWA and to delegate highly customized parts of the checkout or my account area to the ICM. + +## Requirements + +- ICM 7.10.16.6 +- PWA 0.18 + +## Architectural Concept + +![Hybrid Approach Architecture](hybrid-approach-architecture.svg) + +For a minimal-invasive implementation, the mechanics for the hybrid approach are mainly implemented outside of the ICM and in deployment-specific components of the PWA. + +The ICM is proxied in the _express.js_ server of the server-side rendering process and hereby made available to the outside. +A newly introduced mapping table is also used to decide when an incoming request should be handled by the PWA or the ICM. +This mapping table is also used in the PWA for switching from the context of the single page application to the ICM. + +## Configuration + +### Intershop Commerce Management + +The ICM must be run with [Secure URL-only Server Configuration](https://docs.intershop.com/icm/7.10/olh/oma/en/search.html?searchQuery=SecureAccessOnly), which can be done by adding `SecureAccessOnly=true` to the appserver configuration. + +Furthermore the synchronization of the `apiToken` cookie must be switched on, so that users and baskets are synchronized between PWA and ICM. +See [Concept - Integration of Progressive Web App and inSPIRED Storefront](https://support.intershop.com/kb/index.php/Display/2928F6). + +If you also want to support the correct handling for links generated in e-mails, the property `intershop.WebServerSecureURL` must point to nginx. + +_\$SERVER/share/system/config/cluster/appserver.properties_: + +```properties +SecureAccessOnly=true +intershop.apitoken.cookie.enabled=true +intershop.apitoken.cookie.sslmode=true +intershop.WebServerSecureURL=https:// +``` + +### Intershop Progressive Web App + +The server-side rendering process must be started with `SSR_HYBRID=1`. + +Also, the **Service Worker must be disabled** for the PWA, as it installs itself into the browser of the client device and takes over the routing process, making it impossible to break out of the PWA and delegate to the ICM. + +### Mapping Table + +The mapping table resides in the PWA source code and provides the page-specific mapping configuration for the hybrid approach. + +```typescript + { + id: "Product Detail Page", + icm: `${ICM_CONFIG_MATCH}/ViewProduct-Start.*(\\?|&)SKU=(?[\\w-]+).*$`, + pwaBuild: `product/$${PWA_CONFIG_BUILD}`, + pwa: `^.*/product/([\\w-]+).*$`, + icmBuild: `ViewProduct-Start?SKU=$1`, + handledBy: "icm" + } +``` + +Each entry contains: + +- A regular expression to detect a specific incoming URL as ICM or PWA URL (`icm` and `pwa`) +- Corresponding instructions to build the matching URL of the counterpart (`pwaBuild` and `icmBuild`) +- A property `handledBy` (either `icm` or `pwa`) to decide on the target upstream + +The properties `icm` and `pwaBuild` can use [named capture groups]() and are only used in the _node.js_ process running on the server. +However, `pwa` and `icmBuild` are used in the client application where [named capture groups are not yet supported by all browsers](https://github.com/tc39/proposal-regexp-named-groups#implementations). + +# Further References + +- [Guide - Building and Running Server-Side Rendering](../guides/ssr-startup.md) +- [Guide - Handle rewritten ICM URLs in Hybrid Mode](../guides/hybrid-approach-icm-url-rewriting.md) diff --git a/docs/concepts/localization.md b/docs/concepts/localization.md new file mode 100644 index 0000000000..7faf56a78c --- /dev/null +++ b/docs/concepts/localization.md @@ -0,0 +1,274 @@ + + +# Localization + +Intershop Progressive Web App uses a mix of Angular's internationalization tools (i18n) and the internationalization library ngx-translate for localization. + +For more information refer to: + +- [Angular - Internationalization (i18n)](https://angular.io/guide/i18n) +- [NGX-Translate: The internationalization (i18n) library for Angular](http://www.ngx-translate.com/) + +## Usage Examples + +Although ngx-translate provides pipe and directive to localize texts, we want to use a pipe-only approach. + +### Localization of Simple Text + +To localize simple texts, just apply the `translate` pipe to the key: + +**en.json** + +```json +{ ... + "header.wishlists.text": "Wishlist", + ... +} +``` + +**\*.component.html** + +```html + +``` + +### Localization with Parameters + +ngx-translate uses named parameters. +A map of parameters can be supplied as input to the `translate` pipe. + +Localization file: + +**en.json** + +```json +{ ... + "product.items.label": "{{0}} list items" + ... +} +``` + +Parameter setting in HTML: + +**\*.component.html** + +```html +
{{ 'product.items.label' | translate:{'0': '8'} }}
+``` + +Parameter setting in component and usage in HTML: + +**\* .component.ts** + +```typescript +export class Component { + param = '8'; + ... +``` + +**\*.component.html** + +```html +
{{ 'product.items.label' | translate:{'0': param} }}
+``` + +### Localization with Pluralization + +For more information refer to: + +- [Feature: Pluralization](https://github.com/ngx-translate/core/issues/150) +- [Angular - I18nPluralPipe](https://angular.io/api/common/I18nPluralPipe) + +Localization file: + +**en.json** + +```json +{ ... + "product.items.label": { + "=0":"0 items", + "=1": "1 item", + "other": "# items"}, + ... +} +``` + +Parameter setting in HTML: + +**\*.component.html** + +```html +
{{ 8 | i18nPlural: ( 'product.items.label' | translate ) }}
+``` + +Parameter setting in component and usage in HTML: + +**\*component.ts** + +```typescript +export class Component { + products = ['product1','product2','product3']; + ... +``` + +**\*.component.html** + +```html +
+ {{ products.length | i18nPlural: {'=0': 'product.items.label.none','=1': 'product.items.label.singular','other': + 'product.items.label.plural'} | translate:{'0': products.length} }} +
+``` + +### Localization with Formatted Dates + +The date pipe is used as formatter in texts with dates. + +Localization file: + +**en.json** + +```json +{ ... + "quote.edit.submitted.your_quote_expired.text": "Your quote expired at {{0}} {{1}}.", + ... +} +``` + +Date pipe as formatter in HTML: + +**\*.component.html** + +```html +{{ 'quote.edit.submitted.your_quote_expired.text' | translate : { '0': quote['validToDate'] | ishDate: 'shortDate', + '1': quote['validToDate'] | ishDate: 'mediumTime' } }} +``` + +### Localization of Text with HTML Tags + +To skip the cleansing of the translated text (i.e., to insert HTML), the `innerHTML` binding has to be used. + +Localization file: + +**en.json** + +```json +{ ... + "common.header.contact_no.text": "1300 032 032", + ... +} +``` + +Usage in HTML: + +**\*.component.html** + +```html + +``` + +### Localization in the component(.ts) File + +If you want to get the translation for a key within a component file, you have to: + +- Inject `TranslationService` in the component +- Use the `get` method of the translation service, e.g., `translate.get('ID')` +- Use `subscribe` to assign the translation to the data array + +**\*.component.ts** + +```typescript +export class ProductTileComponent implements OnDestroy { + ... + destroy$ = new Subject(); + constructor(protected translate: TranslateService) {} + ... + toggleCompare() { + this.compareToggle.emit(); + this.translate + .get('compare.message.add.text', { 0: this.product.name }) + .pipe(takeUntil(this.destroy$)) + .subscribe((message: string) => { + this.toastr.success(message); + }); + } + ... + ngOnDestroy() { + this.destroy$.next(); + } +} +``` + +See also: [How to translate words in the component(.ts) file?](https://github.com/ngx-translate/core/issues/835) + +### Localization of Text with HTML-Anchors (Links) & OnClickHandlers (Callback-Functions) + +Translated texts can contain html-anchors (e.g. link to external page or internal route) or onclick handlers. + +Here we need the `ServerHtmlDirective`. +Pass the translated text and optional callbacks into `ishServerHtml`-directive. + +To open a modalDialog by click use the callback-handler (1st anchor in example). + +Links with internal route-target will routed by angular (2nd anchor in example). + +Links with external target will navigate by browser (3rd anchor in example). + +Localization file: + +**en.json** + +```json +{ ... + "registration.tac_privacy_policy.label": "I agree to the
Terms & Conditions. Home Google" + ... +} +``` + +Usage in View (.html): + +**\*.component.html** + +```html + + + + + +``` + +Usage in ViewModel (.ts) + +**\*.component.ts** + +```typescript +/* attention: generate callback-function with closure */ +showModalDialog(dialog) { + return () => { + dialog.show(); + }; +} +``` + +## Localization Files Generation + +The idea is to use the existing localization properties files of the current Responsive Starter Store cartridges (or the localization files of a project) and convert them into the proper JSON files that can be used by ngx-translate. +For this purpose a [Gradle plugin](https://gitlab.intershop.de/ISPWA/ngx-translate-plugin) was implemented that can handle this conversion process. + +In the current state of the Intershop Progressive Web App, the converted localization properties from _a_responsive_ (without _app_sf_responsive_b2b_ and _app_sf_responsive_costcenter_) were added and should be used within the HTML templates. + +## Extend Locales + +To learn how languages other than English, German and French can be used in the progressive web app, see [Configuration - Extend Localization](./configuration.md#extend-locales). diff --git a/docs/concepts/logging.md b/docs/concepts/logging.md new file mode 100644 index 0000000000..2c20df4ded --- /dev/null +++ b/docs/concepts/logging.md @@ -0,0 +1,31 @@ + + +# Logging + +## nginx + +The nginx image providing [PageSpeed](https://www.modpagespeed.com/) and multi-channel configuration uses the default logging capabilities of [nginx](https://www.nginx.com/). + +- [Configure Logging](https://docs.nginx.com/nginx/admin-guide/monitoring/logging/) +- [Debugging Nginx Configuration](https://easyengine.io/tutorials/nginx/debugging/) + +## Server-Side Rendering + +The _express.js_ image serving the Angular Universal Server-Side Rendering can be provisioned to log extended information to the console by supplying the environment variable `LOGGING=true`. + +Information logged to the console contains the following: + +- Requests to the SSR process are logged with [morgan](https://github.com/expressjs/morgan) (see configuration in _server.ts_) in the form of: + + ` - ms` + +- Requests handled by the SSR process are logged at the beginning with `SSR ` and at the end with `RES `. + +- Further the redirect actions of the [Hybrid Approach](./hybrid-approach.md) are logged with `RED `. + +- Uncaught `Error` objects thrown in the SSR process, including `HttpErrorResponse` and runtime errors are printed as well. diff --git a/docs/concepts/project-structure.md b/docs/concepts/project-structure.md new file mode 100644 index 0000000000..b6934b358c --- /dev/null +++ b/docs/concepts/project-structure.md @@ -0,0 +1,118 @@ + + +# Project Structure + +## File Name Conventions + +In accordance with the [Angular Style Guide](https://angular.io/guide/styleguide) and the [Angular CLI](https://angular.io/guide/file-structure) convention of naming generated elements in the file system, all file and folder names should use a hyphenated, lowercase structure (kebab-case). camelCase should not be used, especially since it can lead to problems when working with different operating systems, where some systems are case indifferent regarding file and folder naming (Windows). + +## General Folder Structure + +We decided on a folder layout to fit our project-specific needs. +Basic concerns included defining a set of basic rules where components and other artifacts should be located to ease development, customization and bundling to achieve fast loading. +Here we deviate from the general guidelines of Angular CLI, but we provide custom CLI schematics to easily add all artifacts to the project. + +Additionally, the custom tslint rules `project-structure` and `ban-specific-imports` should be activated to have a feedback when adding files to the project. + +The basic structure looks like this: + +```txt +src +├─ app +| ├─ core +| | ├─ models +| | ├─ store +| | ├─ facades +| | ├─ utils +| | └─ ... +| ├─ extensions +| | ├─ foo +| | └─ bar +| ├─ pages +| ├─ shared +| └─ shell +├─ assets +├─ environments +└─ theme +``` + +The `src/app` folder contains all TypeScript code (sources and tests) and HTML templates: + +- `core` contains all configuration and utility code for the main B2C application. + - `core/models` contains models for all data entities for the B2C store. + + - `core/utils` contains all utility functions that are used in multiple cases. + - `core/store` contains the main [State Management](./state-management.md), `core/facades` contains facades for accessing the state in the application. +- `shell` contains the synchronously loaded application shell (header and footer). +- `pages` contains a flat folder list of page modules and components that are only used on that corresponding page. +- `shared` contains code which is shared across multiple modules and pages. +- `extensions` contains extension modules for mainly B2B features that have minimal touch points with the B2C store. Each module (foo and bar) contains code which concerns only itself. The connection to the B2C store is implemented via lazy-loaded modules and components. + +The `src/assets` folder contains files which are statically served (pictures, mock-data, fonts, localization, ...). + +The `src/environments` folder contains environment properties which are switched by Angular CLI, also see [Angular 2: Application Settings using the CLI Environment Option](http://tattoocoder.com/angular-cli-using-the-environment-option/). + +The `src/theme` contains styling files. + +> Components should only reside in `shared`, `shell` and `pages`. + +## Extension Folder Structure + +We decided to group additional features that are not universally used into extensions to keep the main sources structured. + +Each extension module can have multiple sub folders: + +- `exports`: components which lazily load additional components +- `pages`: page modules and components used on this page +- `shared`: components shared among pages for this extension +- `models`: models specific for this extension +- `services`: services specific for this extension +- `store`: ngrx handling (State, Effects, Reducer, Actions, Selectors) +- `facades`: providing access to the state management + +Optionally additional sub folders for module-scoped artifacts are allowed: + +- interceptors +- directives +- guards +- validators +- configurations +- pipes + +## Modules + +As [Angular Modules](https://angular.io/guide/ngmodules) are a rather advanced topic, beginning with the restructured project folder format, we want to give certain guidelines for which modules exist and where components are declared. +The Angular modules are mainly used to feed the Angular dependency injection and with that component factories that populate the templates. +It has little to do with the bundling of lazy-loaded modules when a production-ready ahead-of-time build is executed. + +As a general rule of thumb, modules should mainly aggregate deeper lying artifacts. +Only some exceptions are allowed. + +### Extending Modules + +As a developer who **extends and customizes** the functionality of the PWA, you should only consider modifying/adding to the following modules: + +- `src/app/pages/app.routing.module` - for registering new globally available routes +- `src/app/core/core.module` and `configuration.module` - for registering core functionality (if the third party library documentation asks to add a `SomeModule.forRoot()`, this is the place) +- `src/app/shell/shell.module` - for declaring and exporting components that should be available on the application shell and also on the remaining parts of the application, do not overuse +- `src/app/shared/shared.module` - for declaring and exporting components that are used on more than one page, but not in the application shell +- `src/app/pages//-page.module` - for declaring components that are used only on this page + +As a developer who develops **new functionalities** for the PWA, you also have to deal with the following modules: + +- `src/app/core/X.module` - configuration for the main application organized in various modules +- `src/app/utils/.module` - utility modules like a CMS which supplies shared components and uses _shared.module_ +- `src/app/shared//.module` - utility modules which aggregate functionality exported with _shared.module_ +- `src/app/core/store/**/-store.module` - ngrx specific modules which should only be extended when adding B2C functionality. Current stores should not be extended, it is better to add additional store modules for custom functionalities. + +As a developer who adds **new stand-alone features**: + +- `src/app/extensions//.module` - aggregated collection of components used for this extension, including the ngrx store +- `src/app/extensions//exports/-exports.module` - aggregation of lazy components which lazily load the extension module + +When using `ng generate` with our PWA custom schematics, the components should automatically be declared in the correct modules. diff --git a/docs/concepts/search-engine-optimization.md b/docs/concepts/search-engine-optimization.md new file mode 100644 index 0000000000..9eea914da7 --- /dev/null +++ b/docs/concepts/search-engine-optimization.md @@ -0,0 +1,42 @@ + + +# Search Engine Optimization (SEO) + +This concept documents our approach for search engine optimization for the Intershop Progressive Web App. + +## Server Side Rendering + +The PWA uses Universal for pre-rendering complete pages to tackle SEO concerns. +An Angular application without Universal support will not respond to web crawlers with complete indexable page responses. + +Angular's state transfer mechanism is used to transfer properties to the client side. +We use it to de-hydrate the ngrx state in the server application and re-hydrate it on the client side. +See [Using TransferState API in an Angular v5 Universal App](https://medium.com/angular-in-depth/using-transferstate-api-in-an-angular-5-universal-app-130f3ada9e5b) for specifics. + +Follow the steps in [Guide - Getting Started](../guides/getting-started.md) to build and run the application in Universal mode. + +Official documentation for Angular Universal can be found at [https://angular.io/guide/universal](https://angular.io/guide/universal). + +## robots.txt + +We use the library [express-robots-txt](https://github.com/modosc/express-robots-txt) in the express.js server (`server.ts` in the project root) to supply a response to `robots.txt` for crawlers. + +By default the universal server provides a response with access to all pages except some restricted paths (e.g. `/error` or `/account`). +To use a custom `robots.txt`, place it as a file in the `dist` folder. + +## Page Metadata + +The PWA uses the library [@ngx-meta/core](https://www.npmjs.com/package/@ngx-meta/core) for setting tags for title, meta description, robots, canonical links and open graph infos in page headers. +It is also possible to use translation keys here. + +The process is triggered by adding the guard `MetaGuard` to the routing. +This is automatically done for all routes in the SEO module. +The default `MetaSettings` are also configured in this guard. + +`seo.effects.ts` is the central place for customizations concerning dynamic content, e.g. names of products or categories (asynchronous data from the API). +Effects are an essential part of our [State Management](./state-management.md). diff --git a/docs/concepts/software-architecture.md b/docs/concepts/software-architecture.md new file mode 100644 index 0000000000..1caaf2593d --- /dev/null +++ b/docs/concepts/software-architecture.md @@ -0,0 +1,32 @@ + + +# Software Architecture + +This concept introduces some decisions made from an architectural point of view. + +The Intershop Progressive Web App is a REST API based storefront client that works on top of the Intershop Commerce Management server version 7.10. +This means that the communication between the Angular based storefront and Intershop Commerce Management only functions via REST. +Customizations should fit into that REST based pattern as well. + +## Overview + +Please refer to [Angular - Architecture Overview](https://angular.io/guide/architecture) for an in-depth overview of how an Angular application is structured and composed. +In short, an Angular application consists of templates, components and services. +Templates contain the HTML that is rendered for the browser and displays the UI. +Services implement business functionality using [TypeScript](https://en.wikipedia.org/wiki/TypeScript). +Components are small and (mostly) independent bridges between services and templates that prepare data for display in templates and collect input from the user to interact with services. +Data binding links the template with methods and properties from the component. + +Services should have a single responsibility by encapsulating all functionality required in it. +The API to a service should be as narrow as possible because services are used throughout the application. +It is also possible to combine functionality of multiple services in another more general service if necessary. + +The components handling the templates should only handle view logic and should not implement too much specific functionalities. +If a component does more than just providing data for the template, it might be better to transfer this to service instances instead. + +Multiple components and their respective templates are then composed to pages. diff --git a/docs/concepts/state-management.md b/docs/concepts/state-management.md new file mode 100644 index 0000000000..13faa399ea --- /dev/null +++ b/docs/concepts/state-management.md @@ -0,0 +1,194 @@ + + +# State Management + +This concept describes how [NgRx](https://ngrx.io/) is integrated into the Intershop Progressive Web App for the application wide state management. + +## Architecture + +![State Management](state-management.svg 'State Management') + +NgRx is a framework for handling state information in Angular applications following the Redux pattern. +It consist of a few basic parts: + +### State + +The state is seen as the single source of truth for getting information of the current application state. +There is only one immutable state per application, which is composed of substates. +To get information out of the state, selectors have to be used. +Changing the state can only be done by dispatching actions. + +### Selectors + +Selectors are functions used to retrieve information about the current state from the store. +The selectors are grouped in a separate file. +They always start the query from the root of the state tree and navigate to the required information. +Selectors return observables which can be held in containers and be bound to in templates. + +### Actions + +Actions are simple objects used to alter the current state via reducers or trigger effects. +Action creators are held in a separate file. +The action class contains a type of the action and an optional payload. +To alter the state synchronously, reducers have to be composed. +To alter the state asynchronously, effects are used. + +### Reducers + +Reducers are pure functions which alter the state synchronously. +They take the previous state and an incoming action to compose a new state. +This state is then published and all listening components react automatically to the new state information. +Reducers should be simple operations which are easily testable. + +### Effects + +Effects use incoming actions to trigger asynchronous tasks like querying REST resources. +After successful or erroneous completion, an effect might trigger another action as a way to alter the current state of the application. + +### Facades + +Facades are injectable instances which provide simplified access to the store via exposed observables and action dispatcher methods. +They should be used in Angular components but not within NgRx artifacts themselves. + +## File Structure + +The file structure looks like this: + +```txt +src/app/core + ├─ facades + | └─ foobar.facade.ts + └─ store + └─ foobar + ├─ foo + | ├─ foo.actions.ts + | ├─ foo.effects.ts + | ├─ foo.reducer.ts + | ├─ foo.selectors.ts + | └─ index.ts + ├─ bar + | └─ ... + ├─ foobar.state.ts + └─ foobar.system.ts +``` + +An application module named `foobar` with substates named `foo` and `bar` serves as an example. +The files handling NgRx store should then be contained in the folder `foobar`. +Each substate should aggregate its store components in separate subfolders correspondingly named `foo` and `bar`: + +- _foo.actions.ts_: This file contains all action creators for the `foo` state. Additionally, a bundle type aggregating all action creators and an enum type with all action types is contained here. + +- _foo.effects.ts_: This file defines an effect class with all its containing effect implementations for the `FooState`. + +- _foo.reducer.ts_: This file exports a reducer function which modifies the state of `foo`. Additionally, the `FooState` and its `initialState` is contained here. + +- _foo.selectors.ts_: This file exports all selectors working on the state of `foo`. + +- _index.ts_: This file exports the public API for the state of the `foo` substate. This includes all specific selectors and actions. + +Furthermore, the state of foobar is aggregated in two files: + +- _foobar.state.ts_: Contains the `FoobarState` as an aggregate of the `foo` and `bar` states. + +- _foobar.system.ts_: Contains aggregations for `foobarReducers` and `foobarEffects` of the corresponding substates to be used in modules and `TestBed` declarations. + +Access to the state slice of `foobar` is provided with the `FoobarFacade` located in _foobar.facade.ts_ + +## Naming + +Related to the example in the previous paragraph, we want to establish a particular naming scheme. + +### Actions - Types + +Action types should be aggregated in an enum type. +The enum should be composed of the substate name and 'ActionTypes'. +The key of the type should be written in PascalCase. +The string value of the type should contain the feature in brackets and a readable action description. +The description should give hints about the dispatcher of the said action, i.e., actions dispatched due to a HTTP service response should have 'API' in their name, actions dispatched by other actions should have 'Internal' in their description. + +```typescript +export enum FooActionTypes { + LoadFoo = '[Foo Internal] Load Foo', + InsertFoo = '[Foo] Insert Foo', + LoadFooSuccess = '[Foo API] Load Foo Success', + ... +} +``` + +### Actions - Creators + +The action creator is a class with an optional payload member. +Its PascalCase name should correspond to an action type. +The name should not contain 'Action' as the action is always dispatched via the store and it is therefore implicitly correctly named. + +```typescript +export class LoadFoo implements Action { + readonly type = FooActionTypes.LoadFoo; + constructor(public payload: string) {} +} +``` + +### Actions - Bundle + +The file _actions.ts_ should also contain an action bundle type with the name of the substate + 'Action', which is to be used in the reducer and tests. + +```typescript +export type FooAction = LoadFoo | SaveFoo | ... +``` + +### Reducer + +The exported function for the reducer should be named like the substate + 'Reducer' in camelCase. + +```typescript +export function fooReducer(state = initialState, action: FooAction): FooState { +``` + +### State + +State interfaces should have the state name followed by 'State' in PascalCase. + +```typescript +export interface FooState { +... +``` + +### Selectors + +Selectors should always be camelCase and start with 'get' or 'is'. + +```typescript +export const getSelectedFoo = createSelector( ... +``` + +### Facades - Streams + +Any field ending with \$ indicates that a stream is supplied. i.e. `foos$()`, `bars$`, `foo$(id)`. +The facade takes care that the stream will be loaded or initialized. +The naming should just refer to the object itself without any verbs. + +### Facades - Action Dispatchers + +Action dispatcher helpers are represented by methods with verbs. i.e. `addFoo(foo)`, `deleteBar(id)`, `clearFoos()`. + +## Entity State Adapter for Managing Record Collections: @ngrx/entity + +[@ngrx/entity](https://ngrx.io/guide/entity) provides an API to manipulate and query entity collections. + +- Reduces boilerplate for creating reducers that manage a collection of models. +- Provides performant CRUD operations for managing entity collections. +- Extensible type-safe adapters for selecting entity information. + +## Normalized State + +It is important to have a normalized state when working with NgRx. +To give an example, only the product's state should save products. +Every other slice of the state that also uses products must only save identifiers (in this case SKUs) for products. +In selectors, the data can be linked to views to be easily usable by components. + +see: [NgRx: Normalizing state](https://medium.com/@timdeschryver/ngrx-normalizing-state-d3960a86a3aa) diff --git a/docs/state-management.svg b/docs/concepts/state-management.svg similarity index 100% rename from docs/state-management.svg rename to docs/concepts/state-management.svg diff --git a/docs/concepts/styling-behavior.md b/docs/concepts/styling-behavior.md new file mode 100644 index 0000000000..72c0548882 --- /dev/null +++ b/docs/concepts/styling-behavior.md @@ -0,0 +1,39 @@ + + +# Styling & Behavior + +The visual design (styling) and the interaction design (behavior) of the Intershop Progressive Web App is derived from the Responsive Starter Store with some changes (e.g., the header) to improve and modernize the customer experience and to provide an easy optical distinction between the two Intershop storefronts. +While the Responsive Starter Store is based on a customized/themed [Bootstrap 3](https://getbootstrap.com/docs/3.3/), the Intershop Progressive Web App styling was migrated to build upon the current version of [Bootstrap 4](https://getbootstrap.com/). +This also means that the Intershop Progressive Web App styling is now based on [Sass](http://sass-lang.com/). + +## Bootstrap Integration + +The styling integration is configured in the _/src/themes/main.scss_ of the project where Bootstrap together with the customizations is configured. + +Instead of the Bootstrap 3 Glyphicons, the current styling uses free solid icons of [Font Awesome](https://fontawesome.com/). + +The styling itself is integrated into the project as global style via a _style.scss_ that is referenced in the _angular.json_ and is compiled automatically (see also [Chapter Multitheming](#multitheming)). +Throughout the whole Intershop Progressive Web App, there are almost no component specific `styleUrls` or `styles` properties. + +The [Javascript part of Bootstrap](http://getbootstrap.com/javascript/) for the behavior is not directly used from the Bootstrap dependency since this implementation is jQuery based and not really suited to work in an Angular environment. +For Bootstrap 4, [ng-bootstrap](https://ng-bootstrap.github.io) provides _Bootstrap widgets the angular way_. +Using these components works best with the styling taken from the Responsive Starter Store. +However, the generation and structure of the HTML for the Angular Bootstrap differs from the HTML working with the original jQuery based _bootstrap.js_. +Adaptions and changes in this area are inevitable. + +## Assets + +The assets folder is the place for any static resources like images, colors, etc., that are used by the storefront styling. + +## Fonts + +Currently the default font families for the Intershop Progressive Web App [Roboto](https://www.google.com/fonts/specimen/Roboto) and [Roboto Condensed](https://www.google.com/fonts/specimen/Roboto+Condensed) are defined as npm dependency. + +## References + +[Guide - Multiple Themes](../guides/multiple-themes.md) diff --git a/docs/concepts/testing-test-pyramid.jpg b/docs/concepts/testing-test-pyramid.jpg new file mode 100644 index 0000000000000000000000000000000000000000..496a96716d450eb14d7348f6c35b3a960dc06f18 GIT binary patch literal 23759 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 + +# Testing the PWA + +## Different Levels of Testing + +Testing the PWA follows the basic principle of the test pyramid ([https://martinfowler.com/bliki/TestPyramid.html](https://martinfowler.com/bliki/TestPyramid.html)). + +![Test Pyramid](testing-test-pyramid.jpg) + +### Unit + +Most of the testing should be done in low-level unit tests if possible. +These tests concern themselves with the behavior of a single unit of code, be it a function, a class or even an Angular component with HTML rendering. +All dependencies of that unit shall be mocked out to keep the scope as small as possible. +These tests are written in a [Jasmine](https://jasmine.github.io/)\-like style and executed with [Jest](https://facebook.github.io/jest/). +Running these tests can be very time-efficient and they serve as the primal short-circuit response to developers. + +### Module + +Following unit tests we also run module tests which serve as the first layer of integration tests. +With these tests more dependencies are instantiated in every single test to check the behavior when interconnecting more components. +Examples for these are testing a slice of the ngrx store or checking form validation with multiple Angular components. +They are also implemented using [Jest](https://facebook.github.io/jest/). + +### Integration + +The next layer of tests are integration tests which run the application as a whole but mock out ICM rest responses. +The test is run using a browser and performing various actions and checks on the application. +For this kind of test [Cypress](https://www.cypress.io/) is required. +The tests are written in a Jasmine-like behavior driven style. +For the ease of readability we implemented them using a _PageObject_ pattern, see [https://martinfowler.com/bliki/PageObject.html](https://martinfowler.com/bliki/PageObject.html). +Testing in this stage is of course more time-consuming as the application has to be compiled and started up as a whole. +Mocking server responses also limits the available workflows of the application. +For example designing mock-data for a complete customer journey through the checkout would be too complex and too brittle. +Nevertheless, the tests serve well for a basic overview of some functionality. + +### End-to-End + +The most time-consuming tests are complete end-to-end tests. +They do not mock out anything and run the PWA against an ICM with a deployed `a_responsive:inspired-b2x`. +We also use [Cypress](https://www.cypress.io/) here. +Additionally, all tests from the previous integration step should be composed in a way that they can also be run with real REST responses. +As a basic rule of thumb we only test happy path functionality or workflows that would be too cumbersome to be mocked in module tests. + +## Test File Locations + +Unit and module tests are closely located next to the production source code in the _src_ folder. + +Integration and end-to-end tests currently reside in _cypress/integration/specs_. _PageObjects_ are located in _cypress/integration/pages_. +If the filename of a spec contains the string `mock`, it is run as an integration test with mocked server API. +If it (additionally) contains `b2c` or `b2b` the test also should run on the PWA set up with the corresponding channel. + +## Deviation from Standard Angular Test Frameworks + +By default Angular projects are setup with [Jasmine](https://jasmine.github.io/) and Karma Runner for unit and module tests, as well as [Protractor](https://www.protractortest.org) for end-to-end testing. +We decided to deviate from these frameworks, because there are better alternatives available. + +[Jest](https://facebook.github.io/jest/) provides a better and faster experience when testing. +Jest uses a JavaScript engine comparable to a virtual browser. +There is no need to start up a real browser like it is standard with Jasmine and Karma. +Jest also provides an interactive command line interface with many options. +Integrations for VSCode are available that will ease developing and running tests. +Another big advantage of Jest is the functionality for [Snapshot Testing](https://jestjs.io/docs/en/snapshot-testing). + +We also do not use Protractor for end-to-end testing. +Like all Selenium-based testing frameworks, Protractor deals with the same problems. +Special bindings for the programming language communicate via HTTP with a browser driver which then remotely controls the browser over HTTP. +This indirect way is very fragile against network latency. +Also the functionalities are limited. +Protractor, however, is especially designed for Angular, so it automatically waits for background tasks to finish before continuing the test run. +This functionality must be implemented when using Cypress. + +Cypress uses a different approach for running end-to-end tests. +It runs directly in the browser and thus provides access to application internals like XHR-Monitoring, access to LocalStorage and so on. +The interface also provides page snapshots for debugging, which in turn eases the experience when writing tests or reconstructing bugs. +We use Cypress with a _PageObject_ pattern. + +## PageObject Pattern + +As mentioned earlier, we divide end-to-end tests in _PageObjects_ and Specs. _PageObjects_ abstract the HTML of the pages and offer a human-readable access possibility for the implementation of test-describing business processes. +In that way _PageObjects_ also make certain routines re-usable over all tests. +Specs use these provided _PageObject_ functions to make assertions and trigger actions. _PageObjects_ themselves should not make assertions. +All intended assertions should only be made in Specs. +Specs are the main entry point for developers. +All test-related data and intended behavior should only be available in each single file. + +No form of abstraction shall be made when developing tests, especially not for re-using code. +Prefer composition and introduce action methods in _PageObjects_ instead. + +## Handling Test Data + +For unit and module tests test data is instantiated as required. +Each test should only set fields actually required for each test to ease readability. +If new dependencies are introduced or workflows change, the corresponding test cases have to change, too. + +Integration and end-to-end tests are tailored for the inSPIRED (_a_responsive_) demo store. +Used test data can be abstracted at the start of the file but at all times it should only be accumulated here to ease readability of these test cases. +Further abstraction would lead to longer development cycles as it is harder to understand functionality of test cases if it is distributed among multiple files. + +If the supplied test cases should be reused for projects, the test data has to be adapted. + +The end-to-end tests have to be adapted as well. +Styling and structural changes have to be handled in the _PageObjects_, which are then used across all Specs. +If behavior of the customization differs from the blueprint store, the Specs have to be adapted as well. diff --git a/docs/concepts/url-rewriting.md b/docs/concepts/url-rewriting.md new file mode 100644 index 0000000000..a3cae9d143 --- /dev/null +++ b/docs/concepts/url-rewriting.md @@ -0,0 +1,39 @@ + + +# URL Rewriting + +The PWA allows to supply localized and SEO optimized URLs for categories and product detail pages. + +## Rewriting Concept + +This feature is mainly built on top of Angular's [UrlMatcher] for parsing routes and a custom pipe for generating them. + +## Rewriting Artifacts + +Artifacts for custom routes are accumulated in _src/app/core/routing_. + +### matchXRoute + +Each route must supply an implementation of [UrlMatcher] to be used in the routing module. +We recommend defining custom routes in _src/app/pages/app-last-routing.module.ts_, so they do not interfere with other routes. +The easiest approach for implementation would be to build on top of `UrlSegment`, but it is also possible to implement a custom solution with regular expressions. + +### XRoutePipe + +It is also important to generate the customized routes depending on entities or other configuration. +It is helpful to implement an exported `generateXRoute` helper function, that transforms said entity to a URL. + +### ofXRoute (optional) + +To detect routes in NgRx Effects, we recommend to also supply an operator, that filters for URLs of customized routes using the `RouteNavigation` as an input. + +# Further References + +- [URLMatcher Angular Documentation][urlmatcher] + +[urlmatcher]: https://angular.io/api/router/UrlMatcher diff --git a/docs/customizations.md b/docs/customizations.md deleted file mode 100644 index 6b5c21e4df..0000000000 --- a/docs/customizations.md +++ /dev/null @@ -1,111 +0,0 @@ - - -# Customization Guide - -When customizing the PWA, always keep in mind that you probably want to upgrade from time to time and will have to merge incoming changes into your codebase. We therefore suggest that you do not edit existing files all too much and keep them as intact as possible. In this document we provide a couple of rules that you should follow. Generally speaking: - -> Keep modifications on existing files as minimal as possible! - -It can be tempting to always modify existing templates, component and style files inline when doing customization. However, when merging incoming upgrades the number of merge conflicts can possibly be large. So if you want to upgrade to new PWA versions later, stick to the following recommendations. - -## Start Customization - -To start customizing, **set a prefix** for your custom components with the script `node schematics/customization/add `. After that we recommend to additionally use the prefix in every component to further help identifying customized components. - -```bash -$ node schematics/customization/add custom -$ ng g c shared/components/basket/custom-basket-display -CREATE src/app/shared/components/basket/custom-basket-display/custom-basket-display.component.ts (275 bytes) -... -``` - -The script also creates an **additional theme** under `src/styles/themes`. Here you can adjust colors and add overrides to the existing styling. - -## Import Changes from New Release - -> Prior to 0.16.1 the entire git history changed completely. Please see https://github.com/intershop/intershop-pwa/issues/62 for suggestions on importing the new history. - -Importing changes of new releases is done with `git` tooling. If you stick to the guidelines in this chapter, the problems arising in the process of updating will not be as impacting as you might fear. Also remember to `npm install` after you are importing a change that modified the `package-lock.json` and run tests and linting in the process. - -For importing changes from the current Release you can use different approaches: - -### 1. Merge the New Release in Its Entirety - -This way your development will not deviate that much from the current PWA development, but you will have to resolve all appearing conflicts at once. - -Just add the Intershop PWA GitHub repository as a second remote in your project and `git merge` the release branch. - -### 2. Cherry-Pick Individual Changes From the New Release - -By cherry-picking changes individually you can decide to skip certain changes and you will get smaller amounts of merge conflicts. However, that way the histories will deviate more and more over time. - -To execute this, add the Intershop PWA GitHub repository as a second remote in your project and `git cherry-pick` the range of commits for this release. - -### 3. Rebase Your Changes Onto the New Release - -This way your project code will always be up-to-date with the current Intershop PWA history, as this history will stay the base of the project over all releases. - -To perform this update, use `git rebase --onto ` on your project's main branch. Your running feature branches must then be rebased the same way onto the new development branch. - -As it may be the best way to keep the original history intact, the upgrade process can be quite challenging. By rebasing every single one of your customizations commits, every commit is virtually re-applied onto the current PWA history. You can imagine it as pretending to start the custom project anew onto the current version. If your project's history is clean and every commit is well-described and concise, this might be a way to go. - -## Specific Concerns - -### Components - -When **adding new functionality**, it is better to encapsulate it in **new components** that handle every aspect of the customization and just **use them in existing templates**. That way the modifications on existing code are most often kept to a single line change only. - -When **heavily customizing** existing components it is better to **copy components** and change all references. If 20 % of the component has to be changed, it is already a good idea to duplicate it. That way incoming changes will not affect your customizations. Typical hot-spots where copying is a good idea are header-related or product-detail-page related customizations. - -We are supplying a schematic `customized-copy` for copying components and replacing all usages. - -```bash -$ node schematics/customization/add custom -$ ng g customized-copy shared/components/product/product-price -CREATE src/app/shared/components/product/custom-product-price/custom-product-price.component.html (1591 bytes) -CREATE src/app/shared/components/product/custom-product-price/custom-product-price.component.spec.ts (7632 bytes) -CREATE src/app/shared/components/product/custom-product-price/custom-product-price.component.ts (1370 bytes) -UPDATE src/app/shared/shared.module.ts (12676 bytes) -UPDATE src/app/shared/components/product/product-row/product-row.component.html (4110 bytes) -UPDATE src/app/shared/components/product/product-row/product-row.component.spec.ts (5038 bytes) -UPDATE src/app/shared/components/product/product-tile/product-tile.component.html (2140 bytes) -UPDATE src/app/shared/components/product/product-tile/product-tile.component.spec.ts (4223 bytes) -... -``` - -### Existing Features - -When you want to **disable code** provided by Intershop, it is better to **comment it out instead of deleting** it. This allows Git to merge changes more predictably since original and incoming passages are still similar to each other. Commenting out should be done in the form of block comments starting a line above and ending in an additional line below the code. Use `` for HTML and `/*` and `*/` for SCSS and TypeScript. - -Some of the provided components supply **configuration parameters** which can be used to handle customization in an easy way (i.e. disabling features or switching between them). - -### New Features - -When adding new independent features to the PWA, it might be a good idea to **add an extension** first. Use the provided schematics `ng g extension ` to scaffold the extension. Adding all related pages, components, models and services here is less intrusive than adding them to the existing folder structure. Add additional artifacts to extensions by supplying the `--extension` flag to schematics calls. - -### Data - -When **adding new fields** to PWA data models, add them to interfaces and **map them as early as possible** in mapper classes to model classes. That way the data can be readily used on templates. Improving and parsing improper data too late could lead to more modifications on components and templates which will be harder to upgrade later on. - -### NgRx - -Adding **new data** to the state should always almost exclusively be done by adding new stores in **store groups**. Add one with `ng g store-group ` and then add consecutive stores with `ng g store --feature `. Keep modifications to the existing store as little as possible. As NgRx is loosely coupled by nature, you can deactivate effects by simply commenting out the `@Effect` decorator. - -### Testing - -When modifying components it is most likely that related test cases will fail. If possible, use the Jest update feature **update snapshots** when adapting test cases. When you upgrade the PWA to a new version, those snapshots will most likely have merge conflicts in them. Here you can just accept either modification and update the test snapshots. - -### Styling - -Changing the styling of **existing components** should be done by adding overrides in the custom theme folder under `src/styles/themes`. You can also change relevant information in the global style files under `src/styles`. If too many changes have to be made, it is better to **add the styling in additional files on global or component level**. - -When styling is done on component level, all styling is encapsulated to exactly this component (default behavior). You can re-use variables from global styling on component level by adding imports like `@import '~theme/variables.scss';`. - -### Dependencies - -When updating dependencies and resolving conflicts inside of `package-lock.json`, always **accept Intershop's changes** first. After that run `npm install` to regenerate the file. diff --git a/docs/guides/angular-change-detection.md b/docs/guides/angular-change-detection.md new file mode 100644 index 0000000000..e0107dce30 --- /dev/null +++ b/docs/guides/angular-change-detection.md @@ -0,0 +1,95 @@ + + +# Angular Change Detection + +Change detection is on of the core concepts of Angular. +Component templates contain data bindings that embed data from the component class into the view. +The change detection cycle keeps view and data in sync. +To do so, Angular re-evaluates all data expressions from the templates every time the CD runs. +If the newly returned value differs from the current value, the corresponding DOM element will be updated in the view. +This way, the template stays synchronized with the underlying data. + +## Zones + +Change detection needs to be triggered whenever a potential change happens. +Data changes are most likely invoked by async events (timeout/interval or XHR) or user events (click, input, …). +Running CD whenever such an event occurs is – in most cases – enough for establishing a solid view update mechanism. + +To access those events, Angular uses the concept of zones. +In short words, a zone is an asynchronous execution context that keeps track of all events happening and reports their status to our program. +See also: [Using Zones in Angular for better performance](https://blog.thoughtram.io/angular/2017/02/21/using-zones-in-angular-for-better-performance.html) + +Zones wrap asynchronous browser APIs, and notify a consumer when an asynchronous task has started or ended. +Angular takes advantage of these APIs to get notified when any asynchronous task is done. +This includes things like XHR calls, `setTimeout()` and pretty much all user events like `click`, `submit`, `mousedown`, … etc. + +When async events happen in the application, the zone informs Angular which then triggers change detection. + +## Zone Stability + +The zone tracks all ongoing async events and does also know whether there are pending tasks in the queue. +If so (e.g., a running timer or XHR) it is likely that some change will happen in near future. +This makes the zone **unstable**. +Once all async tasks have been finished, the zone enters the **stable** status. + +- The zone will never become stable with an endless interval running in the application. + +- A **zone is stable** when all pending async tasks have been finished. + +The stability of the Angular zone can be used to make decisions in the code. `ApplicationRef.isStable` exposes an Observable stream which gives information about whether the zone is stable or not. + +```typescript +import { ApplicationRef } from '@angular/core'; + +@Component({ + /* ... */ +}) +export class MyComponent { + constructor(private appRef: ApplicationRef) { + appRef.isStable.subscribe(stable => { + if (stable) { + console.log('Zone is stable'); + } + }); + } +} +``` + +## Using Zone Stability + +Getting to know whether a zone is stable or not is crucial when programmatically accessing the application from the "outside". +Having a stable zone means that Angular has finished rendering and that we do not expect any async tasks to finish in near future. +The Intershop PWA effectively uses this concept for communication with the CMS Design View. +Also, Angular waits for stability in Service Workers and in Universal Rendering (Server-Side Rendering). + +### Design View Communication + +The Design View, which is part of the ICM backoffice, displays the Intershop PWA in an iframe. +It analyzes the component structure of the rendered page and displays it as a tree so that users can edit the structure of the CMS components. + +In order to be able to analyze the structure, the application needs to wait for the rendering to be finished – using `isStable`. +After each router navigation, the app waits for the zone to become stable before the component tree will be analyzed. +The corresponding code can be found in the `SfeAdapterService` class. + +### Service Workers and Universal + +Both `@angular/service-worker` and `@angular/platform-server` use zone stability information internally. +The Service Worker will not be attached to the page before the zone has become stable. +The same applies for server-side rendering: The page will be rendered as soon as the zone is stable. +This is necessary because data from HTTP requests must be resolved to render meaningful content. + +### Pitfall: The Zone Must Become Stable + +For all of those aspects – Design View, Service Workers and Universal rendering – it is essential to get a stable zone at some point. +If not, those aspects will not work properly, e.g. +Universal rendering will never return the rendered HTML and the Design View will never render the component tree view. + +> **Note** +> Avoid long-running timers and intervals. If this is unavoidable, make sure the async tasks do not start before the zone has become stable once. + +If you have any intervals in the application, wait for zone stability first before starting them. diff --git a/docs/guides/angular-component-development.md b/docs/guides/angular-component-development.md new file mode 100644 index 0000000000..b729c75acc --- /dev/null +++ b/docs/guides/angular-component-development.md @@ -0,0 +1,128 @@ + + +# Angular Component Development + +## Declare Components in the Right NgModule + +Angular requires you to declare a component in one and only one NgModule. +Find the right one in the following order: + +_Your Component is used only on one page?_ - Add it to the declarations of the corresponding page.module. + +_Your Component is used among multiple pages?_ - Declare it in the shared.module and also export it there. + +_Your Component is used in the application shell (and maybe again on certain pages)?_ - Declare it in the shell.module and also export it there. + +_(advanced) Your component relates to a specific B2B extension?_ - Declare it in that extension module and add it as an entryComponent, add a lazy-loaded component and add that to the extension exports, which are then im-/exported in the shared.module. + +When using `ng generate`, the right module should be found automatically. + +## Do not use NgRx or Services in Components + +Using NgRx or Services directly in components violates our model of abstraction. +Only facades should be used in components, as they provide the simplest access to the business logic. + +## Delegate Complex Component Logic to Services + +There should not be any string or URL manipulation, routing mapping or REST endpoint string handling within components. +This is supposed to be handled by methods of services. +See also [Angular Style Guide](https://angular.io/guide/styleguide#style-05-15). + +## Put as Little Logic Into `constructor` as Possible - Use `ngOnInit` + +See [The essential difference between Constructor and ngOnInit in Angular](https://blog.angularindepth.com/the-essential-difference-between-constructor-and-ngoninit-in-angular-c9930c209a42) and [Angular constructor versus ngOnInit](https://ultimatecourses.com/blog/angular-constructor-ngoninit-lifecycle-hook). + +## Use Property Binding to Bind Dynamic Values to Attributes or Properties + +See [Explanation of the difference between an HTML attribute and a DOM property](https://angular.io/guide/template-syntax#html-attribute-vs-dom-property). + +There are often two ways to bind values dynamically to attributes or properties: interpolation or property binding. +In the PWA we prefer using property binding since this covers more cases in the same way. +So the code will be more consistent. + +There is an exception for direct string value bindings where we use for example `routerLink="/logout"` instead of `[routerLink]="'/logout'"`. + +:warning: **Pattern to avoid** + +```html +
+ +
+``` + +:heavy_check_mark: **Correct pattern** + +```html +
+ +
+``` + +## Pattern for Conditions (ngif) with Alternative Template (else) in Component Templates + +Also for consistency reasons, we want to establish the following pattern for conditions in component templates: + +:warning: **Condition in template** + +```typescript + + ... (template code for if-branch) + + + + ... (template code for else-branch) + +``` + +This pattern provides the needed flexibility if used together with handling observables with `*ngIf` and the `async` pipe. +In this case the condition should look like this: + +```typescript + +``` + +## Do Not Unsubscribe, Use Destroy Observable and takeUntil Instead + +Following the ideas of the article [RxJS: Don’t Unsubscribe](https://medium.com/@benlesh/rxjs-dont-unsubscribe-6753ed4fda87), the following pattern is used for ending subscriptions to observables that are not handled via async pipe in the templates. + +:heavy_check_mark: **'unsubscribe' via destroy\$ Subject** + +```typescript +export class AnyComponent implements OnInit, OnDestroy { + ... + private destroy$ = new Subject(); + ... + ngOnInit() { + ... + observable$.pipe(takeUntil(this.destroy$)) + .subscribe(/* ... */); + } + ... + ngOnDestroy() { + this.destroy$.next(); + } +} +``` + +## Use `OnPush` Change Detection if Possible + +To reduce the number of ChangeDetection computation cycles, all components should have their `Component` decorator property `changeDetection` set to `ChangeDetectionStrategy.OnPush`. + +## Split Components When Necessary + +Consider splitting one into multiple components when: + +- **Size**: Component code becomes too complex to be easily understandable + +- **Separation of concerns**: A component serves different concerns that should be separated + +- **Reusability**: A component should be reused in different contexts. This can introduce a shared component which could be placed in a shared module. + +- **Async data**: Component relies on async data from the store which makes the component code unnecessarily complex. Use a container component then which resolves the observables at the outside of the child component and passes data in via property bindings. Do not do this for simple cases. + +Single-use dumb components are always okay if it improves readability. diff --git a/docs/guides/code-documentation.md b/docs/guides/code-documentation.md new file mode 100644 index 0000000000..3bf6fbd0a7 --- /dev/null +++ b/docs/guides/code-documentation.md @@ -0,0 +1,151 @@ + + +# Code Documentation + +For our Intershop Progressive Web App, [Compodoc](https://compodoc.app) is used as documentation package. + +For documentation, the _tsconfig.app.json_ file is used as configuration file. +The output folder for the documentation is set to _/docs/compodoc_. + +We use an own styling theme based on the theme '_readthedocs_' provided by Compodoc. +The _style.css_ file of the theme can be found in _/docs/theme_. + +Examples for the comment styling pattern can be found here: [TypeDoc - DocComments](http://typedoc.org/guides/doccomments/). + +## Usage + +### Generate Code Documentation + +**Generate Code Documentation** + +```bash +npm run docs +``` + +The generated documentation can be called by _/docs/compodoc/index.html_. + +### Serve Generated Documentation with Compodoc + +**Serve Generated Documentation with Compodoc** + +```bash +npm run docs:serve +``` + +Documentation is generated at _/docs/compodoc_ (output folder). +The local HTTP server is launched at _http://localhost:8080_. + +**Watch Source Files After Serve and Force Documentation Rebuild** + +```bash +npm run docs:watch +``` + +## Comments + +### General Information + +The JSDoc comment format is used for general information. + +Use this format to describe components, modules, etc., but also methods, inputs, variables and so on. + +**Example for General Description** + +```typescript +/** + * The Product Images Component + */ +``` + +### JSDoc Tags + +Currently Compodoc supports the following JSDoc tags : + +- `@returns` +- `@param` +- `@link` +- `@example` + +**Example for parameter and return values** + +```typescript +/** + * Get products for a given search term respecting pagination. + * @param searchTerm The search term to look for matching products. + * @param page The page to request (1-based numbering) + * @param sortKey The sortKey to sort the list, default value is ''. + * @returns A list of matching Product stubs with a list of possible sortings and the total amount of results. + */ +searchProducts( + searchTerm: string, + page: number, + sortKey?: string +): Observable<{ products: Product[]; sortKeys: string[]; total: number }> { +``` + +**Example for links and implementation examples** + +```typescript +/** + * The Product Images Component displays carousel slides for all images of the product and a thumbnails list as carousel indicator. + * It uses the {@link ProductImageComponent} for the rendering of product images. + * + * @example + * + */ +``` + +> :warning: **Indentation Warning** +> TypeScript has an internal margin for new lines. If you want to keep a level of indentation, put a minimum of 13 space characters as shown in the example: + +**Example with Indentation Keeping** + +```typescript +/** + * @example + *
+ * + * + *
+ */ +``` + +> New lines are created inside a comment with a blank line between two lines: + +**Comments with New Lines** + +```typescript +/** + * First line + * + * Second line + */ + +/** + * First line + * Behind first line, produces only one line + */ +``` + +## :warning: Document only if documentation is needed! + +This is not a project with obligatory documentation, so: Do not document the obvious! For example, if behavior and requirements can be implied by a method signature, no additional documentation is needed. +Instead **pay attention to useful names**. +If you cannot find a pricise name for your variable or method, maybe this is a sign that too much is done here and it would be better to **refactor instead**. + +However, there are some cases where documentation is recommended: + +- Technical background required, + +- Restraints on method arguments, that cannot be inferred by the method signature alone, + +- Tricky parts where some degree of magic is happening (especially useful as in-line documentation), + +- Class documentation for exported shared components. diff --git a/docs/guides/continuous-integration.md b/docs/guides/continuous-integration.md new file mode 100644 index 0000000000..98f4041350 --- /dev/null +++ b/docs/guides/continuous-integration.md @@ -0,0 +1,78 @@ + + +# Continuous Integration + +This section provides an overview of required continuous integration steps to verify the validity of code contributions. + +> All mentioned tools provide feedback on success or failure via exit code. + +## Code Integrity + +Since Angular projects are JavaScript-based, even though they use TypeScript-based code, everything is highly dynamic. +Parts of the software can still run error free with `webpack-dev-server` (`ng serve`), even if other parts were not compiled or have template errors. + +To ensure having a consistent code base, the CI system should always perform at least an ahead-of-time compile step (`ng build --aot`). + +Angular in production mode does AoT and applies some more code optimizations that can sometimes clash with definitions or third-party libraries. +To catch this, a production build should be performed: `ng build --prod`. + +To check the integrity of the unit tests, the TypeScript compiler can be used: `npx tsc -p src/tsconfig.spec.json`. + +## Dependencies + +When using `npm` as manager for third-party libraries, all dependencies get pinned down with exact version numbers and archive digests. +A CI system should check if the current `package.json` corresponds to the checked-in `package-lock.json`. + +This can be done with Git tooling (check for Git changes after `npm install`) or can be done by hashing `package-lock.json` before and after the install step and comparing hash values. + +## Code Formatting + +In larger projects it is beneficial for all users to contribute code in a consistent style. +This reduces the number of conflicts when merging code that was developed in parallel. + +To ensure that contributed code is properly formatted, run the formatter on the CI server with `npm run format` and check for changes with Git tooling or calculate hashes before and after. + +## Unit Testing + +[Jest](https://facebook.github.io/jest/) is used as a test runner. +All tests can and should be run on the CI server with `npm test`. + +Since jest is very flexible in accepting code with compile errors, the code integrity should be checked separately. + +## UI Testing + +UI testing is done with [Cypress](https://www.cypress.io/). +This requires a suitable version of Google Chrome to be installed on the CI worker (or in the Docker image used for the tests). + +Run UI tests interactively with `npm run e2e`. +Before that you have to start up a PWA application. + +## Universal Testing + +Since Angular Universal is used for server-side pre-rendering of content to tackle SEO concerns, the CI system should also check if server-side rendering is still working. +For this purpose, it must be checked whether the server response contains content from lazy-loaded modules, in other words making sure all modules have produced compliant HTML markup. + +This can be done by pointing `curl` to a product detail page and checking if a specific CSS class could be found (via `grep`) in the HTML. +Have a look into `e2e/test-universal.sh` to see how we are doing that. + +## Static Code Analysis + +SCA tools help to improve code quality, which leads to better maintainability and thus to a reduction of technical debts. + +Intershop uses `tslint` for static code analysis. +Run the linting process on the CI with `"npm run lint".` + +If a rule seems too harsh for you, downgrade it to warning level by choosing: + +**tslint.json** + +```typescript +"rule-name": { "severity": "warning" } +``` + +Turn it off completely by using `"rule-name": false`. diff --git a/docs/guides/customizations.md b/docs/guides/customizations.md new file mode 100644 index 0000000000..7e522e4a89 --- /dev/null +++ b/docs/guides/customizations.md @@ -0,0 +1,146 @@ + + +# Customization Guide + +When customizing the PWA, always keep in mind that you probably want to upgrade from time to time and will have to merge incoming changes into your codebase. +We therefore suggest that you do not edit existing files all too much and keep them as intact as possible. +In this document we provide a couple of rules that you should follow. +Generally speaking: + +> Keep modifications on existing files as minimal as possible! + +It can be tempting to always modify existing templates, component and style files inline when doing customization. +However, when merging incoming upgrades the number of merge conflicts can possibly be large. +So if you want to upgrade to new PWA versions later, stick to the following recommendations. + +## Start Customization + +To start customizing, **set a prefix** for your custom components with the script `node schematics/customization/add `. +After that we recommend to additionally use the prefix in every component to further help identifying customized components. + +```bash +$ node schematics/customization/add custom +$ ng g c shared/components/basket/custom-basket-display +CREATE src/app/shared/components/basket/custom-basket-display/custom-basket-display.component.ts (275 bytes) +... +``` + +The script also creates an **additional theme**, see [Guide - Multiple Themes](../guides/multiple-themes.md). + +## Import Changes from New Release + +> Prior to 0.16.1 the entire git history changed completely. Please see [Merging 0.16.1 as 2nd upstream repository: "refusing to merge unrelated histories](https://github.com/intershop/intershop-pwa/issues/62) for suggestions on importing the new history. + +Importing changes of new releases is done with Git tooling. +If you stick to the guidelines in this chapter, the problems arising in the process of updating will not be as impacting as you might fear. +Also remember to use `npm install` after importing a change that modified the `package-lock.json` and run tests and linting in the process. + +For importing changes from the current release you can use different approaches: + +### 1. Merge the New Release in Its Entirety + +This way your development will not deviate that much from the current PWA development, but you will have to resolve all appearing conflicts at once. + +Just add the Intershop PWA GitHub repository as a second remote in your project and `git merge` the release branch. + +### 2. Cherry-Pick Individual Changes From the New Release + +By cherry-picking changes individually you can decide to skip certain changes and you will get smaller amounts of merge conflicts. +However, that way the histories will deviate more and more over time. + +To execute this, add the Intershop PWA GitHub repository as a second remote in your project and `git cherry-pick` the range of commits for this release. + +### 3. Rebase Your Changes Onto the New Release + +This way your project code will always be up-to-date with the current Intershop PWA history, as this history remains the base of the project over all releases. + +To perform this update, use `git rebase --onto ` on your project's main branch. +Your running feature branches must then be rebased the same way onto the new development branch. + +As it may be the best way to keep the original history intact, the upgrade process can be quite challenging. +By rebasing every single one of your customization commits, every commit is virtually re-applied onto the current PWA history. +You can imagine it as pretending to start the custom project anew onto the current version. +If your project's history is clean and every commit is well-described and concise, this might be a way to go. + +## Specific Concerns + +### Components + +When **adding new functionality**, it is better to encapsulate it in **new components** that handle every aspect of the customization and just **use them in existing templates**. +That way the modifications on existing code are most often kept to a single line change only. + +When **heavily customizing** existing components it is better to **copy components** and change all references. +If 20 % of the component has to be changed, it is already a good idea to duplicate it. +That way incoming changes will not affect your customizations. +Typical hot-spots where copying is a good idea are header-related or product-detail-page related customizations. + +We supply a schematic `customized-copy` for copying components and replacing all usages. + +```bash +$ node schematics/customization/add custom +$ ng g customized-copy shared/components/product/product-price +CREATE src/app/shared/components/product/custom-product-price/custom-product-price.component.html (1591 bytes) +CREATE src/app/shared/components/product/custom-product-price/custom-product-price.component.spec.ts (7632 bytes) +CREATE src/app/shared/components/product/custom-product-price/custom-product-price.component.ts (1370 bytes) +UPDATE src/app/shared/shared.module.ts (12676 bytes) +UPDATE src/app/shared/components/product/product-row/product-row.component.html (4110 bytes) +UPDATE src/app/shared/components/product/product-row/product-row.component.spec.ts (5038 bytes) +UPDATE src/app/shared/components/product/product-tile/product-tile.component.html (2140 bytes) +UPDATE src/app/shared/components/product/product-tile/product-tile.component.spec.ts (4223 bytes) +... +``` + +### Existing Features + +When you want to **disable code** provided by Intershop, it is better to **comment it out instead of deleting** it. +This allows Git to merge changes more predictably since original and incoming passages are still similar to each other. +Commenting out should be done in the form of block comments starting a line above and ending in an additional line below the code. +Use `` for HTML and `/*` and `*/` for SCSS and TypeScript. + +Some of the provided components supply **configuration parameters** which can be used to handle customization in an easy way (i.e. disabling features or switching between them). + +### New Features + +When adding new independent features to the PWA, it might be a good idea to **add an extension** first. +Use the provided schematics `ng g extension ` to scaffold the extension. +Adding all related pages, components, models and services here is less intrusive than adding them to the existing folder structure. +Add additional artifacts to extensions by supplying the `--extension` flag to schematics calls. + +### Data + +When **adding new fields** to PWA data models, add them to interfaces and **map them as early as possible** in mapper classes to model classes. +That way the data can be readily used on templates. +Improving and parsing improper data too late could lead to more modifications on components and templates which will be harder to upgrade later on. + +### NgRx + +Adding **new data** to the state should always almost exclusively be done by adding new stores in **store groups**. +Add one with `ng g store-group ` and then add consecutive stores with `ng g store --feature `. +Keep modifications to the existing store as little as possible. +As NgRx is loosely coupled by nature, you can deactivate effects by simply commenting out the `@Effect` decorator. + +### Testing + +When modifying components it is most likely that related test cases will fail. +If possible, use the Jest update feature **update snapshots** when adapting test cases. +When you upgrade the PWA to a new version, those snapshots will most likely have merge conflicts in them. +Here you can just accept either modification and update the test snapshots. + +### Styling + +Changing the styling of **existing components** should be done by adding overrides in the custom theme folder under `src/styles/themes`. +You can also change relevant information in the global style files under `src/styles`. +If too many changes have to be made, it is better to **add the styling in additional files on global or component level**. + +When styling is done on component level, all styling is encapsulated to exactly this component (default behavior). +You can re-use variables from global styling on component level by adding imports like `@import '~theme/variables.scss';`. + +### Dependencies + +When updating dependencies and resolving conflicts inside of `package-lock.json`, always **accept Intershop's changes** first. +After that run `npm install` to regenerate the file. diff --git a/docs/guides/data-handling-with-mappers.md b/docs/guides/data-handling-with-mappers.md new file mode 100644 index 0000000000..952a5a5bf8 --- /dev/null +++ b/docs/guides/data-handling-with-mappers.md @@ -0,0 +1,62 @@ + + +# Data Handling with Mappers + +The data models for server-side and client-side have to be separated, because the data sent by the server may change over iterations or may not be in the right format, while the client side shop data handling should be stable for a long time. +Therefore, each service communicating with the Intershop REST API should only respond with mapped PWA models. + +The format of raw data from the server is defined by an interface (_.interface.ts_) and mapped to a type used in the Angular application (_.model.ts_). +Both files have to be close together so they share a parent directory in _src/core/models_. +Next to them is a _.mapper.ts_ to map the raw type to the other. + +**category.interface.ts** + +```typescript +export interface CategoryData { + id: string; + name: string; + raw: string; +} +``` + +**category.model.ts** + +```typescript +export class Category { + id: string; + name: string; + transformed: number; +} +``` + +**category.mapper.ts** + +```typescript +@Injectable({ providedIn: 'root' }) +export class CategoryMapper { + fromData(categoryData: CategoryData): Category { + const category: Category = { + id: categoryData.id, + name: categoryData.id, + transformed: CategoryHelper.transform(categoryData.raw), + }; + return category; + } + + fromObject(category: Category): CategoryData { + const categoryData: CategoryData = { + id: category.id, + name: category.id, + raw: CategoryHelper.raw(categoryData.transformed), + }; + return categoryData; + } +} +``` + +A _.helper.ts_ can be introduced to provide utility functions for the model. diff --git a/docs/guides/development.md b/docs/guides/development.md new file mode 100644 index 0000000000..855fd9be48 --- /dev/null +++ b/docs/guides/development.md @@ -0,0 +1,121 @@ + + +# Development Environment + +Developing with the Intershop PWA requires to download and install [Node.js](https://nodejs.org) with the included npm package manager. +Check the project's `package.json` in the `engines` section for the recommended node version. + +Clone or download the Intershop PWA GitHub project to your computer, e.g. + +```bash +git clone https://github.com/intershop/intershop-pwa.git +``` + +After having cloned the project from the Git repository, open a command line in the project folder and run `npm install` to download all required dependencies into your development environment. + +The project uses [Angular CLI](https://cli.angular.io) - a command line interface for Angular - that has to be installed globally. +Run `npm install -g @angular/cli` once to globally install Angular CLI on your development machine. + +Use `ng serve --open` to start up the development server and open the Progressive Web app in your browser. + +> **Note:** With the default `environment.ts` configuration the application works with mock data and as a result of that with limited functionality. To experience and work with the full feature set of the Intershop PWA access to an Intershop Commerce Management server is required. + +## Development Server + +Run `ng serve` or `ng s` for a development server that is configured by default via `environment.ts` to use mocked responses instead of actual REST calls. + +Running `ng serve --configuration production` or `ng s -c production` starts a development server that will communicate by default with the Intershop Commerce Management server of our public demo via REST API (see the used `environment.prod.ts` for the configuration). + +The project is also configured to support the usage of an own local environment file `environment.local.ts` that can be configured according to your local development environment needs, e.g. with a different icmBaseURL or different configuration options (see the `environment.model.ts` for the available configuration options). +This file will be ignored by Git so the developer specific settings will not be commited and accidentally shared. + +To create such a development specific `environment.local.ts` file just copy one of the two existing environment files and make the necessary configuration adaptions, e.g. + +- Set your `icmBaseURL: 'http://',` +- Set `production: false,` +- Remove `mockServerAPI: true,` +- Configure the wanted `features: ['compare', 'recently', ...],` +- Maybe disable the service worker for development `serviceWorker: false,` + +To use this local environment configuration, the server should be started with + +```bash +ng s -c local +``` + +Once the server is running, navigate to http://localhost:4200 in your browser to see the application. +The app will automatically reload if you change any of the source files. + +Running `ng serve --port 4300` will start the server on a different port than the default 4200 port, e.g. if one wants to run multiple instances in paralell for comparison. + +Running `ng serve --open` will automatically open a new browser tab with the started application. +The different start options can be combined. + +Further options of the development server can be found running `ng serve --help`. + +> **Warning:** DO NOT USE webpack-dev-server IN PRODUCTION environments! + +## Development Tools + +The used IDE or editor should support the [Prettier - Code formatter](https://prettier.io) that is configured to apply a common formatting style on all TypeScript, Javascript, JSON, HTML, SCSS and other files. +In addition, especially for the file types that are not handled by Prettier, the editor needs to follow the [EditorConfig](http://editorconfig.org) configuration of the project to help maintain consistent coding styles. +Besides that the project has [TSLint](https://palantir.github.io/tslint) and [Stylelint](https://stylelint.io) rules configured to unify the coding style even further. + +The recommende IDE for the Intershop PWA development is + +[**Visual Studio Code** ](https://code.visualstudio.com) = VS Code = VSC + +It is a free IDE built on Open Source and available for the different plattforms with good TypeScript support maintained by Microsoft. + +Within the PWA project we supply configuration files for VS Code that suggest downloading recommended plugins and apply best-practice settings (see the `.vscode` folder of the project). + +If your editor or IDE provides no support for the formatting and linting, make sure the rules are applied otherwise. +E.g. the project provides npm tasks that perform code style checks as well. +Use `npm run lint` to run a static code analysis. +Use `npm run format` to perform a formatting run on the code base with Prettier. `npm run stylelint` will apply a common code style to the projects styling definitions. + +### Pre-Commit Check + +`npm run check` is a combination task of `lint`, `format`, `stylelint` and `test` that runs some of the checks that will also be performed by the continuous integration system checks on the whole code base. +Do not overuse it as the run might take some time on your local development environments. + +Prefer using `npx lint-staged` to perform a manual quick evaluation of staged files. +This also happens automatically when committing files with the configured pre-commit hooks for Git. +It is possible to bypass the verification on commit with the Git option `--no-verify`. + +## Debugging + +Tips and tools for debugging Angular applications can be found on the Internet. +As Angular runs in the browser, all the development tool functionality provided there can also be used for Angular (debugging, call stacks, profiling, storage, audits, ...). + +### Browser Extensions + +- [Redux DevTools](https://github.com/reduxjs/redux-devtools) for debugging application state changes + +- [Angular Augury](https://augury.angular.io) for debugging and profiling Angular applications + +### Recommend Articles + +[Debugging Angular CLI Applications in Visual Studio Code](https://scotch.io/tutorials/debugging-angular-cli-applications-in-visual-studio-code) + +- How to configure Visual Studio Code for debugging an Agular application. + +[A Guide To Debugging Angular Applications](https://medium.com/front-end-weekly/a-guide-to-debugging-angular-applications-5a36bd88b4cf) + +- Use `tap` to log output in RxJS streams. We introduced an operator called `log` for easier use. +- When inspecting an element in the browser development tools, you can then use `ng.probe($0).componentInstance` to get access to the Angular component. +- Use `ng.profiler.timeChangeDetection({record:true})` to profile a change detection cycle of the current page. +- Use the `json` pipe in Angular to print out data on templates. Easy-to-use snippets are available with `ng-debug` and `ng-debug-async` . + +[Everything you need to know about debugging Angular applications](https://indepth.dev/everything-you-need-to-know-about-debugging-angular-applications) + +- Provides a more in-depth view about internals. + +[Debug Angular apps in production without revealing source maps](https://medium.com/angular-in-depth/debug-angular-apps-in-production-without-revealing-source-maps-ab4a235edd85) + +- If you also generate the source maps for production builds, you can load them in the browser development tools and use them for debugging production setups. diff --git a/docs/guides/forms-checkbox.png b/docs/guides/forms-checkbox.png new file mode 100644 index 0000000000000000000000000000000000000000..f49db872bdf4c39f8f44e0c792217ee57fba54d5 GIT binary patch literal 3022 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 literal 0 HcmV?d00001 diff --git a/docs/guides/forms-formfeedback.png b/docs/guides/forms-formfeedback.png new file mode 100644 index 0000000000000000000000000000000000000000..7b39f89a921d1dae5b9ce65eadabed1f52e50d2f GIT binary patch literal 5944 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 literal 0 HcmV?d00001 diff --git a/docs/guides/forms-input.png b/docs/guides/forms-input.png new file mode 100644 index 0000000000000000000000000000000000000000..06f0060b6f11a1bccbe24bf9b967778430fec3f3 GIT binary patch literal 3031 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 F^PhmD3<>}M literal 0 HcmV?d00001 diff --git a/docs/guides/forms-textarea.png b/docs/guides/forms-textarea.png new file mode 100644 index 0000000000000000000000000000000000000000..746568c6770af3bcf868033cc74920ac9437b5cf GIT binary patch literal 4610 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 + +# Forms + +## File and Naming Conventions + +### Reusable Form Components + +- File location: _app/shared/forms/components/\_ or _app/shared/address-forms/components/\_ +- Name: _\-form.component.ts_ +- These forms can be used as (sub)forms on arbitrary pages, e.g., there are address forms on registration page, checkout and my account pages. + +### Page Specific Form Components + +- File location: _app/pages/\/\_ +- Name: _\-form.component.ts_ +- These forms are only valid for a specific page. They are not reusable. +- Example: The credentials form on the registration page. + +### Data Models + +- File location for models and related classes: _app/core/models/\_ +- Model name: _\.model.ts_ +- Mapper file name: _\.mapper.ts_ +- Data (interface) file name: _\.interface.ts_ + +### Services + +- File location for global services: _core/services/\_ +- File location for module specific services: _\/services/\_ +- Name: _\.service.ts_ + +Usually, there should be no form specific data models. +If forms are related to persistent data, use/create generic data models for your forms, e.g., there should be only one data model for addresses. +Each model has its own service class(es). +In this class there are methods concerning the data model, e.g., updateAddress (address: Address) + +### Extensions + +If functionality is implemented as an extension, the form models and services can be found in the extensions folder: + +- forms: _app/extensions/\/pages/\/\_ +- models: _app/extensions/\/models/\_ +- services: _app/extensions/\/services/\_ + +## Form Behavior + +- Labels of required form controls have to be marked with a red asterisk. +- After a form control is validated: + - Its label gets green and a checked icon is displayed at the end of the control in case the input value is valid. + - Its label gets red, an error icon is displayed at the end of the control and an error message is displayed below the control in case the input value is invalid. +- Form validation + - If a form is shown, there should not be any validation error messages. + - If a user starts to enter data in an input field, this field will be validated immediately. + - If the user presses the submit button, all form controls of the form are validated; the submit button will be disabled as long as there is any unhandled form error. + +## General Rules + +### Usage of Template Driven Forms vs Reactive Forms + +In general, you should use reactive forms for creating your forms. +If a form is very simple (e.g. only one form input field without any special validation rules), it is also possible to use template driven forms as an exception. + +### Validators + +For the validation of the form input fields you can use Angular's [Build-in Validators](https://angular.io/api/forms/Validators). + +Additionally, the package [ng2-validation](https://www.npmjs.com/package/ng2-validation) is available. +It provides further validators. + +If there is a need for special custom validators, use class _app/shared/forms/validators/special-validators_ to write your own custom validators. + +### Keep Templates and Type Script Code Simple + +Whenever possible, move logic from the template to the type script. + +Use predefined form control components and directives to get general functionality like displaying control validation status, validation error messages and so on, see the following section. + +## How to Build a Form + +### Build a Form + +- Build a page component which is responsible for getting and sending data using facades. +- Build a form component which holds the form. +- Use either predefined form control components (see below) to build your form or `ish-form-control-feedback` component to display error messages and the validation icons and `ishShowFormFeedback` directive on the form-group elements in order to color labels and controls according to their validation status. +- In both cases the parameter `errorMessages` should be a key value pair of a possible validator that causes the error and its localisation key/localized string, e.g.: + { `'required':'account.login.email.error.required'` , `'email':'account.login.email.error.invalid'` } +- Take care of disabling the form submit button in case the user submits an invalid form (see example below). + +### Example + +**login-form.html** + +```html +
+ + + +
+``` + +**login-form.ts** + +```typescript +import { FormBuilder, FormGroup, Validators } from '@angular/forms' ; +import { CustomValidators } from 'ng2-validation' ; +import { FormUtilsService } from '../../../../core/services/utils/form-utils.service'; +... + +constructor ( + private formBuilder : FormBuilder, + private formUtils: FormUtilsService ) {} +... + +ngOnInit() { + this .loginForm = this .formBuilder.group({ + userName: [ '' , [Validators.required, CustomValidators.email]], + password: [ '' , Validators.required] + }); +} + +onSignin(userCredentials) { + if (this.form.invalid) { + this.submitted = true; + this.formUtils.markAsDirtyRecursive(this.form); + return; + } +this.create.emit(this.form.value); + +} + + +cancelForm() { + this.cancel.emit(); + } +``` + +## Predefined Components / Directives / Services + +| Component/Directive | Example | Description | +| ----------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **ishShowFormFeedback** (Directive) | \
...\
| Directive ishShowFormFeedback Can be used to color labels and form controls in dependence of the validation status of the related form control Should be used at the form-group element | +| **ish-input** (Component) | ![Input field](forms-input.png)   \\ | **Input form control** for Text input fields, Email input fields, Password input fields, Number input fields | +| **ish-select** (Component) | ![Select box](forms-select.png) \\ | **Select form control** 'options' should implement interface SelectOption | +| **ish-textarea** (Component) | ![Textarea input](forms-textarea.png) \\ | **Textarea form control** | +| **ish-checkbox** (Component) | ![Checkbox](forms-checkbox.png) \\ | **Checkbox form control** | +| **ish-form-control-feedback** (Component) | ![Form feedback](forms-formfeedback.png) | **form control feedback component** To display error messages To display validation status icon of the related form control NOTE: Use this component only if you cannot use one of the components above | +| **form-utils** (Service) | Methods: markAsDirtyRecursive(formGroup: FormGroup) updateValidatorsByDataLength(control: AbstractControl,array: any[],validators: ValidatorFn / ValidatorFn[] = Validators.required,async = false) | | Service for form related tasks | + +## Error Handling of Server Side Error Messages + +Server side errors should be saved in the ngrx store. +Please make sure they are also removed if they are obsolete. + +Errors should be read by appropriate facade methods and can be displayed by the error-component. + +**account-profile-user.component.ts** + +```typescript +userError$: Observable; + +ngOnInit() { + ... + this.userError$ = this.accountFacade.userError$; + } +``` + +**account-profile-user.component.html** + +```html + + +``` + +## The Address Form as an Example of a Reusable Form + +If you want to embed a reusable form onto your page or if you want to combine several forms into one big form like the registration form, you always have to use reactive forms. + +### How to Use the address-form Component + +The following steps describe how to use the address-form component on your form (see also the example below): + +Container component: + +1. Get all necessary data (countries, regions, titles etc.) and pass it to the form component. +2. React on country changes by getting country specific data like regions and titles. + +Form component: + +1. Place the address-form component on the html part of your form component. +2. onInit: Add a (sub) formGroup for your address to your form using the `getFactory` method of the AddressFormService. +3. Implement the onCountryChange behavior to switch the address formGroup according to the country specific form controls and emit this to the container. +4. React on region changes: Update validator for "state" control according to regions. + +### How to Create a New Country Specific Form + +1. Create a new country-specific address-form component under _shared/forms/address-forms/forms/address-form-._ +2. Create the related factory class under _shared/forms/address-forms/forms/address-form-._ +3. Add your new component in _shared/forms/address-forms/forms/address-form.html_ under the `ngSwitch` statement. +4. Register your new component in _shared/forms/address-forms/forms/index.ts_ under `components` and `factoryProviders`. + +## Dynamic Forms + +Form creation is also possible dynamically based on a (json) definition using the library [formly](https://formly.dev/). + +This is currently done to create payment parameter forms depending on the payment method (e.g. a credit card form). +The form definition is obtained from the REST interface in this case. + +The following input types are supported by now: + +**forms-dynamic.module.ts** + +```typescript +FormlyModule.forRoot({ + types: [ + { name: 'input', component: InputDynamicComponent }, + { name: 'select', component: SelectDynamicComponent }, + ], + }), +``` + +The InputDynamicComponent and the SelectDynamicComponent map the input of the Type 'FieldType' (formly) to the form predefined control components (see also table above). diff --git a/docs/guides/getting-started.md b/docs/guides/getting-started.md new file mode 100644 index 0000000000..f854ec6449 --- /dev/null +++ b/docs/guides/getting-started.md @@ -0,0 +1,114 @@ + + +# Getting Started + +Before working with this project, download and install [Node.js](https://nodejs.org) with the included npm package manager. +Currently Node.js 12.x LTS with the corresponding npm is required. + +This project was generated with [Angular CLI](https://github.com/angular/angular-cli) and follows the Angular CLI style guide and naming conventions. + +--- + +## Quick Start + +After having cloned the project from the Git repository, open a command line in the project folder and run `npm install`. + +The project uses Angular CLI which has to be installed globally. +Run `npm install -g @angular/cli` once to globally install Angular CLI on your development machine. +Use `ng serve --open` to start up the development server and open the Progressive Web App in your browser. + +The project can alternatively be run in production mode with `npm start`. + +## Customization + +Before customizing the PWA for your specific needs, have a look at our [Customization Guide](./customizations.md) and also have a look at the current [PWA Guide](https://support.intershop.de/kb/index.php?c=Search&qoff=0&qtext=guide+progressive+web+app) first. + +## Development Server + +Run `ng serve` or `ng s` for a development server that is configured by default via _environment.ts_ to use mocked responses instead of actual REST calls. + +Running `ng serve --configuration production` or `ng s -c production` starts a server that will communicate by default with the Intershop Commerce Management of our public demo via REST API (see the used _environment.prod.ts_ for the configuration). + +The project is also configured to support the usage of an own local environment file _environment.local.ts_ that can be configured according to the development environment, e.g. with a different icmBaseURL or different configuration options (see the _environment.model.ts_). +This file will be ignored by Git so the developer-specific setting will not be committed. +To use this local environment configuration, the server should be started with `ng s -c local`. + +Once the server is running, navigate to _http://localhost:4200/_ in your browser to see the application. +The application will automatically reload if you change any of the source files. + +Running `ng serve --port 4300` will start the server on a different port than the default 4200 port, e.g., if one wants to run multiple instances in parallel for comparison. + +Running `ng serve --open` will automatically open a new browser tab with the started application. +The different start options can be combined. + +> **Warning**: Do not use _webpack-dev-server_ in production! + +Also have a look at further information in the [Development Guide](./development.md) + +## Deployment + +Deployments are generated to the _dist_ folder of the project. + +Use `npm run build` to generate the preferred Angular Universal enabled version. +On the server the _dist/server.js_ script has to be executed with `node`. + +Alternatively, you can use `ng build --prod` to get an application using browser rendering. +All the files under `dist/browser` have to be served statically. +The server has to be configured for fallback routing, +see [Server Configuration in Angular Docs](https://angular.io/guide/deployment#server-configuration). + +For a production setup we recommend building the docker image supplied with the `Dockerfile` in the root folder of the project. +Build it with `docker build -t my_pwa .`. +To run the PWA with multiple channels and [Google PageSpeed](https://developers.google.com/speed/pagespeed/insights/) optimizations, you can use the nginx docker image supplied in the sub folder [nginx](../../nginx). + +We provide templates for [Kubernetes Deployments](../../schematics/src/kubernetes-deployment) and [DevOps](../../schematics/src/azure-pipeline) for Microsoft Azure. + +## Progressive Web App (PWA) + +To run the project as a Progressive Web App with an enabled [Service Worker](https://angular.io/guide/service-worker-getting-started), use `npm run start` to build and serve the application. +After that open _http://localhost:4200_ in your browser and test it or run a PWA audit. +Currently only _localhost_ or _127.0.0.1_ will work with the service worker since it requires _https_ communication on any other domain. + +## Running Tests + +Run `npm test` to start an on the fly test running environment to execute the unit tests via [Jest](https://facebook.github.io/jest/) once. +To run Jest in watch mode with interactive interface, run `npm run test:watch`. + +Run `npm run e2e` to execute the end-to-end tests via [cypress](https://www.cypress.io/). +You have to start your development or production server first as cypress will instruct you. + +Head over to the [Testing Concept](../concepts/testing.md) documentation for more information. + +## Code Style + +Use `npm run lint` to run a static code analysis. + +For development make sure the used IDE or editor follows the [EditorConfig](http://editorconfig.org/) configuration of the project and uses [Prettier](https://prettier.io/) to help maintain consistent coding styles (see `.editorconfig` and `.prettierrc.json`). + +Use `npm run format` to perform a formatting run on the code base with Prettier. + +## Pre-Commit Check + +`npm run check` is a combination task of `lint`, `format` and `test` that runs some of the checks that will also be performed in continuous integration on the whole code base. +Do not overuse it as the run might take a long time. + +Prefer using `npx lint-staged` to perform a manual quick evaluation of staged files. +This also happens automatically when committing files. +It is also possible to bypass verification on commit, following the suggestions of the versioning control tool of your choice. + +## Code Scaffolding + +With the integrated `intershop-schematics` this project provides the functionality to generate different code artifacts according to our style guide and project structure. `ng generate` will use our custom schematics by default, e.g. run `ng generate component component-name` in the shared folder to generate a new shared component. `ng generate --help` gives an overview of available Intershop-specific schematics. + +The Angular CLI default schematics are still available and working. +However, they need to be prefixed to use them, e.g. `ng generate @schematics/angular:guard`. +A list of the available Angular CLI schematics can be fetched with `ng generate @schematics/angular: --help`. + +## Further Help + +To get more help on the Angular CLI, use `ng help` or check out the [Angular CLI Documentation](https://github.com/angular/angular-cli/wiki). diff --git a/docs/guides/google-tag-manager.md b/docs/guides/google-tag-manager.md new file mode 100644 index 0000000000..8c5ab1e9bc --- /dev/null +++ b/docs/guides/google-tag-manager.md @@ -0,0 +1,20 @@ + + +# Google Tag Manager + +To enable user tracking and setting it up with [Google Tag Manager](https://github.com/angulartics/angulartics2/tree/master/src/lib/providers/gtm), the popular library [Angulartics2](https://angulartics.github.io/angulartics2/) is used. + +To activate GTM tracking, set the Tag Manager Token either in the used Angular CLI environment file with the property `gtmToken` or via the environment variable `GTM_TOKEN`. +Additionally, the feature toggle `tracking` has to be added to the enabled feature list. +This feature only works in Universal Rendering mode. +Prefer configuration via system environment variables. + +Please refer to the [angulartics2 documentation](https://github.com/angulartics/angulartics2#usage) for information on how to enable tracking for additional events. + +For GDPR compliance the tracked IPs have to be anonymized. +See [How to turn on IP Anonymization in Google Analytics and Google Tag Manager](https://www.optimizesmart.com/how-to-turn-on-ip-anonymization-in-google-analytics-and-google-tag-manager/). diff --git a/docs/guides/hybrid-approach-icm-url-rewriting.md b/docs/guides/hybrid-approach-icm-url-rewriting.md new file mode 100644 index 0000000000..aca4e14a51 --- /dev/null +++ b/docs/guides/hybrid-approach-icm-url-rewriting.md @@ -0,0 +1,81 @@ + + +# Handle Rewritten ICM URLs in Hybrid Mode + +If the ICM is set up with [URL Rewriting](https://support.intershop.com/kb/index.php/Display/28R955), further modifications are required to run the deployment with the [Hybrid Approach](../concepts/hybrid-approach.md). + +The examples in this guide follow the default example for ICM URL rewriting. +In particular we want to focus on the following two examples: + +- Product Detail Pages of the ICM + - URLs are in the form _/Home-Entertainment/SmartHome/google-home-zid201807171_ + - They should be handled by the PWA in the form _/SmartHome/google-home-sku201807171_ +- The help desk of the PWA + - URLs are in the form _/page/systempage.helpdesk.index.pagelet2-Page_ (generic content page) + - This URL should be handled by the ICM: _/helpdesk_ + +## Mapping Incoming Rewritten ICM URLs to the PWA + +Considering the example, the incoming product detail page URL mapping must be included in the mapping table: + +```typescript + { + id: 'Product Detail Page', + icm: '(.*\\/)?(?[\\w-]+)\\/(?[\\w-]+)-zid(?[\\w-]+)$', + pwaBuild: `$/$-$${PWA_CONFIG_BUILD}`, + pwa: `^.*sku([\\w-]+)$`, + // icmBuild not required + handledBy: 'pwa' + } +``` + +## Mapping PWA URLs to Rewritten ICM URLs + +First, the mapping table must be adapted to instruct the PWA to leave the single-page application when switching to the help desk content page: + +```typescript +export const ICM_WEB_URL = '/'; + +... + + { + id: 'Helpdesk', + icm: "${ICM_WEB_URL}helpdesk.*", + // pwaBuild not required + pwa: '^/page/systempage.helpdesk.index.pagelet2-Page$', + icmBuild: 'helpdesk', + handledBy: 'icm' + } +``` + +Then, the nginx configuration must be adapted. +The supplied nginx dynamically adds multi-site configuration parameters to dynamically configure the PWA depending on the incoming URL. +The ICM cannot handle them, so traffic to the ICM must be excluded from adding those parameters. +You can do this by extending the channel configuration: + +_nginx/channel.conf.tmpl_: + +```diff + # let ICM handle everything ICM related +- location ~* ^/INTERSHOP.*$ { ++ location ~* ^/(INTERSHOP|helpdesk).*$ { + proxy_set_header Host $http_host; +``` + +Last but not least, the _express.js_ server must be instructed to proxy traffic to _/helpdesk_ to the ICM upstream: + +_server.ts_: + +```diff + app.use('/INTERSHOP', icmProxy); ++ app.use('/helpdesk', icmProxy); +``` + +# Further References + +- [Concept - Hybrid Approach](../concepts/hybrid-approach.md) diff --git a/docs/migrations.md b/docs/guides/migrations.md similarity index 84% rename from docs/migrations.md rename to docs/guides/migrations.md index 9d2faa6dc5..4418896120 100644 --- a/docs/migrations.md +++ b/docs/guides/migrations.md @@ -1,12 +1,19 @@ # Migrations +## 0.18 to 0.19 + +We migrated from using [ngrx-router](https://github.com/amcdnl/ngrx-router) to the official and better supported [@ngrx/router-store](https://ngrx.io/guide/router-store). +This means that throughout the code all usage of the `ofRoute` operator and `RouteNavigation` actions are no longer available. + +As some of these implementations were very specific, we cannot provide a migration script. + ## 0.16 to 0.17 In this version change, we decided to no longer recommend the container-component-pattern and therefore changed the folder structure of the project. @@ -16,9 +23,3 @@ We did this because the previously introduced facades provide a more convenient You can run the migration by executing `node schematics/migration/0.16-to-0.17`. The script will check if all your components can be moved to the new folder structure and will then perform the migration or notify you of work previously needed. - -## 0.18 to 0.19 - -We migrated from using [ngrx-router](https://github.com/amcdnl/ngrx-router) to the official and better supported [@ngrx/router-store](https://ngrx.io/guide/router-store). This means that throughout the code all usage of the `ofRoute` operator and `RouteNavigation` actions are no longer available. - -As some of these implementations were very specific, we cannot provide a migration script. diff --git a/docs/guides/mocking-rest-calls.md b/docs/guides/mocking-rest-calls.md new file mode 100644 index 0000000000..18b4145ab5 --- /dev/null +++ b/docs/guides/mocking-rest-calls.md @@ -0,0 +1,26 @@ + + +# Mocking REST API Calls + +Sometimes during development it might be necessary to work with mocked data. +This is especially necessary when developing new features in parallel where backend and frontend are involved. +In the PWA we supply a mocking mechanism so the frontend team can start implementation with mocked data until the backend implementation is finished. + +## Switching On Mocking + +Mocking complete REST responses can be configured in _environment.ts_. +The property `mockServerAPI` switches between mocking all calls (true) and only mocking paths that have to be mocked because they do not yet exist in the [REST API](http://developer.cloud.intershop.com). +The property `mustMockPaths` is an array of regular expressions for paths that have to be mocked, regardless if `mockServerAPI` is enabled or disabled. + +## Supply Mocked Data + +Mocked data is put in the folder _assets/mock-data/_. +The path is the full path to the endpoint of the service without additional arguments. +The JSON response is put into a file called _get.json_ in the respective folder. + +Switching to mocked REST API calls is done by the `MockInterceptor` which reads all the configuration and acts accordingly. diff --git a/docs/guides/multiple-themes.md b/docs/guides/multiple-themes.md new file mode 100644 index 0000000000..46e0011a6b --- /dev/null +++ b/docs/guides/multiple-themes.md @@ -0,0 +1,43 @@ + + +# Multiple Themes + +It is possible to create multiple themes for styling. +The Intershop Progressive Web App currently uses multi-theming to provide different styles for the B2C an the B2B application. +The styles for B2C are defined in _/src/styles/themes/default/style.scss_, for B2B in _/src/themes/styles/blue/style.scss_. + +Using schematics to start customizing Intershop Progressive Web App prepares a theme for your own custom styling. (See [Customizations - Start Customization](../guides/customizations.md#start-customization)) + +You can also manually prepare a new theme: + +1. Create a custom theme folder (named _custom_) under _/src/themes/styles/_ with a copy of _styles.scss_ and _variables.scss_ from an available theme. + +2. Reference the styling theme in the _angular.json_, so that the theme bundle will be extracted during the compiling process + + ```json + ... + "styles": [ + ... + { + "input": "src/styles/themes/custom/style.scss", + "lazy": true, + "bundleName": "custom" + }, + ... + ] + ... + ``` + +3. Set the theme to be used in your application settings in the _environment.ts_ + +```typescript +export const environment: Environment = { + ... + theme: 'custom', +}; +``` diff --git a/docs/guides/propagating-environment-variables.md b/docs/guides/propagating-environment-variables.md new file mode 100644 index 0000000000..57a0e8f073 --- /dev/null +++ b/docs/guides/propagating-environment-variables.md @@ -0,0 +1,55 @@ + + +# Propagating Environment Variables + +This guide describes how to propagate additional configuration from the outside into the PWA client application to be used on the storefront. + +Configuration parameters can be provided from different sources. + +## Angular CLI Environment Files + +If properties just have to be provided as compile time settings in the Angular CLI environment files like _src/environments/environment.ts_, you can theoretically access them in any source file directly. +However we recommend creating [InjectionTokens][angular-injectiontoken] and [providing][angular-injectiontoken-provide] them in modules like the `ConfigurationModule`. + +This option provides the easiest approach. +Different configurations can be provided while building the sources with Angular CLI. + +[angular-injectiontoken]: https://angular.io/api/core/InjectionToken +[angular-injectiontoken-provide]: https://angular.io/guide/dependency-injection-providers#non-class-dependencies + +## URL Parameters + +Specific properties can also be supplied by URL parameters (i.e. _shop.de/home;foo=bar_). +The multi-channel configuration handling basically uses this method of configuration to dynamically provision a PWA server-side rendering run for a specific channel. + +Currently all of those properties are transferred into _src/app/core/store/configuration_. +If you want to add another property, add it to the `ConfigurationState` and add the extraction handling in `ConfigurationEffects`. + +## Runtime Environment Properties + +It gets more complicated, when properties from environment variables have to be transferred to the Angular client application. +Environment parameters from the browser cannot be accessed as the browsers basically sandbox all websites for security reasons. +What can be accessed are environment parameters of the environment the server-side rendering process is running in. +This can be the Docker environment (arguments passed by an orchestrator like Kubernetes or Docker Swarm) or the local environment for debug purposes. +See also [Guide - Building and Running Server-Side Rendering][guide-ssr] + +In general the extraction is as follows: + +1. The environment variable is accessed by the SSR process running with _node.js_ (via _process.env_). +2. An effect running in Angular Universal extracts these properties and puts them into the state management. +3. Upon completing the initial page response, the NgRx state is [dehydrated][dehydrated-rehydrated] and appended to the HTML document. +4. The browser boots up angular and [rehydrates][dehydrated-rehydrated] the state, effectively completing the transfer of the property. + +For extracting the environment property, you can use the methods of the `StatePropertiesService`. + +[guide-ssr]: ./ssr-startup.md +[dehydrated-rehydrated]: https://i.stack.imgur.com/YvHXB.gif + +## Further References + +- [Concept - Configuration](../concepts/configuration.md) diff --git a/docs/guides/sentry-error-monitoring.md b/docs/guides/sentry-error-monitoring.md new file mode 100644 index 0000000000..edf4b58d21 --- /dev/null +++ b/docs/guides/sentry-error-monitoring.md @@ -0,0 +1,21 @@ + + +# Client-Side Error Monitoring with Sentry + +We recommend using [Sentry](https://sentry.io/) for browser-side error tracking. +It is integrated with the [official Angular support dependency](https://sentry.io/for/angular2/). + +To activate Sentry in the PWA, set the Sentry DSN URL (_Settings_ | _Projects_ | _Your Project_ | _Client Keys (DSN)_ | _DSN_) either via Angular CLI environment file with the property `sentryDSN` or via the environment variable `SENTRY_DSN`. +Additionally, the feature toggle `sentry` has to be added to the enabled feature list. +This feature only works in Universal Rendering mode. +Prefer configuration via system environment variables. + +For GDPR compliance the tracked IPs have to be anonymized. + +Set your _Prevent Storing of IP Addresses_ option in your _General Settings_ of Organisation and/or Project (Sentry Webinterface). +Read more about [Security & Compliance](https://sentry.io/security/) or [Sensitive Data](https://docs.sentry.io/data-management/sensitive-data/) in Sentry-[Docs](https://docs.sentry.io/). diff --git a/docs/guides/ssr-startup.md b/docs/guides/ssr-startup.md new file mode 100644 index 0000000000..b124c556ef --- /dev/null +++ b/docs/guides/ssr-startup.md @@ -0,0 +1,49 @@ + + +# Building and Running Server-Side Rendering + +## Building + +To **simply** build the Intershop PWA in server-side rendering mode, you can use the _package.json_ script `npm run build`, which will build the Intershop PWA with the `production` configuration of the `angular.json`. +Afterwards you can start the application with `npm run serve` (or do both by using `npm run start`). + +The preferred way for **production deployments** is to build the `Dockerfile` in the project root and run the created image. +While building you can provide a build argument (i.e. via `--build-arg`) `configuration` and build a different configuration from _angular.json_. +By default the `production` configuration is built. + +## Running + +Overwriting configurations of the PWA is entirely done by environment properties. +We chose this approach to have the best compatibility with running the PWA from the command line or in an orchestrator. + +If the format is _any_, then the environment variable just has to be set to any value to be active. +Setting it to `"false"` still counts as active. +Only empty strings count as inactive. + +| | parameter | format | comment | +| ------------------- | --------------- | -------------------- | --------------------------------------------------------------------------- | +| **SSR Specific** | PORT | number | port for running the application | +| | SSL | any | enable TLS (expects `server.crt` and `server.key` in `dist` folder) | +| **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 | +| **Debug** :warning: | TRUST_ICM | any | use this if ICM is deployed with an insecure certificate | +| | LOGGING | any | enable extra log output | +| **Hybrid Approach** | SSR_HYBRID | any | enable running PWA and ICM in [Hybrid Mode](../concepts/hybrid-approach.md) | +| | PROXY_ICM | any | proxy ICM via `/INTERSHOP` (enabled if SSR_HYBRID is active) | +| **Third party** | GTM_TOKEN | string | token for Google Tag Manager | +| | SENTRY_DSN | string | Sentry DSN URL for using Sentry Error Monitor | + +## References + +- [Concept - Configuration](../concepts/configuration.md) +- [Concept - Hybrid Approach](../concepts/hybrid-approach.md) +- [Guide - Client-Side Error Monitoring with Sentry](./sentry-error-monitoring.md) +- [Guide - Google Tag Manager](./google-tag-manager.md) diff --git a/docs/guides/state-management.md b/docs/guides/state-management.md new file mode 100644 index 0000000000..33cff7ba41 --- /dev/null +++ b/docs/guides/state-management.md @@ -0,0 +1,38 @@ + + +# Developing with NgRx + +## NgRx Pitfalls + +### Using Services and catchError + +The operator handling the possible error of a service call must always be contained in the returned observable of the service call, otherwise it has no effect. + +See: [Handling Errors in NgRx Effects](https://medium.com/city-pantry/handling-errors-in-ngrx-effects-a95d918490d9) + +```typescript +@Effect() +effect = this.actions$.pipe( + ofType(ActionLoad), + switchMap(this.service.method().pipe( + map(x => new ActionSuccess(x)), + mapErrorToAction(ActionFail) + ), +) +``` + +### Using `switchMap` can Lead to Race Conditions + +Using flatmapping operators can lead to unexpected behavior. +If in doubt, use `concatMap`. + +See [RxJS: Avoiding switchMap-Related Bugs](https://medium.com/angular-in-depth/switchmap-bugs-b6de69155524) for more information. + +### Should I put XYZ into the Store or the Component? + +Follow the [SHARI-Principle](https://ngrx.io/docs#when-should-i-use-ngrx-for-state-management). diff --git a/docs/guides/testing-cypress.md b/docs/guides/testing-cypress.md new file mode 100644 index 0000000000..8b22f73f97 --- /dev/null +++ b/docs/guides/testing-cypress.md @@ -0,0 +1,55 @@ + + +# End-to-End Testing with Cypress + +## When to Write Cypress Tests? + +With Angular most of the functionality of simple components or artifacts of the state management can be tested with [Unit Tests][guide-unit-tests]. +However, when testing multiple artifacts at once, the `TestBed` definition can take a big amount of work. +Also considering, that not the real runtime modules are used in unit and module tests, it might be better to write short and concise [Cypress][cypress] tests for this functionality. + +Testing issues with timing and interaction between many artifacts is also quite hard to implement as a unit test. + +All in all, cypress testing should not be exhausting. +As a general rule only happy path and smoke tests should be implemented. + +## Rules for Developing Cypress Tests + +### Always Stick to Small Scope + +Testing workflows with **many actions** can lead to **increased instability** when encountering browser hiccups. +Split tests into individual specs when possible. + +### Pay Respect to Individuality + +Every test should be **independent of other tests**. +So whenever a test is including a modification operation, it is best to create an individual user first. + +### Do not Reinvent the Wheel + +When testing functionality that needs to setup specific demo data first, do not create it via the user interface. +Instead write stable **REST API helper** methods, that can set up data faster via the API when the test starts. + +### Stick to the PageObject Pattern + +We introduced the [PageObject Pattern][concept-testing-pageobject] with the motivation of separating business-features from technical background. +So do not use HTML-specific selectors or exposed cypress functionality in tests if possible. + +Also do not hide too much actions with the [PageObject Pattern][concept-testing-pageobject]. +As a rule of thumb, whenever the user triggers an action, it should be represented by a line of code in the spec. + +# Further References + +- [Concept - Testing the PWA][concept-testing] +- [Guide - Unit Testing with Jest][guide-unit-tests] +- [Cypress Website][cypress] + +[concept-testing]: ../concepts/testing.md +[concept-testing-pageobject]: ../concepts/testing.md#pageobject-pattern +[guide-unit-tests]: ./testing-jest.md +[cypress]: https://cypress.io diff --git a/docs/guides/testing-jest.md b/docs/guides/testing-jest.md new file mode 100644 index 0000000000..8c927753b4 --- /dev/null +++ b/docs/guides/testing-jest.md @@ -0,0 +1,515 @@ + + +# Unit Testing with Jest + +- [Unit Testing with Jest](#unit-testing-with-jest) + - [Stick to General Unit Testing Rules](#stick-to-general-unit-testing-rules) + - [Single Responsibility](#single-responsibility) + - [Test Functionality - Not Implementation](#test-functionality---not-implementation) + - [Do not Comment out Tests](#do-not-comment-out-tests) + - [Always Test the Initial State of a Service/Component/Module/...](#always-test-the-initial-state-of-a-servicecomponentmodule) + - [Do not Test the Obvious](#do-not-test-the-obvious) + - [Make Stronger Assertions](#make-stronger-assertions) + - [Do not Meddle with the Framework](#do-not-meddle-with-the-framework) + - [Assure Readability of Tests](#assure-readability-of-tests) + - [Stick to Meaningful Naming](#stick-to-meaningful-naming) + - [Avoid Global Variables](#avoid-global-variables) + - [Avoid Code Duplication in Tests](#avoid-code-duplication-in-tests) + - [Do not Use Features You Do not Need](#do-not-use-features-you-do-not-need) + - [Structure Long Tests](#structure-long-tests) + - [Avoid Having Dead Code](#avoid-having-dead-code) + - [Use a Mocking Framework Instead of Dealing with Stubbed Classes](#use-a-mocking-framework-instead-of-dealing-with-stubbed-classes) + - [Do not Change Implementation to Satisfy Tests](#do-not-change-implementation-to-satisfy-tests) + - [DOM Element Selection](#dom-element-selection) + - [DOM Changes for Tests](#dom-changes-for-tests) + - [Stick to Intershop Conventions Regarding Angular Tests](#stick-to-intershop-conventions-regarding-angular-tests) + - [Every Component Should Have a 'should be created' Test](#every-component-should-have-a-should-be-created-test) + - [Choose the Right Level of Abstraction](#choose-the-right-level-of-abstraction) + - [Be Aware of Common Pitfalls](#be-aware-of-common-pitfalls) + - [Be Careful When Using `toBeDefined`](#be-careful-when-using-tobedefined) + - [Be Careful With Variable Initialization](#be-careful-with-variable-initialization) + - [Use the right way to test EventEmitter](#use-the-right-way-to-test-eventemitter) + +## Stick to General Unit Testing Rules + +### Single Responsibility + +A test should test only one thing. +One given behavior is tested in one and _only_ one test. + +The tests should be independent from others, that means no chaining and no run in a specific order is necessary. + +### Test Functionality - Not Implementation + +A test is implemented incorrectly or the test scenario is meaningless if changes in the HTML structure of the component destroy the test result. + +Example: The test fails if an additional input field is added to the form. + +:warning: **Wrong Test Scenario** + +```typescript +it('should check if input fields are rendered on HTML', () => { + const inputFields = element.getElementsByClassName('form-control'); + expect(inputFields.length).toBe(4); + expect(inputFields[0]).toBeDefined(); + expect(inputFields[1]).toBeDefined(); + expect(inputFields[2]).toBeDefined(); +}); +``` + +### Do not Comment out Tests + +Instead use the `xdescribe` or `xit` feature (just add an '`x`' before the method declaration) to exclude tests. +This way excluded tests are still visible as skipped and can be repaired later on. + +:heavy_check_mark: + +```typescript +xdescribe("description", function() { + it("description", function() { + ... + }); +}); +``` + +### Always Test the Initial State of a Service/Component/Module/... + +This way the test itself documents the initial behavior of the unit under test. +Especially if you test that your action triggers a change: Test for the previous state! + +:heavy_check_mark: + +```typescript +it('should call the cache when data is available', () => { + // precondition + service.getData(); + expect(cacheService.getChachedData).not.toHaveBeenCalled(); + + << change cacheService mock to data available >> + + // test again + service.getData(); + expect(cacheService.getChachedData).toHaveBeenCalled(); +}); +``` + +### Do not Test the Obvious + +Testing should not be done for the sake of tests existing: + +- It is not useful to test getter and setter methods and use spy on methods which are directly called later on. +- Do not use assertions which are logically always true. + +### Make Stronger Assertions + +It is easy to always test with `toBeTruthy` or `toBeFalsy` when you expect something as a return value, but it is better to make stronger assertions like `toBeTrue`, `toBeNull` or `toEqual(12)`. + +:warning: + +```typescript +it('should cache data with encryption', () => { + customCacheService.storeDataToCache('My task is testing', 'task', true); + expect(customCacheService.cacheKeyExists('task')).toBeTruthy(); +}); +``` + +:heavy_check_mark: + +```typescript +it('should cache data with encryption', () => { + customCacheService.storeDataToCache('My task is testing', 'task', true); + expect(customCacheService.cacheKeyExists('task')).toBeTrue(); +}); +``` + +Again, do not rely too much on the implementation. +If user customizations can easily break the test code, your assertions are too strong. + +:warning: **Test too Close to Implementation** + +```typescript +it('should test if tags with their text are getting rendered on the HTML', () => { + expect(element.getElementsByTagName('h3')[0].textContent).toContain('We are sorry'); + expect(element.getElementsByTagName('p')[0].textContent).toContain( + 'The page you are looking for is currently not available' + ); + expect(element.getElementsByTagName('h4')[0].textContent).toContain('Please try one of the following:'); + expect(element.getElementsByClassName('btn-primary')[0].textContent).toContain('Search'); +}); +``` + +:heavy_check_mark: **Same Test in a more Stable Way** + +```typescript +it('should test if tags with their text are getting rendered on the HTML', () => { + expect(element.getElementsByClassName('error-text')).toBeTruthy(); + expect(element.getElementsByClassName('btn-primary')[0].textContent).toContain('Search'); +}); +``` + +### Do not Meddle with the Framework + +Methods like `ngOnInit()` are lifecycle-hook methods which are called by Angular – The test should not call it directly. +When doing component testing, you most likely use `TestBed` anyway, so use the `detectChanges()` method of your available `ComponentFixture`. + +:warning: **Wrong Test with ngOnInit() Method Calling** + +```typescript +it('should call ngOnInit method', () => { + component.ngOnInit(); + expect(component.loginForm).toBeDefined(); +}); +``` + +:heavy_check_mark: **Test without ngOnInit() Method Call** + +```typescript +it('should contain the login form', () => { + fixture.detectChanges(); + expect(component.loginForm).not.toBeNull(); +}); +``` + +## Assure Readability of Tests + +### Stick to Meaningful Naming + +The test name describes perfectly what the test is doing. + +:warning: **Wrong Naming** + +```typescript +it ('wishlist test', () => {...}) +``` + +:heavy_check_mark: **Correct Naming** + +```typescript +it ('should add a product to an existing wishlist when the button is clicked', () => {...}) +``` + +Basically it should read like a documentation for the unit under test, not a documentation about what the test does. [Jasmine](https://jasmine.github.io) has named the methods accordingly. +Read it like \`I am describing , it should when/if/on (because/to )\`. + +This also applies to assertions. +They should be readable like meaningful sentences. + +:warning: + +```typescript +const result = accountService.isAuthorized(); +expect(result).toBeTrue(); +``` + +:heavy_check_mark: + +```typescript +const authorized = accountService.isAuthorized(); +expect(authorized).toBeTrue(); +``` + +or directly + +```typescript +expect(accountService.isAuthorized()).toBeTrue(); +``` + +### Avoid Global Variables + +Tests should define Variables only in the scope where they are needed. +Do not define Variables before `describe` or respective `it` methods. + +### Avoid Code Duplication in Tests + +This increases readability of test cases. + +- Common initialization code of constants or sub-elements should be located in `beforeEach` methods. +- When using `TestBed` you can handle injection to variables in a separate `beforeEach` method. + +:warning: + +```typescript +it('should create the app', async(() => { + const fixture = TestBed.createComponent(AppComponent); + const component = fixture.componentInstance; + ... +}); +it('should have the title "app"', async(() => { + const fixture = TestBed.createComponent(AppComponent); + const component = fixture.componentInstance; + ... +}); +it('should match the text passed in Header Component', async(() => { + const fixture = TestBed.createComponent(AppComponent); +}); +``` + +:heavy_check_mark: + +```typescript +describe('AppComponent', () => { + let translate: TranslateService; + let fixture: ComponentFixture; + let component: AppComponent; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ ... ] }); + fixture = TestBed.createComponent(AppComponent); + component = fixture.componentInstance; + }) + it('should create the app', () => { ... }); + it(\`should have as title 'app'\`, () => { ... }); + it('should match the text passed in Header Component', () => { ... }); +}); +``` + +### Do not Use Features You Do not Need + +This increases readability of test cases. + +If you do not need the functionality of : + +- `ComponentFixture.debugElement` +- `TestBed` +- `async, fakeAsync` +- `inject` + +... do not use it. + +:warning: **Wrong Test With Useless Features (TestBed, ComponentFixture.debugElement)** + +```typescript +it('should create the app', async(() => { + const app = fixture.debugElement.componentInstance; + expect(app).toBeTruthy(); +})); +``` + +:heavy_check_mark: **Same Test - Works Without These Features** + +```typescript +it('should be created', () => { + const app = fixture.componentInstance; + expect(app).toBeTruthy(); +}); +``` + +### Structure Long Tests + +The `describe` methods in Jasmine are nestable. +You can use this to group various `it` methods into a nested `describe` where you can also use an additional `beforeEach` initialization method. + +:heavy_check_mark: **Nested describe Methods** + +```typescript +describe('AccountLogin Component', () => { + it('should be created', () => { ... }); + it('should check if controls are rendered on Login page', () => { ... }); + .... + describe('Username Field', () => { + it('should be valid when a correct email is assigned', () => { ... }); + .... + }); +}); +``` + +### Avoid Having Dead Code + +Always only declare what you need. +Unused variables, classes and imports reduce the readability of unit tests. + +### Use a Mocking Framework Instead of Dealing with Stubbed Classes + +This way less code needs to be implemented which again increases readability of unit tests. +Also mocks can be stubbed on time, depending on the current method. +We decided to use _ts-mockito_ as the Test Mocking Framework. + +## Do not Change Implementation to Satisfy Tests + +### DOM Element Selection + +Use only IDs or definite class names to select DOM elements in tests. +Try to avoid general class names. + +:warning: **Wrong Selector** + +```typescript +const selectedLanguage = element.getElementsByClassName('hidden-xs'); +``` + +:heavy_check_mark: **Correct Selector** + +```typescript +// by id + +const selectedLanguage = element.querySelector('#language-switch'); + +// by class + +const selectedLanguage = element.getElementsByClassName('language-switch'); +``` + +### DOM Changes for Tests + +Use `data-testing-id` via attribute binding to implement an identifier used for testing purpose only. + +:heavy_check_mark: **Correct Testing ID** + +\*.component.html + +```html +
    +
  1. {{ section.text }}
  2. +
+``` + +\*.spec.ts + +```typescript +element.querySelectorAll('[data-testing-id]')[0].innerHTML; + +element.querySelectorAll("[data-testing-id='en']").length; +``` + +> :warning: **Note** +> Do not overuse this feature! + +## Stick to Intershop Conventions Regarding Angular Tests + +### Every Component Should Have a 'should be created' Test + +Every component should have a 'should be created' test like the one Angular CLI auto-generates. +This test handles runtime initialization Errors. + +:heavy_check_mark: + +```typescript +it('should be created', () => { + expect(component).toBeTruthy(); + expect(element).toBeTruthy(); + expect(() => fixture.detectChanges()).not.toThrow(); +}); +``` + +### Choose the Right Level of Abstraction + +- When working mainly with stubs for specific services which mock dependencies of services under test, you should mainly use spies to check whether the right methods of the stub are called. +- When working mainly with fully configured services, it is best to check return values. +- When testing complex scenarios (e.g., when the test has to handle multiple pages), it might be better to implement a Geb+Spock end to end test. + +See [Three Ways to Test Angular Components](https://vsavkin.com/three-ways-to-test-angular-2-components-dcea8e90bd8d) for more information. + +## Be Aware of Common Pitfalls + +### Be Careful When Using `toBeDefined` + +Be careful when using `toBeDefined`, because a dynamic language like JavaScript has another meaning of defined (see: [Is It Defined? toBeDefined, toBeUndefined](http://www.safaribooksonline.com/library/view/javascript-testing-with/9781449356729/ch04.html)). + +> :warning: **Warning** +> Do not use `toBeDefined` if you really want to check for not null because technically 'null' is defined. Use `toBeTruthy` instead. + +### Be Careful With Variable Initialization + +Jasmine does not automatically reset all your variables for each test like other test frameworks do. +If you initialize directly under `describe`, the variable is initialized only once. + +> :warning: **Warning** +> Since tests should be independent of each other, do not do this. + +```typescript +describe(... () => { + let varA = true; // if changed once, value is not initialized again + const varB = true; // immutable value + let varC; // initialized in beforeEach for every test + + beforeEach({ varC = true; }); + + it( 'test1' () => { + varA = false; + // varB = false; not possible + varC = false; }); + it( 'test2' () => { + // varA is still false + // varB is still true + // varC is back to true + }) +}); +``` + +As shown in the above example, `varA` shows the wrong way of initializing variables in tests. + +If you do not need to change the value, use a `const` declaration like variable `varB`. +If you need to change the value in some tests, assure it is reinitialized each time in the `beforeEach` method like `varC`. + +### Use the right way to test EventEmitter + +Testing `EventEmitter` firing can be done in multiple ways that have advantages and disadvantages. +Consider the following example: + +```typescript +import { EventEmitter } from '@angular/core'; +import { anything, capture, deepEqual, spy, verify } from 'ts-mockito'; + +describe('Emitter', () => { + class Component { + valueChange = new EventEmitter<{ val: number }>(); + + do() { + this.valueChange.emit({ val: 0 }); + } + } + + let component: Component; + + beforeEach(() => { + component = new Component(); + }); + + it('should detect errors using spy with extract', () => { + // *1* + const emitter = spy(component.valueChange); + + component.do(); + + verify(emitter.emit(anything())).once(); + const [arg] = capture(emitter.emit).last(); + expect(arg).toEqual({ val: 0 }); + }); + + it('should detect errors using spy with deepEqual', () => { + // *2* + const emitter = spy(component.valueChange); + + component.do(); + + verify(emitter.emit(deepEqual({ val: 0 }))).once(); + }); + + it('should detect errors using subscribe', done => { + // *3* + component.valueChange.subscribe(data => { + expect(data).toEqual({ val: 0 }); + done(); + }); + + component.do(); + }); +}); +``` + +As `EventEmitter` is `Observable`, subscribing to it might be the most logical way of testing it. +We, however, would recommend using `ts-mockito` to increase readability. +The ways 1 and 2 portrait two options, we would recommend using the first one. + +| | 1 (preferred) | 2 | 3 | +| ------------------------------ | --------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | +| Description | - Using ts-mockito spy and then verify it has fired - Then check argument for expected value | Using ts-mockito spy and then verify it has fired with the expected value | - Using subscription and asynchronous method safeguard | +| Readability | Capturing arguments with ts-mockito might seem tricky and therefore reduces readability, but the test is done in the right order. | :heavy_check_mark: Right order, fewest lines of code | :warning: Order is reversed. | +| In case it does not emit | :heavy_check_mark: Correct line number and a missing emission is reported. | :heavy_check_mark: Correct line number and a missing emission is reported. | :warning: Test runs into timeout as the asynchronous callback is not called. | +| In case it emits another value | :heavy_check_mark: Correct line number and an incorrect value is reported. | :warning: Missing emission is reported. | :heavy_check_mark: Correct line number and an incorrect value is reported. | diff --git a/docs/project-structure.md b/docs/project-structure.md deleted file mode 100644 index b09ef3f3d3..0000000000 --- a/docs/project-structure.md +++ /dev/null @@ -1,218 +0,0 @@ - - -# Project Structure - -## File Name Conventions - -In accordance with the [Angular Style Guide](https://angular.io/guide/styleguide) and the [Angular CLI](https://angular.io/guide/file-structure) convention of naming generated elements in the file system, all file and folder names should use a hyphenated, lowercase structure (kebab-case). camelCase should not be used, especially since it can lead to problems when working with different operating systems, where some systems are case indifferent regarding file and folder naming (Windows). - -## General Folder Structure - -We decided on a folder layout to fit our project-specific needs. Basic concerns included defining a set of basic rules where components and other artifacts should be located to ease development, customization and bundling to achieve fast loading. Here we deviate from the general guidelines of Angular CLI, but we provide custom CLI schematics to easily add all artifacts to the project. - -Additionally, the custom tslint rules `project-structure` and `ban-specific-imports` should be activated to have a feedback when adding files to the project. - -The basic structure looks like this: - -```txt -src -├─ app -| ├─ core -| | ├─ models -| | ├─ store -| | ├─ facades -| | ├─ utils -| | └─ ... -| ├─ extensions -| | ├─ foo -| | └─ bar -| ├─ pages -| ├─ shared -| └─ shell -├─ assets -├─ environments -└─ theme -``` - -The `src/app` folder contains all TypeScript code (sources and tests) and HTML templates: - -- `core` contains all configuration and utility code for the main B2C application. - - `core/models` contains models for all data entities for the B2C store. - - - `core/utils` contains all utility functions that are used in multiple cases. - - `core/store` contains the main [State Management](./state-management.md), `core/facades` contains facades for accessing the state in the application. -- `shell` contains the synchronously loaded application shell (header and footer). -- `pages` contains a flat folder list of page modules and components that are only used on that corresponding page. -- `shared` contains code which is shared across multiple modules and pages. -- `extensions` contains extension modules for mainly B2B features that have minimal touch points with the B2C store. Each module (foo and bar) contains code which concerns only itself. The connection to the B2C store is implemented via lazy-loaded modules and components. - -The `src/assets` folder contains files which are statically served (pictures, mock-data, fonts, localization, ...). - -The `src/environments` folder contains environment properties which are switched by Angular CLI, also see [Angular 2: Application Settings using the CLI Environment Option](http://tattoocoder.com/angular-cli-using-the-environment-option/). - -The `src/theme` contains styling files. - ->> Components should only reside in `shared`, `shell` and `pages`. - -## Extension Folder Structure - -We decided to group additional features, that are not universally used, into extensions to keep the main sources structured. - -Each extension module can have multiple sub folders: - -- `exports`: components which lazily load additional components -- `pages`: page modules and components used on this page -- `shared`: components shared among pages for this extension -- `models`: models specific for this extension -- `services`: services specific for this extension -- `store`: ngrx handling (State, Effects, Reducer, Actions, Selectors) -- `facades`: providing access to the state management - -Optionally additional sub folders for module-scoped artifacts are allowed: - -- interceptors -- directives -- guards -- validators -- configurations -- pipes - -## Modules - -As [Angular Modules](https://angular.io/guide/ngmodules) are a rather advanced topic, beginning with the restructured project folder format, we want to give certain guidelines for which modules exist and where components are declared. The Angular modules are mainly used to feed the Angular dependency injection and with that component factories that populate the templates. It has little to do with the bundling of lazy-loaded modules when a production-ready Ahead-of-Time build is executed. - -As a general rule of thumb, modules should mainly aggregate deeper lying artifacts. Only some exceptions are allowed. - -### Extending Modules - -As a developer who **extends and customizes** the functionality of the PWA, you should only consider modifying/adding to the following modules: - -- `src/app/pages/app.routing.module` - for registering new globally available routes -- `src/app/core/core.module` and `configuration.module` - for registering core functionality (if the third party library documentation asks to add a `SomeModule.forRoot()`, this is the place) -- `src/app/shell/shell.module` - for declaring and exporting components that should be available on the application shell and also on the remaining parts of the application, do not overuse -- `src/app/shared/shared.module` - for declaring and exporting components that are used on more than one page, but not in the application shell -- `src/app/pages//-page.module` - for declaring components that are used only on this page - -As a developer who develops **new functionalities** for the PWA, you also have to deal with the following modules: - -- `src/app/core/X.module` - configuration for the main application organized in various modules -- `src/app/utils/.module` - utility modules like a CMS which supplies shared components and uses shared.module -- `src/app/shared//.module` - utility modules which aggregate functionality exported with shared.module -- `src/app/core/store/**/-store.module` - ngrx specific modules which should only be extended when adding B2C functionality. Current stores should not be extended, it is better to add additional store modules for custom functionalities. - -As a developer who adds **new stand-alone features**: - -- `src/app/extensions//.module` - aggregated collection of components used for this extension, including the ngrx store -- `src/app/extensions//exports/-exports.module` - aggregation of lazy components which lazily load the extension module - -When using `ng generate` with our PWA custom schematics, the components should automatically be declared in the correct modules. - -## Rules for Component Development - -### Declare Components in the Right NgModule - -Angular requires you to declare a component in one and only one NgModule. Find the right one in the following order: - -*Your Component is used only on one page?* - Add it to the declarations of the corresponding page.module. - -*Your Component is used among multiple pages?* - Declare it in the shared.module and also export it there. - -*Your Component is used in the application shell (and maybe again on certain pages)?* - Declare it in the shell.module and also export it there. - -*(advanced) Your component relates to a specific B2B extension?* - Declare it in that extension module and add it as an entryComponent, add a lazy-loaded component and add that to the extension exports, which are then im-/exported in the shared.module. - -When using `ng generate`, the right module should be found automatically. - -### Do not use NgRx or Services in Components - -Using NgRx or Services directly in components violates our model of abstraction. Only facades should be used in components, as they provide the simplest access to the business logic. - -### Delegate Complex Component Logic to Services - -There should not be any string or URL manipulation, routing mapping or REST endpoint string handling within components. This is supposed to be handled by methods of services. See also [Angular Style Guide](https://angular.io/guide/styleguide#style-05-15). - -### Put as Little Logic Into `constructor` as Possible - Use `ngOnInit` - -See [The essential difference between Constructor and ngOnInit in Angular](https://blog.angularindepth.com/the-essential-difference-between-constructor-and-ngoninit-in-angular-c9930c209a42) and [Angular constructor versus ngOnInit](https://ultimatecourses.com/blog/angular-constructor-ngoninit-lifecycle-hook). - -### Use Property Binding to Bind Dynamic Values to Attributes or Properties - -See [Explanation of the difference between an HTML attribute and a DOM property](https://angular.io/guide/template-syntax#html-attribute-vs-dom-property). - -There are often two ways to bind values dynamically to attributes or properties: interpolation or property binding. -In the PWA we prefer using property binding since this covers more cases in the same way. So the code will be more consistent. - -There is an exception for direct string value bindings where we use for example `routerLink="/logout"` instead of `[routerLink]="'/logout'"`. - -Pattern to avoid: -- `
` -- `` - -Prefer: -- `
` -- `` - -### Pattern for Conditions (ngif) with Alternative Template (else) in Component Templates - -Also for consistency reasons, we want to establish the following pattern for conditions in component templates: - -```typescript - - ... (template code for if-branch) - - - - ... (template code for else-branch) - -``` - -This pattern provides the needed flexibility if used together with handling observables with `*ngIf` and the `async` pipe. -In this case the condition should look like this: - -```typescript - -``` - -### Do Not Unsubscribe, Use Destroy Observable and takeUntil Instead - -Following the ideas of the article [RxJS: Don’t Unsubscribe](https://medium.com/@benlesh/rxjs-dont-unsubscribe-6753ed4fda87), the following pattern is used for ending subscriptions to observables that are not handled via async pipe in the templates. - -```typescript -export class AnyComponent implements OnInit, OnDestroy { - ... - private destroy$ = new Subject(); - ... - ngOnInit() { - ... - observable$.pipe(takeUntil(this.destroy$)) - .subscribe(/* ... */); - } - ... - ngOnDestroy() { - this.destroy$.next(); - } -} -``` - -### Use `OnPush` Change Detection if Possible - -To reduce the number of ChangeDetection computation cycles, all components should have their `Component` decorator property `changeDetection` set to `ChangeDetectionStrategy.OnPush`. - -### Split Components When Necessary - -Consider splitting one into multiple components when: - -- **Size**: Component code becomes too complex to be easily understandable - -- **Separation of concerns**: A component serves different concerns that should be separated - -- **Reusability**: A component should be reused in different contexts. This can introduce a shared component which could be placed in a shared module. - -- **Async data**: Component relies on async data from the store which makes the component code unnecessarily complex. Use a container component then which resolves the observables at the outside of the child component and passes data in via property bindings. Do not do this for simple cases. - -Single-use dumb components are always okay if it improves readability. diff --git a/docs/search-engine-optimization.md b/docs/search-engine-optimization.md deleted file mode 100644 index 25f6758574..0000000000 --- a/docs/search-engine-optimization.md +++ /dev/null @@ -1,34 +0,0 @@ - - -# Search Engine Optimization (SEO) - -This section documents our approach for Search Engine Optimization for the Intershop Progressive Web App. - -## Server Side Rendering - -The PWA uses Universal for pre-rendering complete pages to tackle SEO concerns. An Angular application without Universal support will not respond to web crawlers with complete indexable page responses. - -Angular's state transfer mechanism is used to transfer properties to the client side. We use it to de-hydrate the ngrx state in the server application and re-hydrate it on the client side. See [Using TransferState API in an Angular v5 Universal App](https://medium.com/angular-in-depth/using-transferstate-api-in-an-angular-5-universal-app-130f3ada9e5b) for specifics. - -Follow the steps in the Getting Started to build and run the application in Universal mode. - -Official Documentation for Angular Universal can be found at https://angular.io/guide/universal. - -## robots.txt - -We use the library [express-robots-txt](https://github.com/modosc/express-robots-txt) in the express.js server (`server.ts` in the project root) to supply a response to `robots.txt` for crawlers. - -By default the universal server provides a response with access to all pages except some restricted paths (e.g. `/error` or `/account`). To use a custom `robots.txt` place it as a file in the `dist` folder. - -## Page Metadata - -The PWA uses the library [@ngx-meta/core](https://www.npmjs.com/package/@ngx-meta/core) for setting tags for title, meta description, robots, canonical links and open graph infos in page headers. It is also possible to use translation keys here. - -The process is triggered by adding the guard `MetaGuard` to the routing, this is automatically done for all routes in the seo module. The default `MetaSettings` are also configured in this guard. - -`seo.effects.ts` is the central place for customizations concerning dynamic content, e.g. names of products or categories (asynchronous data from the API). Effects are an essential part of our [State Management](./state-management.md). diff --git a/docs/state-management.md b/docs/state-management.md deleted file mode 100644 index 0671021991..0000000000 --- a/docs/state-management.md +++ /dev/null @@ -1,194 +0,0 @@ - - -# State Management - -This section describes how [NgRx](https://ngrx.io/) is integrated into the Intershop Progressive Web App for the application wide state management. - -## Architecture - -![State Management](state-management.svg "State Management") - -NgRx is a framework for handling state information in Angular applications following the Redux pattern. It consist of a few basic parts: - -### State - -The state is seen as the single source of truth for getting information of the current application state. There is only one immutable state per application, which is composed of substates. To get information out of the state, selectors have to be used. Changing the state can only be done by dispatching actions. - -### Selectors - -Selectors are functions used to retrieve information about the current state from the store. The selectors are grouped in a separate file. They always start the query from the root of the state tree and navigate to the required information. Selectors return observables which can be held in containers and be bound to in templates. - -### Actions - -Actions are simple objects used to alter the current state via reducers or trigger effects. Action creators are held in a separate file. The action class contains a type of the action and an optional payload. To alter the state synchronously, reducers have to be composed. To alter the state asynchronously, effects are used. - -### Reducers - -Reducers are pure functions which alter the state synchronously. They take the previous state and an incoming action to compose a new state. This state is then published and all listening components react automatically to the new state information. Reducers should be simple operations which are easily testable. - -### Effects - -Effects use incoming actions to trigger asynchronous tasks like querying REST resources. After successful or erroneous completion an effect might trigger another action as a way to alter the current state of the application. - -### Facades - -Facades are injectable instances which provide simplified access to the store via exposed observables and action dispatcher methods. They should be used in Angular components but not within NgRx artifacts themselves. - -## File Structure - -The file structure looks like this: - -``` -src/app/core - ├─ facades - | └─ foobar.facade.ts - └─ store - └─ foobar - ├─ foo - | ├─ foo.actions.ts - | ├─ foo.effects.ts - | ├─ foo.reducer.ts - | ├─ foo.selectors.ts - | └─ index.ts - ├─ bar - | └─ ... - ├─ foobar.state.ts - └─ foobar.system.ts -``` - -An application module named `foobar` with substates named `foo` and `bar` serves as an example. The files handling NgRx store should then be contained in the folder `foobar`. Each substate should aggregate its store components in separate subfolders correspondingly named `foo` and `bar`: - -- *foo.actions.ts*: This file contains all action creators for the `foo` state. Additionally, a bundle type aggregating all action creators and an enum type with all action types is contained here. - -- *foo.effects.ts*: This file defines an effect class with all its containing effect implementations for the `FooState`. - -- *foo.reducer.ts*: This file exports a reducer function which modifies the state of `foo`. Additionally, the `FooState` and its `initialState` is contained here. - -- *foo.selectors.ts*: This file exports all selectors working on the state of `foo`. - -- *index.ts*: This file exports the public API for the state of the `foo` substate. In here all specific selectors and actions are exported. - -Furthermore, the state of foobar is aggregated in two files: - -- *foobar.state.ts*: Contains the `FoobarState` as an aggregate of the `foo` and `bar` states. - -- *foobar.system.ts*: Contains aggregations for `foobarReducers` and `foobarEffects` of the corresponding substates to be used in modules and `TestBed` declarations. - -Access to the state slice of `foobar` is provided with the `FoobarFacade` located in *foobar.facade.ts* - -## Naming - -Related to the example in the previous paragraph we want to establish a particular naming scheme. - -### Actions - Types - -Action types should be aggregated in an enum type. The enum should be composed of the substate name and 'ActionTypes'. The key of the type should be written in PascalCase. The string value of the type should contain the feature in brackets and a readable action description. The description should give hints about the dispatcher of the said action, i.e., actions dispatched due to a HTTP service response should have 'API' in their name, actions dispatched by other actions should have 'Internal' in their description. - -```typescript -export enum FooActionTypes { - LoadFoo = '[Foo Internal] Load Foo', - InsertFoo = '[Foo] Insert Foo', - LoadFooSuccess = '[Foo API] Load Foo Success', - ... -} -``` - -### Actions - Creators - -The action creator is a class with an optional payload member. Its PascalCase name should correspond to an action type. The name should not contain 'Action' as the action is always dispatched via the store and it is therefor implicitly correctly named. - -```typescript -export class LoadFoo implements Action { - readonly type = FooActionTypes.LoadFoo; - constructor(public payload: string) { } -} -``` - -### Actions - Bundle - -The file *actions.ts* should also contain an action bundle type with the name of the substate + 'Action', which is to be used in the reducer and tests. - -```typescript -export type FooAction = LoadFoo | SaveFoo | ... -``` - -### Reducer - -The exported function for the reducer should be named like the substate + 'Reducer' in camelCase. - -```typescript -export function fooReducer(state = initialState, action: FooAction): FooState { -``` - -### State - -State interfaces should have the state name followed by 'State' in PascalCase. - -```typescript -export interface FooState { -... -``` - -### Selectors - -Selectors should always be camelCase and start with 'get' or 'is'. - -```typescript -export const getSelectedFoo = createSelector( ... -``` - -### Facades - Streams - -Any field ending with $ indicates that a stream is supplied. i.e. `foos$()`, `bars$`, `foo$(id)`. The facade takes care that the stream will be loaded or initialized. The naming should just refer to the object itself without any verbs. - -### Facades - Action Dispatchers - -Action dispatcher helpers are represented by methods with verbs. i.e. `addFoo(foo)`, `deleteBar(id)`, `clearFoos()`. - -## Entity State Adapter for Managing Record Collections: @ngrx/entity - -[@ngrx/entity](https://ngrx.io/guide/entity) provides an API to manipulate and query entity collections. - -- Reduces boilerplate for creating reducers that manage a collection of models. -- Provides performant CRUD operations for managing entity collections. -- Extensible type-safe adapters for selecting entity information. - -## Normalized State - -It is important to have a normalized state when working with ngrx. To give an example, only the product's state should save products. Every other slice of the state that also uses products must only save identifiers (in this case SKUs) for products. In selectors the data can be linked into views to be easily usable by components. - -see: [NgRx: Normalizing state](https://medium.com/@timdeschryver/ngrx-normalizing-state-d3960a86a3aa) - -## ngrx Pitfalls - -### Using Services and catchError - -The operator handling the possible error of a service call must always be contained in the returned observable of the service call, otherwise it has no effect. - -See: [Handling Errors in NgRx Effects](https://medium.com/city-pantry/handling-errors-in-ngrx-effects-a95d918490d9) - -```typescript -@Effect() -effect = this.actions$.pipe( - ofType(ActionLoad), - switchMap(this.service.method().pipe( - map(x => new ActionSuccess(x)), - mapErrorToAction(ActionFail) - ), -) -``` - -### Using `switchMap` can Lead to Race Conditions - -If in doubt, use `concatMap`. - -See [RxJS: Avoiding switchMap-Related Bugs](https://medium.com/angular-in-depth/switchmap-bugs-b6de69155524) - -### Should I put XYZ into the Store or the Component? - -See: [SHARI-Principle](https://ngrx.io/docs#when-should-i-use-ngrx-for-state-management) diff --git a/package.json b/package.json index ef2fb2c153..ff6e559c20 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "e2e": "cd e2e && npm install && node open-cypress", "lint": "ng lint", "stylelint": "stylelint \"src/**/*.scss\" --fix", - "format": "prettier --loglevel warn --write \"**/*.*\"", + "format": "node docs/check-sentence-newline && prettier --loglevel warn --write \"**/*.*\"", "dead-code": "npx ts-node scripts/find-dead-code.ts", "check": "npm run lint -- --fix --format stylish && npm run format && npm run stylelint && tsc --project tsconfig.spec.json && npm run build && npm run dead-code && npm run test", "changelog": "npx -p conventional-changelog-cli conventional-changelog -n intershop-changelog.js -i CHANGELOG.md -s", @@ -157,7 +157,15 @@ }, "lint-staged": { "linters": { - "!(*.ts)": [ + "!(*.ts|docs/**/*.md)": [ + "npx prettier --loglevel warn --write", + "git add" + ], + "docs/**/*.md": [ + "node docs/check-kb-labels", + "node docs/check-dead-links fast", + "node docs/check-documentation-overview", + "node docs/check-sentence-newline", "npx prettier --loglevel warn --write", "git add" ], diff --git a/src/app/core/services/products/products.service.ts b/src/app/core/services/products/products.service.ts index d7bf8b7b04..560c5fff95 100644 --- a/src/app/core/services/products/products.service.ts +++ b/src/app/core/services/products/products.service.ts @@ -98,6 +98,7 @@ export class ProductsService { * Get products for a given search term respecting pagination. * @param searchTerm The search term to look for matching products. * @param page The page to request (1-based numbering) + * @param sortKey The sortKey to sort the list, default value is ''. * @returns A list of matching Product stubs with a list of possible sortings and the total amount of results. */ searchProducts(