diff --git a/projects/frontend/shared-components/gui/.editorconfig b/projects/frontend/shared-components/gui/.editorconfig new file mode 100644 index 0000000000..5eccf58ea0 --- /dev/null +++ b/projects/frontend/shared-components/gui/.editorconfig @@ -0,0 +1,23 @@ +# Editor configuration, see https://editorconfig.org +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 4 +insert_final_newline = true +trim_trailing_whitespace = true + +[*.{ts, js}] +quote_type = single + +[*.js] +max_line_length = off + +[*.{json, scss}] +quote_type = double +indent_size = 2 + +[*.md] +max_line_length = off +trim_trailing_whitespace = false diff --git a/projects/frontend/shared-components/gui/.eslintignore b/projects/frontend/shared-components/gui/.eslintignore new file mode 100644 index 0000000000..91bbe59907 --- /dev/null +++ b/projects/frontend/shared-components/gui/.eslintignore @@ -0,0 +1,4 @@ +projects/shared/src/main.ts +projects/documentation-ui/src/main.ts +projects/documentation-ui/src/test.ts +polyfills.ts diff --git a/projects/frontend/shared-components/gui/.eslintrc.json b/projects/frontend/shared-components/gui/.eslintrc.json new file mode 100644 index 0000000000..9cf9e07c32 --- /dev/null +++ b/projects/frontend/shared-components/gui/.eslintrc.json @@ -0,0 +1,108 @@ +{ + "root": true, + "ignorePatterns": [ + "projects/**/*", + "**/node_modules/*" + ], + "settings": { + "import/parsers": { + "@typescript-eslint/parser": [ + ".ts", + ".tsx" + ] + }, + "import/resolver": { + "typescript": { + "project": [ + "tsconfig.json" + ] + }, + "node": { + "project": [ + "tsconfig.json" + ] + } + } + }, + "overrides": [ + { + "files": [ + "*.ts" + ], + "parserOptions": { + "project": [ + "tsconfig.json" + ], + "createDefaultProgram": true + }, + "extends": [ + "plugin:@typescript-eslint/recommended", + "plugin:@typescript-eslint/recommended-requiring-type-checking", + "plugin:@angular-eslint/recommended", + "plugin:@angular-eslint/template/process-inline-templates", + "plugin:import/recommended", + "plugin:import/typescript", + "plugin:ngrx/recommended", + "plugin:rxjs/recommended" + ], + "plugins": [ + "ngrx", + "prefer-arrow", + "rxjs" + ], + "rules": { + "@typescript-eslint/no-explicit-any": "warn", + "@typescript-eslint/no-unsafe-assignment": "warn", + "@typescript-eslint/dot-notation": [ + "warn", + { + "allowPrivateClassPropertyAccess": true, + "allowProtectedClassPropertyAccess": true, + "allowIndexSignaturePropertyAccess": true + } + ], + "@typescript-eslint/unbound-method": "warn", + "@typescript-eslint/no-floating-promises": "warn", + "@typescript-eslint/ban-ts-comment": "warn", + "@typescript-eslint/no-unsafe-argument": "error", + "@typescript-eslint/no-unused-vars": [ + "warn", + { + "argsIgnorePattern": "^_" + } + ], + "ngrx/prefer-effect-callback-in-block-statement": "off", + "ngrx/use-consistent-global-store-name": "off", + "ngrx/prefer-action-creator": "off", + "ngrx/no-typed-global-store": "off", + "ngrx/prefer-selector-in-select": "off", + "no-underscore-dangle": "off" + } + }, + { + "files": [ + "*.html" + ], + "extends": [ + "plugin:@angular-eslint/template/recommended" + ], + "rules": { + "@angular-eslint/template/conditional-complexity": [ + "warn", + { + "maxComplexity": 10 + } + ], + "@angular-eslint/template/cyclomatic-complexity": [ + "warn", + { + "maxComplexity": 10 + } + ], + "@angular-eslint/template/no-positive-tabindex": "warn", + "@angular-eslint/template/use-track-by-function": "off", + "@angular-eslint/template/no-call-expression": "warn" + } + } + ] +} diff --git a/projects/frontend/shared-components/gui/.gitignore b/projects/frontend/shared-components/gui/.gitignore index 53a8849eea..fc257e9a4f 100644 --- a/projects/frontend/shared-components/gui/.gitignore +++ b/projects/frontend/shared-components/gui/.gitignore @@ -41,6 +41,7 @@ npm-debug.log yarn-error.log testem.log /typings +/reports/** # System Files .DS_Store diff --git a/projects/frontend/shared-components/gui/.npmrc b/projects/frontend/shared-components/gui/.npmrc new file mode 100644 index 0000000000..521a9f7c07 --- /dev/null +++ b/projects/frontend/shared-components/gui/.npmrc @@ -0,0 +1 @@ +legacy-peer-deps=true diff --git a/projects/frontend/shared-components/gui/.nvmrc b/projects/frontend/shared-components/gui/.nvmrc new file mode 100644 index 0000000000..99cdd8009c --- /dev/null +++ b/projects/frontend/shared-components/gui/.nvmrc @@ -0,0 +1 @@ +16.15.0 diff --git a/projects/frontend/shared-components/gui/.prettierrc.json b/projects/frontend/shared-components/gui/.prettierrc.json new file mode 100644 index 0000000000..b09315e4c1 --- /dev/null +++ b/projects/frontend/shared-components/gui/.prettierrc.json @@ -0,0 +1,26 @@ +{ + "tabWidth": 2, + "useTabs": true, + "semi": true, + "singleQuote": true, + "quoteProps": "as-needed", + "trailingComma": "none", + "bracketSpacing": true, + "jsxBracketSameLine": true, + "arrowParens": "always", + "insertPragma": false, + "overrides": [ + { + "files": "*.component.html", + "options": { + "parser": "angular" + } + }, + { + "files": "*.html", + "options": { + "parser": "html" + } + } + ] +} diff --git a/projects/frontend/shared-components/gui/README.md b/projects/frontend/shared-components/gui/README.md index 2aa2c0e0f6..31d993013d 100644 --- a/projects/frontend/shared-components/gui/README.md +++ b/projects/frontend/shared-components/gui/README.md @@ -22,7 +22,7 @@ Bug in general the steps are: ```bash $ npm config set legacy-peer-deps true ``` -1. Install Angular Cli and all other dependencies except '@taurus/shared' +1. Install Angular Cli and all other dependencies except '@vdk/shared' ```bash npm i -g @angular/cli ``` @@ -64,6 +64,3 @@ Run `npm run lint` to execute lint via [ESLint](https://eslint.org/docs/user-gui _**Note**_: Code coverage report will be generated in `reports/coverage`(for the UI Document Wrapper and the Shared Components Library) _**Note**_: Code coverage report for the Shared UI Components library will be also logged in the Console(and consumed by the CI/CD) - -[E2E] Run `npm run e2e` to open [Cypress](https://www.cypress.io/) dev tool for e2e testing -- Cypress environment variables must be supplied providing credentials for needed infrastructure diff --git a/projects/frontend/shared-components/gui/angular.json b/projects/frontend/shared-components/gui/angular.json new file mode 100644 index 0000000000..226088c167 --- /dev/null +++ b/projects/frontend/shared-components/gui/angular.json @@ -0,0 +1,214 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "cli": { + "analytics": false, + "defaultCollection": "@angular-eslint/schematics", + "packageManager": "npm" + }, + "version": 1, + "newProjectRoot": "projects", + "projects": { + "shared": { + "projectType": "library", + "root": "projects/shared", + "sourceRoot": "projects/shared/src", + "prefix": "shared", + "architect": { + "build": { + "builder": "@angular-devkit/build-angular:ng-packagr", + "options": { + "tsConfig": "projects/shared/tsconfig.lib.json", + "project": "projects/shared/ng-package.json" + }, + "configurations": { + "production": { + "tsConfig": "projects/shared/tsconfig.lib.prod.json", + "project": "projects/shared/ng-package.json" + }, + "development": {} + }, + "defaultConfiguration": "production" + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "projects/shared/src/test.ts", + "tsConfig": "projects/shared/tsconfig.spec.json", + "karmaConfig": "projects/shared/karma.conf.js", + "codeCoverage": true, + "codeCoverageExclude": [ + "projects/shared/src/**/*component.ts", + "projects/shared/src/lib/unit-testing/**/*" + ], + "sourceMap": { + "scripts": true, + "vendor": true, + "styles": true + } + } + }, + "lint": { + "builder": "@angular-eslint/builder:lint", + "options": { + "lintFilePatterns": [ + "projects/shared/**/*.ts", + "projects/shared/**/*.html" + ] + } + } + }, + "schematics": { + "@schematics/angular:component": { + "style": "scss" + } + } + }, + "documentation-ui": { + "projectType": "application", + "root": "projects/documentation-ui", + "sourceRoot": "projects/documentation-ui/src", + "prefix": "app", + "architect": { + "build": { + "builder": "@angular-devkit/build-angular:browser", + "options": { + "outputPath": "dist/documentation-ui", + "index": "projects/documentation-ui/src/index.html", + "main": "projects/documentation-ui/src/main.ts", + "polyfills": "projects/documentation-ui/src/polyfills.ts", + "tsConfig": "projects/documentation-ui/tsconfig.app.json", + "allowedCommonJsDependencies": [ + "dagre", + "webcola" + ], + "assets": [ + "projects/documentation-ui/src/favicon.ico", + "projects/documentation-ui/src/assets", + { + "glob": "clr-ui*.min.*", + "input": "node_modules/@clr/ui", + "output": "assets/css" + }, + { + "glob": "**/*", + "input": "node_modules/@vdk/shared/assets", + "output": "/assets/" + } + ], + "styles": [ + "node_modules/@clr/icons/clr-icons.min.css", + "projects/documentation-ui/src/styles.scss" + ], + "scripts": [ + "node_modules/@clr/icons/clr-icons.min.js" + ] + }, + "configurations": { + "development": { + "sourceMap": { + "scripts": true, + "vendor": true, + "styles": true + }, + "vendorChunk": true, + "extractLicenses": false, + "buildOptimizer": false, + "optimization": false, + "namedChunks": true, + "preserveSymlinks": true + }, + "production": { + "fileReplacements": [ + { + "replace": "projects/documentation-ui/src/environments/environment.ts", + "with": "projects/documentation-ui/src/environments/environment.prod.ts" + } + ], + "sourceMap": false, + "vendorChunk": false, + "extractLicenses": true, + "buildOptimizer": true, + "optimization": true, + "namedChunks": false, + "outputHashing": "all", + "budgets": [ + { + "type": "initial", + "maximumWarning": "2mb", + "maximumError": "5mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "6kb", + "maximumError": "10kb" + } + ] + } + }, + "defaultConfiguration": "development" + }, + "serve": { + "builder": "@angular-devkit/build-angular:dev-server", + "options": { + "host": "localhost.vmware.com", + "port": 4200, + "proxyConfig": "proxy.config.json" + }, + "configurations": { + "development": { + "browserTarget": "documentation-ui:build:development" + }, + "production": { + "browserTarget": "documentation-ui:build:production" + } + }, + "defaultConfiguration": "development" + }, + "extract-i18n": { + "builder": "@angular-devkit/build-angular:extract-i18n", + "options": { + "browserTarget": "documentation-ui:build" + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "projects/documentation-ui/src/test.ts", + "polyfills": "projects/documentation-ui/src/polyfills.ts", + "tsConfig": "projects/documentation-ui/tsconfig.spec.json", + "karmaConfig": "projects/documentation-ui/karma.conf.js", + "codeCoverage": true, + "codeCoverageExclude": [ + "projects/documentation-ui/src/**/*component.ts" + ], + "assets": [ + "projects/documentation-ui/src/favicon.ico", + "projects/documentation-ui/src/assets" + ], + "styles": [ + "projects/documentation-ui/src/styles.scss" + ], + "scripts": [ + "node_modules/@clr/icons/clr-icons.min.js" + ] + } + }, + "lint": { + "builder": "@angular-eslint/builder:lint", + "options": { + "lintFilePatterns": [ + "projects/documentation-ui/**/*.ts", + "projects/documentation-ui/**/*.html" + ] + } + } + }, + "schematics": { + "@schematics/angular:component": { + "style": "scss" + } + } + } + }, + "defaultProject": "shared" +} diff --git a/projects/frontend/shared-components/gui/package-lock.json b/projects/frontend/shared-components/gui/package-lock.json new file mode 100644 index 0000000000..803ce18d13 --- /dev/null +++ b/projects/frontend/shared-components/gui/package-lock.json @@ -0,0 +1,16476 @@ +{ + "name": "shared-wrapper", + "version": "3.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "shared-wrapper", + "version": "3.0.0", + "dependencies": { + "@angular/animations": "13.3.10", + "@angular/cdk": "13.3.8", + "@angular/common": "13.3.10", + "@angular/compiler": "13.3.10", + "@angular/core": "13.3.10", + "@angular/forms": "13.3.10", + "@angular/platform-browser": "13.3.10", + "@angular/platform-browser-dynamic": "13.3.10", + "@angular/router": "13.3.10", + "@cds/core": "5.7.2", + "@clr/angular": "13.3.1", + "@clr/icons": "13.0.2", + "@clr/ui": "13.3.1", + "@ngrx/effects": "13.2.0", + "@ngrx/router-store": "13.2.0", + "@ngrx/store": "13.2.0", + "@swimlane/ngx-graph": "8.0.0", + "@webcomponents/custom-elements": "1.5.0", + "angular-ng-autocomplete": "2.0.8", + "d3": "5.16.0", + "dompurify": "2.3.8", + "lodash": "4.17.21", + "ngx-clipboard": "15.1.0", + "ngx-cookie-service": "13.2.1", + "rxjs": "7.5.5", + "tslib": "2.3.1", + "zone.js": "0.11.7" + }, + "devDependencies": { + "@angular-devkit/build-angular": "13.3.7", + "@angular-devkit/schematics": "13.3.7", + "@angular-eslint/builder": "13.2.1", + "@angular-eslint/eslint-plugin": "13.2.1", + "@angular-eslint/eslint-plugin-template": "13.2.1", + "@angular-eslint/schematics": "13.2.1", + "@angular-eslint/template-parser": "13.2.1", + "@angular/cli": "13.3.7", + "@angular/compiler-cli": "13.3.10", + "@neuralegion/cypress-har-generator": "5.3.0", + "@ngrx/schematics": "13.2.0", + "@ngrx/store-devtools": "13.2.0", + "@types/jasmine": "4.0.3", + "@types/lodash": "4.14.182", + "@types/node": "17.0.35", + "@typescript-eslint/eslint-plugin": "5.26.0", + "@typescript-eslint/parser": "5.26.0", + "angular-oauth2-oidc": "13.0.1", + "cypress": "9.7.0", + "cypress-grep": "2.13.1", + "cypress-localstorage-commands": "1.7.0", + "cypress-terminal-report": "3.5.2", + "eslint": "8.16.0", + "eslint-import-resolver-typescript": "2.7.1", + "eslint-plugin-cypress": "2.12.1", + "eslint-plugin-import": "2.26.0", + "eslint-plugin-jasmine": "4.1.3", + "eslint-plugin-jsdoc": "39.3.2", + "eslint-plugin-ngrx": "2.1.2", + "eslint-plugin-prefer-arrow": "1.2.3", + "eslint-plugin-prettier": "4.0.0", + "eslint-plugin-rxjs": "5.0.2", + "jasmine-core": "4.1.1", + "jasmine-spec-reporter": "7.0.0", + "karma": "6.3.20", + "karma-chrome-launcher": "3.1.1", + "karma-coverage": "2.2.0", + "karma-jasmine": "5.0.1", + "karma-jasmine-html-reporter": "1.7.0", + "karma-junit-reporter": "2.0.1", + "ng-packagr": "13.3.1", + "npm-run-all": "4.1.5", + "prettier": "2.6.2", + "rimraf": "3.0.2", + "rxjs-marbles": "7.0.1", + "start-server-and-test": "1.14.0", + "typescript": "4.6.4" + }, + "engines": { + "node": "^14.15.0 || >=16.10.0", + "npm": ">=8.5.5" + }, + "optionalDependencies": { + "@vdk/shared": "0.0.0" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.2.0", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.1.0", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@angular-devkit/architect": { + "version": "0.1303.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "13.3.7", + "rxjs": "6.6.7" + }, + "engines": { + "node": "^12.20.0 || ^14.15.0 || >=16.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-devkit/architect/node_modules/rxjs": { + "version": "6.6.7", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" + } + }, + "node_modules/@angular-devkit/architect/node_modules/tslib": { + "version": "1.14.1", + "dev": true, + "license": "0BSD" + }, + "node_modules/@angular-devkit/build-angular": { + "version": "13.3.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "2.2.0", + "@angular-devkit/architect": "0.1303.7", + "@angular-devkit/build-webpack": "0.1303.7", + "@angular-devkit/core": "13.3.7", + "@babel/core": "7.16.12", + "@babel/generator": "7.16.8", + "@babel/helper-annotate-as-pure": "7.16.7", + "@babel/plugin-proposal-async-generator-functions": "7.16.8", + "@babel/plugin-transform-async-to-generator": "7.16.8", + "@babel/plugin-transform-runtime": "7.16.10", + "@babel/preset-env": "7.16.11", + "@babel/runtime": "7.16.7", + "@babel/template": "7.16.7", + "@discoveryjs/json-ext": "0.5.6", + "@ngtools/webpack": "13.3.7", + "ansi-colors": "4.1.1", + "babel-loader": "8.2.5", + "babel-plugin-istanbul": "6.1.1", + "browserslist": "^4.9.1", + "cacache": "15.3.0", + "circular-dependency-plugin": "5.2.2", + "copy-webpack-plugin": "10.2.1", + "core-js": "3.20.3", + "critters": "0.0.16", + "css-loader": "6.5.1", + "esbuild-wasm": "0.14.22", + "glob": "7.2.0", + "https-proxy-agent": "5.0.0", + "inquirer": "8.2.0", + "jsonc-parser": "3.0.0", + "karma-source-map-support": "1.4.0", + "less": "4.1.2", + "less-loader": "10.2.0", + "license-webpack-plugin": "4.0.2", + "loader-utils": "3.2.0", + "mini-css-extract-plugin": "2.5.3", + "minimatch": "3.0.5", + "open": "8.4.0", + "ora": "5.4.1", + "parse5-html-rewriting-stream": "6.0.1", + "piscina": "3.2.0", + "postcss": "8.4.5", + "postcss-import": "14.0.2", + "postcss-loader": "6.2.1", + "postcss-preset-env": "7.2.3", + "regenerator-runtime": "0.13.9", + "resolve-url-loader": "5.0.0", + "rxjs": "6.6.7", + "sass": "1.49.9", + "sass-loader": "12.4.0", + "semver": "7.3.5", + "source-map-loader": "3.0.1", + "source-map-support": "0.5.21", + "stylus": "0.56.0", + "stylus-loader": "6.2.0", + "terser": "5.11.0", + "text-table": "0.2.0", + "tree-kill": "1.2.2", + "tslib": "2.3.1", + "webpack": "5.70.0", + "webpack-dev-middleware": "5.3.0", + "webpack-dev-server": "4.7.3", + "webpack-merge": "5.8.0", + "webpack-subresource-integrity": "5.1.0" + }, + "engines": { + "node": "^12.20.0 || ^14.15.0 || >=16.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "optionalDependencies": { + "esbuild": "0.14.22" + }, + "peerDependencies": { + "@angular/compiler-cli": "^13.0.0 || ^13.3.0-rc.0", + "@angular/localize": "^13.0.0 || ^13.3.0-rc.0", + "@angular/service-worker": "^13.0.0 || ^13.3.0-rc.0", + "karma": "^6.3.0", + "ng-packagr": "^13.0.0", + "protractor": "^7.0.0", + "tailwindcss": "^2.0.0 || ^3.0.0", + "typescript": ">=4.4.3 <4.7" + }, + "peerDependenciesMeta": { + "@angular/localize": { + "optional": true + }, + "@angular/service-worker": { + "optional": true + }, + "karma": { + "optional": true + }, + "ng-packagr": { + "optional": true + }, + "protractor": { + "optional": true + }, + "tailwindcss": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/rxjs": { + "version": "6.6.7", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/rxjs/node_modules/tslib": { + "version": "1.14.1", + "dev": true, + "license": "0BSD" + }, + "node_modules/@angular-devkit/build-webpack": { + "version": "0.1303.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/architect": "0.1303.7", + "rxjs": "6.6.7" + }, + "engines": { + "node": "^12.20.0 || ^14.15.0 || >=16.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "webpack": "^5.30.0", + "webpack-dev-server": "^4.0.0" + } + }, + "node_modules/@angular-devkit/build-webpack/node_modules/rxjs": { + "version": "6.6.7", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" + } + }, + "node_modules/@angular-devkit/build-webpack/node_modules/tslib": { + "version": "1.14.1", + "dev": true, + "license": "0BSD" + }, + "node_modules/@angular-devkit/core": { + "version": "13.3.7", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "8.9.0", + "ajv-formats": "2.1.1", + "fast-json-stable-stringify": "2.1.0", + "magic-string": "0.25.7", + "rxjs": "6.6.7", + "source-map": "0.7.3" + }, + "engines": { + "node": "^12.20.0 || ^14.15.0 || >=16.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^3.5.2" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/core/node_modules/rxjs": { + "version": "6.6.7", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" + } + }, + "node_modules/@angular-devkit/core/node_modules/tslib": { + "version": "1.14.1", + "dev": true, + "license": "0BSD" + }, + "node_modules/@angular-devkit/schematics": { + "version": "13.3.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "13.3.7", + "jsonc-parser": "3.0.0", + "magic-string": "0.25.7", + "ora": "5.4.1", + "rxjs": "6.6.7" + }, + "engines": { + "node": "^12.20.0 || ^14.15.0 || >=16.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-devkit/schematics/node_modules/rxjs": { + "version": "6.6.7", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" + } + }, + "node_modules/@angular-devkit/schematics/node_modules/tslib": { + "version": "1.14.1", + "dev": true, + "license": "0BSD" + }, + "node_modules/@angular-eslint/builder": { + "version": "13.2.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@nrwl/devkit": "13.1.3" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0", + "typescript": "*" + } + }, + "node_modules/@angular-eslint/bundled-angular-compiler": { + "version": "13.2.1", + "dev": true, + "license": "MIT" + }, + "node_modules/@angular-eslint/eslint-plugin": { + "version": "13.2.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-eslint/utils": "13.2.1", + "@typescript-eslint/experimental-utils": "5.17.0" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0", + "typescript": "*" + } + }, + "node_modules/@angular-eslint/eslint-plugin-template": { + "version": "13.2.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-eslint/bundled-angular-compiler": "13.2.1", + "@typescript-eslint/experimental-utils": "5.17.0", + "aria-query": "^4.2.2", + "axobject-query": "^2.2.0" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0", + "typescript": "*" + } + }, + "node_modules/@angular-eslint/schematics": { + "version": "13.2.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-eslint/eslint-plugin": "13.2.1", + "@angular-eslint/eslint-plugin-template": "13.2.1", + "ignore": "5.2.0", + "strip-json-comments": "3.1.1", + "tmp": "0.2.1" + }, + "peerDependencies": { + "@angular/cli": ">= 13.0.0 < 14.0.0" + } + }, + "node_modules/@angular-eslint/template-parser": { + "version": "13.2.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-eslint/bundled-angular-compiler": "13.2.1", + "eslint-scope": "^5.1.0" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0", + "typescript": "*" + } + }, + "node_modules/@angular-eslint/utils": { + "version": "13.2.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-eslint/bundled-angular-compiler": "13.2.1", + "@typescript-eslint/experimental-utils": "5.17.0" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0", + "typescript": "*" + } + }, + "node_modules/@angular/animations": { + "version": "13.3.10", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^12.20.0 || ^14.15.0 || >=16.10.0" + }, + "peerDependencies": { + "@angular/core": "13.3.10" + } + }, + "node_modules/@angular/cdk": { + "version": "13.3.8", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "optionalDependencies": { + "parse5": "^5.0.0" + }, + "peerDependencies": { + "@angular/common": "^13.0.0 || ^14.0.0-0", + "@angular/core": "^13.0.0 || ^14.0.0-0", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/cli": { + "version": "13.3.7", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/architect": "0.1303.7", + "@angular-devkit/core": "13.3.7", + "@angular-devkit/schematics": "13.3.7", + "@schematics/angular": "13.3.7", + "@yarnpkg/lockfile": "1.1.0", + "ansi-colors": "4.1.1", + "debug": "4.3.3", + "ini": "2.0.0", + "inquirer": "8.2.0", + "jsonc-parser": "3.0.0", + "npm-package-arg": "8.1.5", + "npm-pick-manifest": "6.1.1", + "open": "8.4.0", + "ora": "5.4.1", + "pacote": "12.0.3", + "resolve": "1.22.0", + "semver": "7.3.5", + "symbol-observable": "4.0.0", + "uuid": "8.3.2" + }, + "bin": { + "ng": "bin/ng.js" + }, + "engines": { + "node": "^12.20.0 || ^14.15.0 || >=16.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular/common": { + "version": "13.3.10", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^12.20.0 || ^14.15.0 || >=16.10.0" + }, + "peerDependencies": { + "@angular/core": "13.3.10", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/compiler": { + "version": "13.3.10", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^12.20.0 || ^14.15.0 || >=16.10.0" + } + }, + "node_modules/@angular/compiler-cli": { + "version": "13.3.10", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.17.2", + "chokidar": "^3.0.0", + "convert-source-map": "^1.5.1", + "dependency-graph": "^0.11.0", + "magic-string": "^0.26.0", + "reflect-metadata": "^0.1.2", + "semver": "^7.0.0", + "sourcemap-codec": "^1.4.8", + "tslib": "^2.3.0", + "yargs": "^17.2.1" + }, + "bin": { + "ng-xi18n": "bundles/src/bin/ng_xi18n.js", + "ngc": "bundles/src/bin/ngc.js", + "ngcc": "bundles/ngcc/main-ngcc.js" + }, + "engines": { + "node": "^12.20.0 || ^14.15.0 || >=16.10.0" + }, + "peerDependencies": { + "@angular/compiler": "13.3.10", + "typescript": ">=4.4.2 <4.7" + } + }, + "node_modules/@angular/compiler-cli/node_modules/@babel/core": { + "version": "7.18.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.1.0", + "@babel/code-frame": "^7.16.7", + "@babel/generator": "^7.18.2", + "@babel/helper-compilation-targets": "^7.18.2", + "@babel/helper-module-transforms": "^7.18.0", + "@babel/helpers": "^7.18.2", + "@babel/parser": "^7.18.0", + "@babel/template": "^7.16.7", + "@babel/traverse": "^7.18.2", + "@babel/types": "^7.18.2", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.1", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@angular/compiler-cli/node_modules/@babel/core/node_modules/semver": { + "version": "6.3.0", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@angular/compiler-cli/node_modules/@babel/generator": { + "version": "7.18.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.18.2", + "@jridgewell/gen-mapping": "^0.3.0", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@angular/compiler-cli/node_modules/@jridgewell/gen-mapping": { + "version": "0.3.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.0.0", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@angular/compiler-cli/node_modules/magic-string": { + "version": "0.26.2", + "dev": true, + "license": "MIT", + "dependencies": { + "sourcemap-codec": "^1.4.8" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular/core": { + "version": "13.3.10", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^12.20.0 || ^14.15.0 || >=16.10.0" + }, + "peerDependencies": { + "rxjs": "^6.5.3 || ^7.4.0", + "zone.js": "~0.11.4" + } + }, + "node_modules/@angular/forms": { + "version": "13.3.10", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^12.20.0 || ^14.15.0 || >=16.10.0" + }, + "peerDependencies": { + "@angular/common": "13.3.10", + "@angular/core": "13.3.10", + "@angular/platform-browser": "13.3.10", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/platform-browser": { + "version": "13.3.10", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^12.20.0 || ^14.15.0 || >=16.10.0" + }, + "peerDependencies": { + "@angular/animations": "13.3.10", + "@angular/common": "13.3.10", + "@angular/core": "13.3.10" + }, + "peerDependenciesMeta": { + "@angular/animations": { + "optional": true + } + } + }, + "node_modules/@angular/platform-browser-dynamic": { + "version": "13.3.10", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^12.20.0 || ^14.15.0 || >=16.10.0" + }, + "peerDependencies": { + "@angular/common": "13.3.10", + "@angular/compiler": "13.3.10", + "@angular/core": "13.3.10", + "@angular/platform-browser": "13.3.10" + } + }, + "node_modules/@angular/router": { + "version": "13.3.10", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^12.20.0 || ^14.15.0 || >=16.10.0" + }, + "peerDependencies": { + "@angular/common": "13.3.10", + "@angular/core": "13.3.10", + "@angular/platform-browser": "13.3.10", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@assemblyscript/loader": { + "version": "0.10.1", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@babel/code-frame": { + "version": "7.16.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/highlight": "^7.16.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.17.10", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.16.12", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.16.7", + "@babel/generator": "^7.16.8", + "@babel/helper-compilation-targets": "^7.16.7", + "@babel/helper-module-transforms": "^7.16.7", + "@babel/helpers": "^7.16.7", + "@babel/parser": "^7.16.12", + "@babel/template": "^7.16.7", + "@babel/traverse": "^7.16.10", + "@babel/types": "^7.16.8", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.1.2", + "semver": "^6.3.0", + "source-map": "^0.5.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.0", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/core/node_modules/source-map": { + "version": "0.5.7", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@babel/generator": { + "version": "7.16.8", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.16.8", + "jsesc": "^2.5.1", + "source-map": "^0.5.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/generator/node_modules/source-map": { + "version": "0.5.7", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.16.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.16.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-builder-binary-assignment-operator-visitor": { + "version": "7.16.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-explode-assignable-expression": "^7.16.7", + "@babel/types": "^7.16.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.18.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.17.10", + "@babel/helper-validator-option": "^7.16.7", + "browserslist": "^4.20.2", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.0", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.18.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.16.7", + "@babel/helper-environment-visitor": "^7.16.7", + "@babel/helper-function-name": "^7.17.9", + "@babel/helper-member-expression-to-functions": "^7.17.7", + "@babel/helper-optimise-call-expression": "^7.16.7", + "@babel/helper-replace-supers": "^7.16.7", + "@babel/helper-split-export-declaration": "^7.16.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin": { + "version": "7.17.12", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.16.7", + "regexpu-core": "^5.0.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.3.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.13.0", + "@babel/helper-module-imports": "^7.12.13", + "@babel/helper-plugin-utils": "^7.13.0", + "@babel/traverse": "^7.13.0", + "debug": "^4.1.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.14.2", + "semver": "^6.1.2" + }, + "peerDependencies": { + "@babel/core": "^7.4.0-0" + } + }, + "node_modules/@babel/helper-define-polyfill-provider/node_modules/semver": { + "version": "6.3.0", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-environment-visitor": { + "version": "7.18.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-explode-assignable-expression": { + "version": "7.16.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.16.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.17.9", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.16.7", + "@babel/types": "^7.17.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-hoist-variables": { + "version": "7.16.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.16.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.17.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.17.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.16.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.16.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.18.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-environment-visitor": "^7.16.7", + "@babel/helper-module-imports": "^7.16.7", + "@babel/helper-simple-access": "^7.17.7", + "@babel/helper-split-export-declaration": "^7.16.7", + "@babel/helper-validator-identifier": "^7.16.7", + "@babel/template": "^7.16.7", + "@babel/traverse": "^7.18.0", + "@babel/types": "^7.18.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.16.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.16.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.17.12", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-remap-async-to-generator": { + "version": "7.16.8", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.16.7", + "@babel/helper-wrap-function": "^7.16.8", + "@babel/types": "^7.16.8" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.18.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-environment-visitor": "^7.18.2", + "@babel/helper-member-expression-to-functions": "^7.17.7", + "@babel/helper-optimise-call-expression": "^7.16.7", + "@babel/traverse": "^7.18.2", + "@babel/types": "^7.18.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.18.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.18.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.16.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.16.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.16.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.16.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.16.7", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.16.7", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-wrap-function": { + "version": "7.16.8", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-function-name": "^7.16.7", + "@babel/template": "^7.16.7", + "@babel/traverse": "^7.16.8", + "@babel/types": "^7.16.8" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.18.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.16.7", + "@babel/traverse": "^7.18.2", + "@babel/types": "^7.18.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.17.12", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.16.7", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.18.3", + "dev": true, + "license": "MIT", + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.17.12", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.17.12" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.17.12", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.17.12", + "@babel/helper-skip-transparent-expression-wrappers": "^7.16.0", + "@babel/plugin-proposal-optional-chaining": "^7.17.12" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.13.0" + } + }, + "node_modules/@babel/plugin-proposal-async-generator-functions": { + "version": "7.16.8", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.16.7", + "@babel/helper-remap-async-to-generator": "^7.16.8", + "@babel/plugin-syntax-async-generators": "^7.8.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-class-properties": { + "version": "7.17.12", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.17.12", + "@babel/helper-plugin-utils": "^7.17.12" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-class-static-block": { + "version": "7.18.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.18.0", + "@babel/helper-plugin-utils": "^7.17.12", + "@babel/plugin-syntax-class-static-block": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0" + } + }, + "node_modules/@babel/plugin-proposal-dynamic-import": { + "version": "7.16.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.16.7", + "@babel/plugin-syntax-dynamic-import": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-export-namespace-from": { + "version": "7.17.12", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.17.12", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-json-strings": { + "version": "7.17.12", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.17.12", + "@babel/plugin-syntax-json-strings": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-logical-assignment-operators": { + "version": "7.17.12", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.17.12", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-nullish-coalescing-operator": { + "version": "7.17.12", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.17.12", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-numeric-separator": { + "version": "7.16.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.16.7", + "@babel/plugin-syntax-numeric-separator": "^7.10.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-object-rest-spread": { + "version": "7.18.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.17.10", + "@babel/helper-compilation-targets": "^7.17.10", + "@babel/helper-plugin-utils": "^7.17.12", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-transform-parameters": "^7.17.12" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-optional-catch-binding": { + "version": "7.16.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.16.7", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-optional-chaining": { + "version": "7.17.12", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.17.12", + "@babel/helper-skip-transparent-expression-wrappers": "^7.16.0", + "@babel/plugin-syntax-optional-chaining": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-private-methods": { + "version": "7.17.12", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.17.12", + "@babel/helper-plugin-utils": "^7.17.12" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.17.12", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.16.7", + "@babel/helper-create-class-features-plugin": "^7.17.12", + "@babel/helper-plugin-utils": "^7.17.12", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-unicode-property-regex": { + "version": "7.17.12", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.17.12", + "@babel/helper-plugin-utils": "^7.17.12" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-dynamic-import": { + "version": "7.8.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-export-namespace-from": { + "version": "7.8.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.17.12", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.17.12" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-to-generator": { + "version": "7.16.8", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.16.7", + "@babel/helper-plugin-utils": "^7.16.7", + "@babel/helper-remap-async-to-generator": "^7.16.8" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoped-functions": { + "version": "7.16.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.16.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.17.12", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.17.12" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-classes": { + "version": "7.17.12", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.16.7", + "@babel/helper-environment-visitor": "^7.16.7", + "@babel/helper-function-name": "^7.17.9", + "@babel/helper-optimise-call-expression": "^7.16.7", + "@babel/helper-plugin-utils": "^7.17.12", + "@babel/helper-replace-supers": "^7.16.7", + "@babel/helper-split-export-declaration": "^7.16.7", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.17.12", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.17.12" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.18.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.17.12" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dotall-regex": { + "version": "7.16.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.16.7", + "@babel/helper-plugin-utils": "^7.16.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-keys": { + "version": "7.17.12", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.17.12" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-exponentiation-operator": { + "version": "7.16.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.16.7", + "@babel/helper-plugin-utils": "^7.16.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-for-of": { + "version": "7.18.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.17.12" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-function-name": { + "version": "7.16.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.16.7", + "@babel/helper-function-name": "^7.16.7", + "@babel/helper-plugin-utils": "^7.16.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-literals": { + "version": "7.17.12", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.17.12" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-member-expression-literals": { + "version": "7.16.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.16.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-amd": { + "version": "7.18.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.18.0", + "@babel/helper-plugin-utils": "^7.17.12", + "babel-plugin-dynamic-import-node": "^2.3.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.18.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.18.0", + "@babel/helper-plugin-utils": "^7.17.12", + "@babel/helper-simple-access": "^7.18.2", + "babel-plugin-dynamic-import-node": "^2.3.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-systemjs": { + "version": "7.18.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-hoist-variables": "^7.16.7", + "@babel/helper-module-transforms": "^7.18.0", + "@babel/helper-plugin-utils": "^7.17.12", + "@babel/helper-validator-identifier": "^7.16.7", + "babel-plugin-dynamic-import-node": "^2.3.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-umd": { + "version": "7.18.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.18.0", + "@babel/helper-plugin-utils": "^7.17.12" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.17.12", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.17.12", + "@babel/helper-plugin-utils": "^7.17.12" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-new-target": { + "version": "7.17.12", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.17.12" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-super": { + "version": "7.16.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.16.7", + "@babel/helper-replace-supers": "^7.16.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-parameters": { + "version": "7.17.12", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.17.12" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-property-literals": { + "version": "7.16.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.16.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.18.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.17.12", + "regenerator-transform": "^0.15.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-reserved-words": { + "version": "7.17.12", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.17.12" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-runtime": { + "version": "7.16.10", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.16.7", + "@babel/helper-plugin-utils": "^7.16.7", + "babel-plugin-polyfill-corejs2": "^0.3.0", + "babel-plugin-polyfill-corejs3": "^0.5.0", + "babel-plugin-polyfill-regenerator": "^0.3.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-runtime/node_modules/semver": { + "version": "6.3.0", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/plugin-transform-shorthand-properties": { + "version": "7.16.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.16.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-spread": { + "version": "7.17.12", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.17.12", + "@babel/helper-skip-transparent-expression-wrappers": "^7.16.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-sticky-regex": { + "version": "7.16.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.16.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-template-literals": { + "version": "7.18.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.17.12" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typeof-symbol": { + "version": "7.17.12", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.17.12" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-escapes": { + "version": "7.16.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.16.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-regex": { + "version": "7.16.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.16.7", + "@babel/helper-plugin-utils": "^7.16.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-env": { + "version": "7.16.11", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.16.8", + "@babel/helper-compilation-targets": "^7.16.7", + "@babel/helper-plugin-utils": "^7.16.7", + "@babel/helper-validator-option": "^7.16.7", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.16.7", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.16.7", + "@babel/plugin-proposal-async-generator-functions": "^7.16.8", + "@babel/plugin-proposal-class-properties": "^7.16.7", + "@babel/plugin-proposal-class-static-block": "^7.16.7", + "@babel/plugin-proposal-dynamic-import": "^7.16.7", + "@babel/plugin-proposal-export-namespace-from": "^7.16.7", + "@babel/plugin-proposal-json-strings": "^7.16.7", + "@babel/plugin-proposal-logical-assignment-operators": "^7.16.7", + "@babel/plugin-proposal-nullish-coalescing-operator": "^7.16.7", + "@babel/plugin-proposal-numeric-separator": "^7.16.7", + "@babel/plugin-proposal-object-rest-spread": "^7.16.7", + "@babel/plugin-proposal-optional-catch-binding": "^7.16.7", + "@babel/plugin-proposal-optional-chaining": "^7.16.7", + "@babel/plugin-proposal-private-methods": "^7.16.11", + "@babel/plugin-proposal-private-property-in-object": "^7.16.7", + "@babel/plugin-proposal-unicode-property-regex": "^7.16.7", + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5", + "@babel/plugin-transform-arrow-functions": "^7.16.7", + "@babel/plugin-transform-async-to-generator": "^7.16.8", + "@babel/plugin-transform-block-scoped-functions": "^7.16.7", + "@babel/plugin-transform-block-scoping": "^7.16.7", + "@babel/plugin-transform-classes": "^7.16.7", + "@babel/plugin-transform-computed-properties": "^7.16.7", + "@babel/plugin-transform-destructuring": "^7.16.7", + "@babel/plugin-transform-dotall-regex": "^7.16.7", + "@babel/plugin-transform-duplicate-keys": "^7.16.7", + "@babel/plugin-transform-exponentiation-operator": "^7.16.7", + "@babel/plugin-transform-for-of": "^7.16.7", + "@babel/plugin-transform-function-name": "^7.16.7", + "@babel/plugin-transform-literals": "^7.16.7", + "@babel/plugin-transform-member-expression-literals": "^7.16.7", + "@babel/plugin-transform-modules-amd": "^7.16.7", + "@babel/plugin-transform-modules-commonjs": "^7.16.8", + "@babel/plugin-transform-modules-systemjs": "^7.16.7", + "@babel/plugin-transform-modules-umd": "^7.16.7", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.16.8", + "@babel/plugin-transform-new-target": "^7.16.7", + "@babel/plugin-transform-object-super": "^7.16.7", + "@babel/plugin-transform-parameters": "^7.16.7", + "@babel/plugin-transform-property-literals": "^7.16.7", + "@babel/plugin-transform-regenerator": "^7.16.7", + "@babel/plugin-transform-reserved-words": "^7.16.7", + "@babel/plugin-transform-shorthand-properties": "^7.16.7", + "@babel/plugin-transform-spread": "^7.16.7", + "@babel/plugin-transform-sticky-regex": "^7.16.7", + "@babel/plugin-transform-template-literals": "^7.16.7", + "@babel/plugin-transform-typeof-symbol": "^7.16.7", + "@babel/plugin-transform-unicode-escapes": "^7.16.7", + "@babel/plugin-transform-unicode-regex": "^7.16.7", + "@babel/preset-modules": "^0.1.5", + "@babel/types": "^7.16.8", + "babel-plugin-polyfill-corejs2": "^0.3.0", + "babel-plugin-polyfill-corejs3": "^0.5.0", + "babel-plugin-polyfill-regenerator": "^0.3.0", + "core-js-compat": "^3.20.2", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-env/node_modules/semver": { + "version": "6.3.0", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/preset-modules": { + "version": "0.1.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-proposal-unicode-property-regex": "^7.4.4", + "@babel/plugin-transform-dotall-regex": "^7.4.4", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.16.7", + "dev": true, + "license": "MIT", + "dependencies": { + "regenerator-runtime": "^0.13.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/runtime-corejs3": { + "version": "7.18.3", + "dev": true, + "license": "MIT", + "dependencies": { + "core-js-pure": "^3.20.2", + "regenerator-runtime": "^0.13.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.16.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.16.7", + "@babel/parser": "^7.16.7", + "@babel/types": "^7.16.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.18.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.16.7", + "@babel/generator": "^7.18.2", + "@babel/helper-environment-visitor": "^7.18.2", + "@babel/helper-function-name": "^7.17.9", + "@babel/helper-hoist-variables": "^7.16.7", + "@babel/helper-split-export-declaration": "^7.16.7", + "@babel/parser": "^7.18.0", + "@babel/types": "^7.18.2", + "debug": "^4.1.0", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/@babel/generator": { + "version": "7.18.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.18.2", + "@jridgewell/gen-mapping": "^0.3.0", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/@jridgewell/gen-mapping": { + "version": "0.3.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.0.0", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.18.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.16.7", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@cds/city": { + "version": "1.1.0", + "license": "OFL-1.1", + "optional": true + }, + "node_modules/@cds/core": { + "version": "5.7.2", + "license": "MIT", + "dependencies": { + "lit": "^2.1.3", + "ramda": "^0.28.0", + "tslib": "^2.3.1" + }, + "optionalDependencies": { + "@cds/city": "^1.1.0", + "@types/resize-observer-browser": "^0.1.5", + "modern-normalize": "1.1.0" + } + }, + "node_modules/@clr/angular": { + "version": "13.3.1", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/common": "^13.0.0", + "@angular/core": "^13.0.0", + "@cds/core": "next || ^6.0.0 || ^5.6.0" + } + }, + "node_modules/@clr/icons": { + "version": "13.0.2", + "license": "MIT", + "peerDependencies": { + "@webcomponents/custom-elements": "^1.0.0" + } + }, + "node_modules/@clr/ui": { + "version": "13.3.1", + "license": "MIT", + "peerDependencies": { + "@clr/angular": "13.3.1" + } + }, + "node_modules/@colors/colors": { + "version": "1.5.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@csstools/postcss-progressive-custom-properties": { + "version": "1.3.0", + "dev": true, + "license": "CC0-1.0", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.3" + } + }, + "node_modules/@csstools/selector-specificity": { + "version": "1.0.0", + "dev": true, + "license": "CC0-1.0", + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.3", + "postcss-selector-parser": "^6.0.10" + } + }, + "node_modules/@cypress/request": { + "version": "2.88.10", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "http-signature": "~1.3.6", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.5.0", + "tunnel-agent": "^0.6.0", + "uuid": "^8.3.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@cypress/xvfb": { + "version": "1.2.4", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.1.0", + "lodash.once": "^4.1.1" + } + }, + "node_modules/@cypress/xvfb/node_modules/debug": { + "version": "3.2.7", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/@discoveryjs/json-ext": { + "version": "0.5.6", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@es-joy/jsdoccomment": { + "version": "0.31.0", + "dev": true, + "license": "MIT", + "dependencies": { + "comment-parser": "1.3.1", + "esquery": "^1.4.0", + "jsdoc-type-pratt-parser": "~3.1.0" + }, + "engines": { + "node": "^14 || ^16 || ^17 || ^18" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "1.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.3.2", + "globals": "^13.15.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ajv": { + "version": "6.12.6", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@eslint/eslintrc/node_modules/argparse": { + "version": "2.0.1", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "13.15.0", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/eslintrc/node_modules/js-yaml": { + "version": "4.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { + "version": "0.4.1", + "dev": true, + "license": "MIT" + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/eslintrc/node_modules/type-fest": { + "version": "0.20.2", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@gar/promisify": { + "version": "1.1.3", + "dev": true, + "license": "MIT" + }, + "node_modules/@hapi/hoek": { + "version": "9.3.0", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/topo": { + "version": "5.1.0", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.9.5", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^1.2.1", + "debug": "^4.1.1", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "1.2.1", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.0.0", + "@jridgewell/sourcemap-codec": "^1.4.10" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.0.7", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.13", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.13", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@lit/reactive-element": { + "version": "1.3.2", + "license": "BSD-3-Clause" + }, + "node_modules/@neuralegion/cypress-har-generator": { + "version": "5.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "chrome-remote-interface": "~0.30.0", + "tslib": "^2.2.0" + } + }, + "node_modules/@neuralegion/cypress-har-generator/node_modules/ansi-styles": { + "version": "4.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@neuralegion/cypress-har-generator/node_modules/chalk": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@neuralegion/cypress-har-generator/node_modules/color-convert": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@neuralegion/cypress-har-generator/node_modules/color-name": { + "version": "1.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/@neuralegion/cypress-har-generator/node_modules/has-flag": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@neuralegion/cypress-har-generator/node_modules/supports-color": { + "version": "7.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@ngrx/effects": { + "version": "13.2.0", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@angular/core": "^13.0.0", + "@ngrx/store": "13.2.0", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@ngrx/router-store": { + "version": "13.2.0", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@angular/common": "^13.0.0", + "@angular/core": "^13.0.0", + "@angular/router": "^13.0.0", + "@ngrx/store": "13.2.0", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@ngrx/schematics": { + "version": "13.2.0", + "dev": true, + "license": "MIT" + }, + "node_modules/@ngrx/store": { + "version": "13.2.0", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@angular/core": "^13.0.0", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@ngrx/store-devtools": { + "version": "13.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@ngrx/store": "13.2.0", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@ngtools/webpack": { + "version": "13.3.7", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.15.0 || >=16.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "@angular/compiler-cli": "^13.0.0", + "typescript": ">=4.4.3 <4.7", + "webpack": "^5.30.0" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@npmcli/fs": { + "version": "1.1.1", + "dev": true, + "license": "ISC", + "dependencies": { + "@gar/promisify": "^1.0.1", + "semver": "^7.3.5" + } + }, + "node_modules/@npmcli/git": { + "version": "2.1.0", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/promise-spawn": "^1.3.2", + "lru-cache": "^6.0.0", + "mkdirp": "^1.0.4", + "npm-pick-manifest": "^6.1.1", + "promise-inflight": "^1.0.1", + "promise-retry": "^2.0.1", + "semver": "^7.3.5", + "which": "^2.0.2" + } + }, + "node_modules/@npmcli/installed-package-contents": { + "version": "1.0.7", + "dev": true, + "license": "ISC", + "dependencies": { + "npm-bundled": "^1.1.1", + "npm-normalize-package-bin": "^1.0.1" + }, + "bin": { + "installed-package-contents": "index.js" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@npmcli/move-file": { + "version": "1.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@npmcli/node-gyp": { + "version": "1.0.3", + "dev": true, + "license": "ISC" + }, + "node_modules/@npmcli/promise-spawn": { + "version": "1.3.2", + "dev": true, + "license": "ISC", + "dependencies": { + "infer-owner": "^1.0.4" + } + }, + "node_modules/@npmcli/run-script": { + "version": "2.0.0", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/node-gyp": "^1.0.2", + "@npmcli/promise-spawn": "^1.3.2", + "node-gyp": "^8.2.0", + "read-package-json-fast": "^2.0.1" + } + }, + "node_modules/@nrwl/cli": { + "version": "14.1.9", + "dev": true, + "license": "MIT", + "dependencies": { + "nx": "14.1.9" + } + }, + "node_modules/@nrwl/cli/node_modules/@nrwl/tao": { + "version": "14.1.9", + "dev": true, + "license": "MIT", + "dependencies": { + "nx": "14.1.9" + }, + "bin": { + "tao": "index.js" + } + }, + "node_modules/@nrwl/cli/node_modules/ansi-styles": { + "version": "4.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@nrwl/cli/node_modules/chalk": { + "version": "4.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@nrwl/cli/node_modules/color-convert": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@nrwl/cli/node_modules/color-name": { + "version": "1.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/@nrwl/cli/node_modules/fast-glob": { + "version": "3.2.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@nrwl/cli/node_modules/fs-extra": { + "version": "10.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@nrwl/cli/node_modules/glob": { + "version": "7.1.4", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@nrwl/cli/node_modules/has-flag": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@nrwl/cli/node_modules/minimatch": { + "version": "3.0.4", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@nrwl/cli/node_modules/nx": { + "version": "14.1.9", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@nrwl/cli": "14.1.9", + "@nrwl/tao": "14.1.9", + "@parcel/watcher": "2.0.4", + "@swc-node/register": "^1.4.2", + "@swc/core": "^1.2.173", + "chalk": "4.1.0", + "chokidar": "^3.5.1", + "cli-cursor": "3.1.0", + "cli-spinners": "2.6.1", + "cliui": "^7.0.2", + "dotenv": "~10.0.0", + "enquirer": "~2.3.6", + "fast-glob": "3.2.7", + "figures": "3.2.0", + "flat": "^5.0.2", + "fs-extra": "^10.1.0", + "glob": "7.1.4", + "ignore": "^5.0.4", + "jsonc-parser": "3.0.0", + "minimatch": "3.0.4", + "npm-run-path": "^4.0.1", + "open": "^8.4.0", + "semver": "7.3.4", + "string-width": "^4.2.3", + "tar-stream": "~2.2.0", + "tmp": "~0.2.1", + "tsconfig-paths": "^3.9.0", + "tslib": "^2.3.0", + "v8-compile-cache": "2.3.0", + "yargs": "^17.4.0", + "yargs-parser": "21.0.1" + }, + "bin": { + "nx": "bin/nx.js" + } + }, + "node_modules/@nrwl/cli/node_modules/semver": { + "version": "7.3.4", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@nrwl/cli/node_modules/supports-color": { + "version": "7.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@nrwl/cli/node_modules/yargs-parser": { + "version": "21.0.1", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/@nrwl/devkit": { + "version": "13.1.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@nrwl/tao": "13.1.3", + "ejs": "^3.1.5", + "ignore": "^5.0.4", + "rxjs": "^6.5.4", + "semver": "7.3.4", + "tslib": "^2.0.0" + } + }, + "node_modules/@nrwl/devkit/node_modules/rxjs": { + "version": "6.6.7", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" + } + }, + "node_modules/@nrwl/devkit/node_modules/rxjs/node_modules/tslib": { + "version": "1.14.1", + "dev": true, + "license": "0BSD" + }, + "node_modules/@nrwl/devkit/node_modules/semver": { + "version": "7.3.4", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@nrwl/tao": { + "version": "13.1.3", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "4.1.0", + "enquirer": "~2.3.6", + "fs-extra": "^9.1.0", + "jsonc-parser": "3.0.0", + "nx": "13.1.3", + "rxjs": "^6.5.4", + "rxjs-for-await": "0.0.2", + "semver": "7.3.4", + "tmp": "~0.2.1", + "tslib": "^2.0.0", + "yargs-parser": "20.0.0" + }, + "bin": { + "tao": "index.js" + } + }, + "node_modules/@nrwl/tao/node_modules/ansi-styles": { + "version": "4.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@nrwl/tao/node_modules/chalk": { + "version": "4.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@nrwl/tao/node_modules/color-convert": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@nrwl/tao/node_modules/color-name": { + "version": "1.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/@nrwl/tao/node_modules/has-flag": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@nrwl/tao/node_modules/rxjs": { + "version": "6.6.7", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" + } + }, + "node_modules/@nrwl/tao/node_modules/rxjs/node_modules/tslib": { + "version": "1.14.1", + "dev": true, + "license": "0BSD" + }, + "node_modules/@nrwl/tao/node_modules/semver": { + "version": "7.3.4", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@nrwl/tao/node_modules/supports-color": { + "version": "7.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@parcel/watcher": { + "version": "2.0.4", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^3.2.1", + "node-gyp-build": "^4.3.0" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@rollup/plugin-json": { + "version": "4.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^3.0.8" + }, + "peerDependencies": { + "rollup": "^1.20.0 || ^2.0.0" + } + }, + "node_modules/@rollup/plugin-node-resolve": { + "version": "13.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^3.1.0", + "@types/resolve": "1.17.1", + "deepmerge": "^4.2.2", + "is-builtin-module": "^3.1.0", + "is-module": "^1.0.0", + "resolve": "^1.19.0" + }, + "engines": { + "node": ">= 10.0.0" + }, + "peerDependencies": { + "rollup": "^2.42.0" + } + }, + "node_modules/@rollup/pluginutils": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "0.0.39", + "estree-walker": "^1.0.1", + "picomatch": "^2.2.2" + }, + "engines": { + "node": ">= 8.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0" + } + }, + "node_modules/@schematics/angular": { + "version": "13.3.7", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "13.3.7", + "@angular-devkit/schematics": "13.3.7", + "jsonc-parser": "3.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.15.0 || >=16.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@sideway/address": { + "version": "4.1.4", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, + "node_modules/@sideway/formula": { + "version": "3.0.0", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@sideway/pinpoint": { + "version": "2.0.0", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@sindresorhus/is": { + "version": "0.14.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/@swc-node/core": { + "version": "1.9.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@swc/core": "^1.2.172" + }, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@swc-node/register": { + "version": "1.5.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@swc-node/core": "^1.9.0", + "@swc-node/sourcemap-support": "^0.2.0", + "colorette": "^2.0.16", + "debug": "^4.3.4", + "pirates": "^4.0.5", + "tslib": "^2.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "typescript": ">= 4.3" + } + }, + "node_modules/@swc-node/register/node_modules/debug": { + "version": "4.3.4", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@swc-node/register/node_modules/tslib": { + "version": "2.4.0", + "dev": true, + "license": "0BSD" + }, + "node_modules/@swc-node/sourcemap-support": { + "version": "0.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "source-map-support": "^0.5.21" + } + }, + "node_modules/@swc/core": { + "version": "1.2.194", + "dev": true, + "license": "Apache-2.0", + "bin": { + "swcx": "run_swcx.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/swc" + }, + "optionalDependencies": { + "@swc/core-android-arm-eabi": "1.2.194", + "@swc/core-android-arm64": "1.2.194", + "@swc/core-darwin-arm64": "1.2.194", + "@swc/core-darwin-x64": "1.2.194", + "@swc/core-freebsd-x64": "1.2.194", + "@swc/core-linux-arm-gnueabihf": "1.2.194", + "@swc/core-linux-arm64-gnu": "1.2.194", + "@swc/core-linux-arm64-musl": "1.2.194", + "@swc/core-linux-x64-gnu": "1.2.194", + "@swc/core-linux-x64-musl": "1.2.194", + "@swc/core-win32-arm64-msvc": "1.2.194", + "@swc/core-win32-ia32-msvc": "1.2.194", + "@swc/core-win32-x64-msvc": "1.2.194" + } + }, + "node_modules/@swc/core-darwin-arm64": { + "version": "1.2.194", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swimlane/ngx-graph": { + "version": "8.0.0", + "license": "MIT", + "dependencies": { + "d3-dispatch": "^1.0.3", + "d3-ease": "^1.0.5", + "d3-force": "^1.1.0", + "d3-selection": "^1.2.0", + "d3-shape": "^1.2.0", + "d3-timer": "^1.0.7", + "d3-transition": "^1.1.1", + "dagre": "^0.8.4", + "transformation-matrix": "^1.15.3", + "tslib": "^2.0.0", + "webcola": "^3.3.8" + }, + "peerDependencies": { + "@angular/animations": "10.x || 11.x || 12.x || 13.x", + "@angular/cdk": "10.x || 11.x || 12.x || 13.x", + "@angular/common": "10.x || 11.x || 12.x || 13.x", + "@angular/core": "10.x || 11.x || 12.x || 13.x", + "rxjs": "6.x" + } + }, + "node_modules/@szmarczak/http-timer": { + "version": "1.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "defer-to-connect": "^1.0.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@tootallnate/once": { + "version": "1.1.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/bonjour": { + "version": "3.5.10", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/component-emitter": { + "version": "1.2.11", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/connect": { + "version": "3.4.35", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/connect-history-api-fallback": { + "version": "1.3.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express-serve-static-core": "*", + "@types/node": "*" + } + }, + "node_modules/@types/cookie": { + "version": "0.4.1", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/cors": { + "version": "2.8.12", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/eslint": { + "version": "8.4.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "0.0.39", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/express": { + "version": "4.17.13", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.18", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.17.28", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*" + } + }, + "node_modules/@types/http-proxy": { + "version": "1.17.9", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/jasmine": { + "version": "4.0.3", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.11", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json5": { + "version": "0.0.29", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/lodash": { + "version": "4.14.182", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mime": { + "version": "1.3.2", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "17.0.35", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/parse-json": { + "version": "4.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/qs": { + "version": "6.9.7", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.4", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/resize-observer-browser": { + "version": "0.1.7", + "license": "MIT", + "optional": true + }, + "node_modules/@types/resolve": { + "version": "1.17.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/retry": { + "version": "0.12.0", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/serve-index": { + "version": "1.9.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.13.10", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/sinonjs__fake-timers": { + "version": "8.1.1", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/sizzle": { + "version": "2.3.3", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/sockjs": { + "version": "0.3.33", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/trusted-types": { + "version": "2.0.2", + "license": "MIT" + }, + "node_modules/@types/ws": { + "version": "8.5.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/yargs": { + "version": "17.0.10", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/yauzl": { + "version": "2.10.0", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "5.26.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "5.26.0", + "@typescript-eslint/type-utils": "5.26.0", + "@typescript-eslint/utils": "5.26.0", + "debug": "^4.3.4", + "functional-red-black-tree": "^1.0.1", + "ignore": "^5.2.0", + "regexpp": "^3.2.0", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^5.0.0", + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/debug": { + "version": "4.3.4", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/semver": { + "version": "7.3.7", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/experimental-utils": { + "version": "5.17.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/utils": "5.17.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/experimental-utils/node_modules/@typescript-eslint/scope-manager": { + "version": "5.17.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "5.17.0", + "@typescript-eslint/visitor-keys": "5.17.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/experimental-utils/node_modules/@typescript-eslint/types": { + "version": "5.17.0", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/experimental-utils/node_modules/@typescript-eslint/typescript-estree": { + "version": "5.17.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "5.17.0", + "@typescript-eslint/visitor-keys": "5.17.0", + "debug": "^4.3.2", + "globby": "^11.0.4", + "is-glob": "^4.0.3", + "semver": "^7.3.5", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/experimental-utils/node_modules/@typescript-eslint/utils": { + "version": "5.17.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "@typescript-eslint/scope-manager": "5.17.0", + "@typescript-eslint/types": "5.17.0", + "@typescript-eslint/typescript-estree": "5.17.0", + "eslint-scope": "^5.1.1", + "eslint-utils": "^3.0.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/experimental-utils/node_modules/@typescript-eslint/visitor-keys": { + "version": "5.17.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "5.17.0", + "eslint-visitor-keys": "^3.0.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "5.26.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/scope-manager": "5.26.0", + "@typescript-eslint/types": "5.26.0", + "@typescript-eslint/typescript-estree": "5.26.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser/node_modules/debug": { + "version": "4.3.4", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "5.26.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "5.26.0", + "@typescript-eslint/visitor-keys": "5.26.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "5.26.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/utils": "5.26.0", + "debug": "^4.3.4", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/debug": { + "version": "4.3.4", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "5.26.0", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "5.26.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "5.26.0", + "@typescript-eslint/visitor-keys": "5.26.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/debug": { + "version": "4.3.4", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.3.7", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "5.26.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "@typescript-eslint/scope-manager": "5.26.0", + "@typescript-eslint/types": "5.26.0", + "@typescript-eslint/typescript-estree": "5.26.0", + "eslint-scope": "^5.1.1", + "eslint-utils": "^3.0.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "5.26.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "5.26.0", + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.11.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/helper-numbers": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.11.1", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.11.1", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.11.1", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.11.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.11.1", + "@webassemblyjs/helper-api-error": "1.11.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.11.1", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.11.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-buffer": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1", + "@webassemblyjs/wasm-gen": "1.11.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.11.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.11.1", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.11.1", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.11.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-buffer": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1", + "@webassemblyjs/helper-wasm-section": "1.11.1", + "@webassemblyjs/wasm-gen": "1.11.1", + "@webassemblyjs/wasm-opt": "1.11.1", + "@webassemblyjs/wasm-parser": "1.11.1", + "@webassemblyjs/wast-printer": "1.11.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.11.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1", + "@webassemblyjs/ieee754": "1.11.1", + "@webassemblyjs/leb128": "1.11.1", + "@webassemblyjs/utf8": "1.11.1" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.11.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-buffer": "1.11.1", + "@webassemblyjs/wasm-gen": "1.11.1", + "@webassemblyjs/wasm-parser": "1.11.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.11.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-api-error": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1", + "@webassemblyjs/ieee754": "1.11.1", + "@webassemblyjs/leb128": "1.11.1", + "@webassemblyjs/utf8": "1.11.1" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.11.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.11.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webcomponents/custom-elements": { + "version": "1.5.0", + "license": "BSD-3-Clause" + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@yarnpkg/lockfile": { + "version": "1.1.0", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/abab": { + "version": "2.0.6", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/abbrev": { + "version": "1.1.1", + "dev": true, + "license": "ISC" + }, + "node_modules/accepts": { + "version": "1.3.8", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.7.1", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-assertions": { + "version": "1.8.0", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^8" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/adjust-sourcemap-loader": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "loader-utils": "^2.0.0", + "regex-parser": "^2.2.11" + }, + "engines": { + "node": ">=8.9" + } + }, + "node_modules/adjust-sourcemap-loader/node_modules/loader-utils": { + "version": "2.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/agentkeepalive": { + "version": "4.2.1", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.0", + "depd": "^1.1.2", + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/agentkeepalive/node_modules/depd": { + "version": "1.1.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ajv": { + "version": "8.9.0", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-keywords": { + "version": "3.5.2", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/angular-ng-autocomplete": { + "version": "2.0.8", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/common": "*", + "@angular/core": "*" + } + }, + "node_modules/angular-oauth2-oidc": { + "version": "13.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-sha256": "^1.3.0", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@angular/common": ">=12.0.0", + "@angular/core": ">=12.0.0" + } + }, + "node_modules/ansi-align": { + "version": "3.0.1", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.1.0" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-html-community": { + "version": "0.0.8", + "dev": true, + "engines": [ + "node >= 0.8.0" + ], + "license": "Apache-2.0", + "bin": { + "ansi-html": "bin/ansi-html" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/anymatch": { + "version": "3.1.2", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/aproba": { + "version": "2.0.0", + "dev": true, + "license": "ISC" + }, + "node_modules/arch": { + "version": "2.2.0", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/are-we-there-yet": { + "version": "3.0.0", + "dev": true, + "license": "ISC", + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16" + } + }, + "node_modules/argparse": { + "version": "1.0.10", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/aria-query": { + "version": "4.2.2", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@babel/runtime": "^7.10.2", + "@babel/runtime-corejs3": "^7.10.2" + }, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/array-flatten": { + "version": "2.1.2", + "dev": true, + "license": "MIT" + }, + "node_modules/array-includes": { + "version": "3.1.5", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.19.5", + "get-intrinsic": "^1.1.1", + "is-string": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-union": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.2", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/asn1": { + "version": "0.2.6", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, + "node_modules/assert-plus": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/astral-regex": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/async": { + "version": "3.2.3", + "dev": true, + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "dev": true, + "license": "MIT" + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/atob": { + "version": "2.1.2", + "dev": true, + "license": "(MIT OR Apache-2.0)", + "bin": { + "atob": "bin/atob.js" + }, + "engines": { + "node": ">= 4.5.0" + } + }, + "node_modules/autoprefixer": { + "version": "10.4.7", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.20.3", + "caniuse-lite": "^1.0.30001335", + "fraction.js": "^4.2.0", + "normalize-range": "^0.1.2", + "picocolors": "^1.0.0", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/aws-sign2": { + "version": "0.7.0", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "*" + } + }, + "node_modules/aws4": { + "version": "1.11.0", + "dev": true, + "license": "MIT" + }, + "node_modules/axios": { + "version": "0.21.4", + "dev": true, + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.14.0" + } + }, + "node_modules/axobject-query": { + "version": "2.2.0", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/babel-loader": { + "version": "8.2.5", + "dev": true, + "license": "MIT", + "dependencies": { + "find-cache-dir": "^3.3.1", + "loader-utils": "^2.0.0", + "make-dir": "^3.1.0", + "schema-utils": "^2.6.5" + }, + "engines": { + "node": ">= 8.9" + }, + "peerDependencies": { + "@babel/core": "^7.0.0", + "webpack": ">=2" + } + }, + "node_modules/babel-loader/node_modules/loader-utils": { + "version": "2.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/babel-plugin-dynamic-import-node": { + "version": "2.3.3", + "dev": true, + "license": "MIT", + "dependencies": { + "object.assign": "^4.1.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-polyfill-corejs2": { + "version": "0.3.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.13.11", + "@babel/helper-define-polyfill-provider": "^0.3.1", + "semver": "^6.1.1" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/babel-plugin-polyfill-corejs2/node_modules/semver": { + "version": "6.3.0", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.5.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.3.1", + "core-js-compat": "^3.21.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/babel-plugin-polyfill-regenerator": { + "version": "0.3.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.3.1" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/base64id": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, + "node_modules/batch": { + "version": "0.6.1", + "dev": true, + "license": "MIT" + }, + "node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, + "node_modules/bent": { + "version": "7.3.12", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bytesish": "^0.4.1", + "caseless": "~0.12.0", + "is-stream": "^2.0.0" + } + }, + "node_modules/big.js": { + "version": "5.2.2", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/blob-util": { + "version": "2.0.2", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/bluebird": { + "version": "3.7.2", + "dev": true, + "license": "MIT" + }, + "node_modules/body-parser": { + "version": "1.20.0", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.10.3", + "raw-body": "2.5.1", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/body-parser/node_modules/qs": { + "version": "6.10.3", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/bonjour": { + "version": "3.5.0", + "dev": true, + "license": "MIT", + "dependencies": { + "array-flatten": "^2.1.0", + "deep-equal": "^1.0.1", + "dns-equal": "^1.0.0", + "dns-txt": "^2.0.2", + "multicast-dns": "^6.0.1", + "multicast-dns-service-types": "^1.1.0" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "dev": true, + "license": "ISC" + }, + "node_modules/boxen": { + "version": "5.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-align": "^3.0.0", + "camelcase": "^6.2.0", + "chalk": "^4.1.0", + "cli-boxes": "^2.2.1", + "string-width": "^4.2.2", + "type-fest": "^0.20.2", + "widest-line": "^3.1.0", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/boxen/node_modules/ansi-styles": { + "version": "4.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/boxen/node_modules/camelcase": { + "version": "6.3.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/boxen/node_modules/chalk": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/boxen/node_modules/color-convert": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/boxen/node_modules/color-name": { + "version": "1.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/boxen/node_modules/has-flag": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/boxen/node_modules/supports-color": { + "version": "7.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/boxen/node_modules/type-fest": { + "version": "0.20.2", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.20.3", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001332", + "electron-to-chromium": "^1.4.118", + "escalade": "^3.1.1", + "node-releases": "^2.0.3", + "picocolors": "^1.0.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "dev": true, + "license": "MIT" + }, + "node_modules/buffer-indexof": { + "version": "1.1.1", + "dev": true, + "license": "MIT" + }, + "node_modules/builtin-modules": { + "version": "3.3.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/builtins": { + "version": "1.0.3", + "dev": true, + "license": "MIT" + }, + "node_modules/bytes": { + "version": "3.1.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/bytesish": { + "version": "0.4.4", + "dev": true, + "license": "(Apache-2.0 AND MIT)" + }, + "node_modules/cacache": { + "version": "15.3.0", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/fs": "^1.0.0", + "@npmcli/move-file": "^1.0.1", + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "glob": "^7.1.4", + "infer-owner": "^1.0.4", + "lru-cache": "^6.0.0", + "minipass": "^3.1.1", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.2", + "mkdirp": "^1.0.3", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^3.0.2", + "ssri": "^8.0.1", + "tar": "^6.0.2", + "unique-filename": "^1.1.1" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/cacheable-request": { + "version": "6.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^3.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^4.1.0", + "responselike": "^1.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cacheable-request/node_modules/lowercase-keys": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cachedir": { + "version": "2.3.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/call-bind": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001343", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/caseless": { + "version": "0.12.0", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/chalk": { + "version": "2.4.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chardet": { + "version": "0.7.0", + "dev": true, + "license": "MIT" + }, + "node_modules/check-more-types": { + "version": "2.24.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/chokidar": { + "version": "3.5.3", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/chrome-remote-interface": { + "version": "0.30.1", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "2.11.x", + "ws": "^7.2.0" + }, + "bin": { + "chrome-remote-interface": "bin/client.js" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0" + } + }, + "node_modules/ci-info": { + "version": "3.3.1", + "dev": true, + "license": "MIT" + }, + "node_modules/circular-dependency-plugin": { + "version": "5.2.2", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=6.0.0" + }, + "peerDependencies": { + "webpack": ">=4.0.1" + } + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/cli-boxes": { + "version": "2.2.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-spinners": { + "version": "2.6.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-table3": { + "version": "0.6.2", + "dev": true, + "license": "MIT", + "dependencies": { + "string-width": "^4.2.0" + }, + "engines": { + "node": "10.* || >= 12.*" + }, + "optionalDependencies": { + "@colors/colors": "1.5.0" + } + }, + "node_modules/cli-truncate": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "slice-ansi": "^3.0.0", + "string-width": "^4.2.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-width": { + "version": "3.0.0", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 10" + } + }, + "node_modules/cliui": { + "version": "7.0.4", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/clone-deep": { + "version": "4.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/clone-response": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^1.0.0" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "dev": true, + "license": "MIT" + }, + "node_modules/color-support": { + "version": "1.1.3", + "dev": true, + "license": "ISC", + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/colorette": { + "version": "2.0.16", + "dev": true, + "license": "MIT" + }, + "node_modules/colors": { + "version": "1.4.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "2.11.0", + "license": "MIT" + }, + "node_modules/comment-parser": { + "version": "1.3.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/common-tags": { + "version": "1.8.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/commondir": { + "version": "1.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/component-emitter": { + "version": "1.3.0", + "dev": true, + "license": "MIT" + }, + "node_modules/compressible": { + "version": "2.0.18", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.7.4", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "~1.3.5", + "bytes": "3.0.0", + "compressible": "~2.0.16", + "debug": "2.6.9", + "on-headers": "~1.0.2", + "safe-buffer": "5.1.2", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/compression/node_modules/bytes": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/compression/node_modules/debug": { + "version": "2.6.9", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/compression/node_modules/ms": { + "version": "2.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/configstore": { + "version": "5.0.1", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dot-prop": "^5.2.0", + "graceful-fs": "^4.1.2", + "make-dir": "^3.0.0", + "unique-string": "^2.0.0", + "write-file-atomic": "^3.0.0", + "xdg-basedir": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/connect": { + "version": "3.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "finalhandler": "1.1.2", + "parseurl": "~1.3.3", + "utils-merge": "1.0.1" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/connect-history-api-fallback": { + "version": "1.6.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/connect/node_modules/debug": { + "version": "2.6.9", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/connect/node_modules/ms": { + "version": "2.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "dev": true, + "license": "ISC" + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-disposition/node_modules/safe-buffer": { + "version": "5.2.1", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/content-type": { + "version": "1.0.4", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "1.8.0", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.1" + } + }, + "node_modules/cookie": { + "version": "0.4.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "dev": true, + "license": "MIT" + }, + "node_modules/copy-anything": { + "version": "2.0.6", + "dev": true, + "license": "MIT", + "dependencies": { + "is-what": "^3.14.1" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/copy-webpack-plugin": { + "version": "10.2.1", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-glob": "^3.2.7", + "glob-parent": "^6.0.1", + "globby": "^12.0.2", + "normalize-path": "^3.0.0", + "schema-utils": "^4.0.0", + "serialize-javascript": "^6.0.0" + }, + "engines": { + "node": ">= 12.20.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + } + }, + "node_modules/copy-webpack-plugin/node_modules/ajv-keywords": { + "version": "5.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/copy-webpack-plugin/node_modules/array-union": { + "version": "3.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/copy-webpack-plugin/node_modules/glob-parent": { + "version": "6.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/copy-webpack-plugin/node_modules/globby": { + "version": "12.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^3.0.1", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.7", + "ignore": "^5.1.9", + "merge2": "^1.4.1", + "slash": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/copy-webpack-plugin/node_modules/schema-utils": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.8.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.0.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/copy-webpack-plugin/node_modules/slash": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/core-js": { + "version": "3.20.3", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-js-compat": { + "version": "3.22.7", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.20.3", + "semver": "7.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-js-compat/node_modules/semver": { + "version": "7.0.0", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/core-js-pure": { + "version": "3.22.7", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-util-is": { + "version": "1.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.5", + "dev": true, + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/cosmiconfig": { + "version": "7.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/critters": { + "version": "0.0.16", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "chalk": "^4.1.0", + "css-select": "^4.2.0", + "parse5": "^6.0.1", + "parse5-htmlparser2-tree-adapter": "^6.0.1", + "postcss": "^8.3.7", + "pretty-bytes": "^5.3.0" + } + }, + "node_modules/critters/node_modules/ansi-styles": { + "version": "4.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/critters/node_modules/chalk": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/critters/node_modules/color-convert": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/critters/node_modules/color-name": { + "version": "1.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/critters/node_modules/has-flag": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/critters/node_modules/parse5": { + "version": "6.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/critters/node_modules/supports-color": { + "version": "7.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/crypto-random-string": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/css": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.4", + "source-map": "^0.6.1", + "source-map-resolve": "^0.6.0" + } + }, + "node_modules/css-blank-pseudo": { + "version": "3.0.3", + "dev": true, + "license": "CC0-1.0", + "dependencies": { + "postcss-selector-parser": "^6.0.9" + }, + "bin": { + "css-blank-pseudo": "dist/cli.cjs" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/css-has-pseudo": { + "version": "3.0.4", + "dev": true, + "license": "CC0-1.0", + "dependencies": { + "postcss-selector-parser": "^6.0.9" + }, + "bin": { + "css-has-pseudo": "dist/cli.cjs" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/css-loader": { + "version": "6.5.1", + "dev": true, + "license": "MIT", + "dependencies": { + "icss-utils": "^5.1.0", + "postcss": "^8.2.15", + "postcss-modules-extract-imports": "^3.0.0", + "postcss-modules-local-by-default": "^4.0.0", + "postcss-modules-scope": "^3.0.0", + "postcss-modules-values": "^4.0.0", + "postcss-value-parser": "^4.1.0", + "semver": "^7.3.5" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/css-prefers-color-scheme": { + "version": "6.0.3", + "dev": true, + "license": "CC0-1.0", + "bin": { + "css-prefers-color-scheme": "dist/cli.cjs" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/css-select": { + "version": "4.3.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.0.1", + "domhandler": "^4.3.1", + "domutils": "^2.8.0", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.1.0", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css/node_modules/source-map": { + "version": "0.6.1", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/cssdb": { + "version": "5.1.0", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/cssesc": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cuint": { + "version": "0.2.2", + "dev": true, + "license": "MIT" + }, + "node_modules/custom-event": { + "version": "1.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/cypress": { + "version": "9.7.0", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@cypress/request": "^2.88.10", + "@cypress/xvfb": "^1.2.4", + "@types/node": "^14.14.31", + "@types/sinonjs__fake-timers": "8.1.1", + "@types/sizzle": "^2.3.2", + "arch": "^2.2.0", + "blob-util": "^2.0.2", + "bluebird": "^3.7.2", + "buffer": "^5.6.0", + "cachedir": "^2.3.0", + "chalk": "^4.1.0", + "check-more-types": "^2.24.0", + "cli-cursor": "^3.1.0", + "cli-table3": "~0.6.1", + "commander": "^5.1.0", + "common-tags": "^1.8.0", + "dayjs": "^1.10.4", + "debug": "^4.3.2", + "enquirer": "^2.3.6", + "eventemitter2": "^6.4.3", + "execa": "4.1.0", + "executable": "^4.1.1", + "extract-zip": "2.0.1", + "figures": "^3.2.0", + "fs-extra": "^9.1.0", + "getos": "^3.2.1", + "is-ci": "^3.0.0", + "is-installed-globally": "~0.4.0", + "lazy-ass": "^1.6.0", + "listr2": "^3.8.3", + "lodash": "^4.17.21", + "log-symbols": "^4.0.0", + "minimist": "^1.2.6", + "ospath": "^1.2.2", + "pretty-bytes": "^5.6.0", + "proxy-from-env": "1.0.0", + "request-progress": "^3.0.0", + "semver": "^7.3.2", + "supports-color": "^8.1.1", + "tmp": "~0.2.1", + "untildify": "^4.0.0", + "yauzl": "^2.10.0" + }, + "bin": { + "cypress": "bin/cypress" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/cypress-grep": { + "version": "2.13.1", + "dev": true, + "license": "ISC", + "dependencies": { + "debug": "4.3.1", + "find-test-names": "^1.3.2", + "globby": "^11.0.4" + } + }, + "node_modules/cypress-grep/node_modules/debug": { + "version": "4.3.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/cypress-localstorage-commands": { + "version": "1.7.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "cypress": ">=2.1.0" + } + }, + "node_modules/cypress-terminal-report": { + "version": "3.5.2", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^3.0.0", + "fs-extra": "^9.0.1", + "methods": "^1.1.2", + "semver": "^7.3.5", + "tv4": "^1.3.0" + }, + "peerDependencies": { + "cypress": ">=4.10.0" + } + }, + "node_modules/cypress-terminal-report/node_modules/ansi-styles": { + "version": "4.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cypress-terminal-report/node_modules/chalk": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cypress-terminal-report/node_modules/color-convert": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/cypress-terminal-report/node_modules/color-name": { + "version": "1.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/cypress-terminal-report/node_modules/has-flag": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cypress-terminal-report/node_modules/supports-color": { + "version": "7.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cypress/node_modules/@types/node": { + "version": "14.18.18", + "dev": true, + "license": "MIT" + }, + "node_modules/cypress/node_modules/ansi-styles": { + "version": "4.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cypress/node_modules/chalk": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/cypress/node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cypress/node_modules/color-convert": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/cypress/node_modules/color-name": { + "version": "1.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/cypress/node_modules/commander": { + "version": "5.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/cypress/node_modules/has-flag": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cypress/node_modules/supports-color": { + "version": "8.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/d3": { + "version": "5.16.0", + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "1", + "d3-axis": "1", + "d3-brush": "1", + "d3-chord": "1", + "d3-collection": "1", + "d3-color": "1", + "d3-contour": "1", + "d3-dispatch": "1", + "d3-drag": "1", + "d3-dsv": "1", + "d3-ease": "1", + "d3-fetch": "1", + "d3-force": "1", + "d3-format": "1", + "d3-geo": "1", + "d3-hierarchy": "1", + "d3-interpolate": "1", + "d3-path": "1", + "d3-polygon": "1", + "d3-quadtree": "1", + "d3-random": "1", + "d3-scale": "2", + "d3-scale-chromatic": "1", + "d3-selection": "1", + "d3-shape": "1", + "d3-time": "1", + "d3-time-format": "2", + "d3-timer": "1", + "d3-transition": "1", + "d3-voronoi": "1", + "d3-zoom": "1" + } + }, + "node_modules/d3-array": { + "version": "1.2.4", + "license": "BSD-3-Clause" + }, + "node_modules/d3-axis": { + "version": "1.0.12", + "license": "BSD-3-Clause" + }, + "node_modules/d3-brush": { + "version": "1.1.6", + "license": "BSD-3-Clause", + "dependencies": { + "d3-dispatch": "1", + "d3-drag": "1", + "d3-interpolate": "1", + "d3-selection": "1", + "d3-transition": "1" + } + }, + "node_modules/d3-chord": { + "version": "1.0.6", + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "1", + "d3-path": "1" + } + }, + "node_modules/d3-collection": { + "version": "1.0.7", + "license": "BSD-3-Clause" + }, + "node_modules/d3-color": { + "version": "1.4.1", + "license": "BSD-3-Clause" + }, + "node_modules/d3-contour": { + "version": "1.3.2", + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "^1.1.1" + } + }, + "node_modules/d3-dispatch": { + "version": "1.0.6", + "license": "BSD-3-Clause" + }, + "node_modules/d3-drag": { + "version": "1.2.5", + "license": "BSD-3-Clause", + "dependencies": { + "d3-dispatch": "1", + "d3-selection": "1" + } + }, + "node_modules/d3-dsv": { + "version": "1.2.0", + "license": "BSD-3-Clause", + "dependencies": { + "commander": "2", + "iconv-lite": "0.4", + "rw": "1" + }, + "bin": { + "csv2json": "bin/dsv2json", + "csv2tsv": "bin/dsv2dsv", + "dsv2dsv": "bin/dsv2dsv", + "dsv2json": "bin/dsv2json", + "json2csv": "bin/json2dsv", + "json2dsv": "bin/json2dsv", + "json2tsv": "bin/json2dsv", + "tsv2csv": "bin/dsv2dsv", + "tsv2json": "bin/dsv2json" + } + }, + "node_modules/d3-ease": { + "version": "1.0.7", + "license": "BSD-3-Clause" + }, + "node_modules/d3-fetch": { + "version": "1.2.0", + "license": "BSD-3-Clause", + "dependencies": { + "d3-dsv": "1" + } + }, + "node_modules/d3-force": { + "version": "1.2.1", + "license": "BSD-3-Clause", + "dependencies": { + "d3-collection": "1", + "d3-dispatch": "1", + "d3-quadtree": "1", + "d3-timer": "1" + } + }, + "node_modules/d3-format": { + "version": "1.4.5", + "license": "BSD-3-Clause" + }, + "node_modules/d3-geo": { + "version": "1.12.1", + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "1" + } + }, + "node_modules/d3-hierarchy": { + "version": "1.1.9", + "license": "BSD-3-Clause" + }, + "node_modules/d3-interpolate": { + "version": "1.4.0", + "license": "BSD-3-Clause", + "dependencies": { + "d3-color": "1" + } + }, + "node_modules/d3-path": { + "version": "1.0.9", + "license": "BSD-3-Clause" + }, + "node_modules/d3-polygon": { + "version": "1.0.6", + "license": "BSD-3-Clause" + }, + "node_modules/d3-quadtree": { + "version": "1.0.7", + "license": "BSD-3-Clause" + }, + "node_modules/d3-random": { + "version": "1.1.2", + "license": "BSD-3-Clause" + }, + "node_modules/d3-scale": { + "version": "2.2.2", + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "^1.2.0", + "d3-collection": "1", + "d3-format": "1", + "d3-interpolate": "1", + "d3-time": "1", + "d3-time-format": "2" + } + }, + "node_modules/d3-scale-chromatic": { + "version": "1.5.0", + "license": "BSD-3-Clause", + "dependencies": { + "d3-color": "1", + "d3-interpolate": "1" + } + }, + "node_modules/d3-selection": { + "version": "1.4.2", + "license": "BSD-3-Clause" + }, + "node_modules/d3-shape": { + "version": "1.3.7", + "license": "BSD-3-Clause", + "dependencies": { + "d3-path": "1" + } + }, + "node_modules/d3-time": { + "version": "1.1.0", + "license": "BSD-3-Clause" + }, + "node_modules/d3-time-format": { + "version": "2.3.0", + "license": "BSD-3-Clause", + "dependencies": { + "d3-time": "1" + } + }, + "node_modules/d3-timer": { + "version": "1.0.10", + "license": "BSD-3-Clause" + }, + "node_modules/d3-transition": { + "version": "1.3.2", + "license": "BSD-3-Clause", + "dependencies": { + "d3-color": "1", + "d3-dispatch": "1", + "d3-ease": "1", + "d3-interpolate": "1", + "d3-selection": "^1.1.0", + "d3-timer": "1" + } + }, + "node_modules/d3-voronoi": { + "version": "1.1.4", + "license": "BSD-3-Clause" + }, + "node_modules/d3-zoom": { + "version": "1.8.3", + "license": "BSD-3-Clause", + "dependencies": { + "d3-dispatch": "1", + "d3-drag": "1", + "d3-interpolate": "1", + "d3-selection": "1", + "d3-transition": "1" + } + }, + "node_modules/dagre": { + "version": "0.8.5", + "license": "MIT", + "dependencies": { + "graphlib": "^2.1.8", + "lodash": "^4.17.15" + } + }, + "node_modules/dashdash": { + "version": "1.14.1", + "dev": true, + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/date-format": { + "version": "4.0.10", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/dayjs": { + "version": "1.11.2", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.3.3", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "5.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decode-uri-component": { + "version": "0.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/decompress-response": { + "version": "3.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/deep-equal": { + "version": "1.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arguments": "^1.0.4", + "is-date-object": "^1.0.1", + "is-regex": "^1.0.4", + "object-is": "^1.0.1", + "object-keys": "^1.1.1", + "regexp.prototype.flags": "^1.2.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/deepmerge": { + "version": "4.2.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/default-gateway": { + "version": "6.0.3", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "execa": "^5.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/default-gateway/node_modules/execa": { + "version": "5.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/default-gateway/node_modules/get-stream": { + "version": "6.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-gateway/node_modules/human-signals": { + "version": "2.1.0", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/defaults": { + "version": "1.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "clone": "^1.0.2" + } + }, + "node_modules/defer-to-connect": { + "version": "1.1.3", + "dev": true, + "license": "MIT" + }, + "node_modules/define-lazy-prop": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/define-properties": { + "version": "1.1.4", + "dev": true, + "license": "MIT", + "dependencies": { + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/del": { + "version": "6.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "globby": "^11.0.1", + "graceful-fs": "^4.2.4", + "is-glob": "^4.0.1", + "is-path-cwd": "^2.2.0", + "is-path-inside": "^3.0.2", + "p-map": "^4.0.0", + "rimraf": "^3.0.2", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/delegates": { + "version": "1.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/depd": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dependency-graph": { + "version": "0.11.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-node": { + "version": "2.1.0", + "dev": true, + "license": "MIT" + }, + "node_modules/di": { + "version": "0.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dns-equal": { + "version": "1.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/dns-packet": { + "version": "1.3.4", + "dev": true, + "license": "MIT", + "dependencies": { + "ip": "^1.1.0", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/dns-txt": { + "version": "2.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-indexof": "^1.0.0" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dom-serialize": { + "version": "2.2.1", + "dev": true, + "license": "MIT", + "dependencies": { + "custom-event": "~1.0.0", + "ent": "~2.2.0", + "extend": "^3.0.0", + "void-elements": "^2.0.0" + } + }, + "node_modules/dom-serializer": { + "version": "1.4.1", + "dev": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "4.3.1", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.2.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/dompurify": { + "version": "2.3.8", + "license": "(MPL-2.0 OR Apache-2.0)" + }, + "node_modules/domutils": { + "version": "2.8.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dot-prop": { + "version": "5.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "is-obj": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dotenv": { + "version": "10.0.0", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=10" + } + }, + "node_modules/duplexer": { + "version": "0.1.2", + "dev": true, + "license": "MIT" + }, + "node_modules/duplexer3": { + "version": "0.1.4", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/ecc-jsbn": { + "version": "0.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "dev": true, + "license": "MIT" + }, + "node_modules/ejs": { + "version": "3.1.8", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.4.139", + "dev": true, + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/emojis-list": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/encoding": { + "version": "0.1.13", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/engine.io": { + "version": "6.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/cookie": "^0.4.1", + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.4.1", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.0.3", + "ws": "~8.2.3" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io-parser": { + "version": "5.0.4", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io/node_modules/ws": { + "version": "8.2.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/enhanced-resolve": { + "version": "5.9.3", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/enquirer": { + "version": "2.3.6", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-colors": "^4.1.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/ent": { + "version": "2.2.0", + "dev": true, + "license": "MIT" + }, + "node_modules/entities": { + "version": "2.2.0", + "dev": true, + "license": "BSD-2-Clause", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "dev": true, + "license": "MIT" + }, + "node_modules/errno": { + "version": "0.1.8", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "prr": "~1.0.1" + }, + "bin": { + "errno": "cli.js" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-abstract": { + "version": "1.20.1", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "function.prototype.name": "^1.1.5", + "get-intrinsic": "^1.1.1", + "get-symbol-description": "^1.0.0", + "has": "^1.0.3", + "has-property-descriptors": "^1.0.0", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.3", + "is-callable": "^1.2.4", + "is-negative-zero": "^2.0.2", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "is-string": "^1.0.7", + "is-weakref": "^1.0.2", + "object-inspect": "^1.12.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.2", + "regexp.prototype.flags": "^1.4.3", + "string.prototype.trimend": "^1.0.5", + "string.prototype.trimstart": "^1.0.5", + "unbox-primitive": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-module-lexer": { + "version": "0.9.3", + "dev": true, + "license": "MIT" + }, + "node_modules/es-shim-unscopables": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has": "^1.0.3" + } + }, + "node_modules/es-to-primitive": { + "version": "1.2.1", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/esbuild": { + "version": "0.14.22", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "esbuild-android-arm64": "0.14.22", + "esbuild-darwin-64": "0.14.22", + "esbuild-darwin-arm64": "0.14.22", + "esbuild-freebsd-64": "0.14.22", + "esbuild-freebsd-arm64": "0.14.22", + "esbuild-linux-32": "0.14.22", + "esbuild-linux-64": "0.14.22", + "esbuild-linux-arm": "0.14.22", + "esbuild-linux-arm64": "0.14.22", + "esbuild-linux-mips64le": "0.14.22", + "esbuild-linux-ppc64le": "0.14.22", + "esbuild-linux-riscv64": "0.14.22", + "esbuild-linux-s390x": "0.14.22", + "esbuild-netbsd-64": "0.14.22", + "esbuild-openbsd-64": "0.14.22", + "esbuild-sunos-64": "0.14.22", + "esbuild-windows-32": "0.14.22", + "esbuild-windows-64": "0.14.22", + "esbuild-windows-arm64": "0.14.22" + } + }, + "node_modules/esbuild-darwin-arm64": { + "version": "0.14.22", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-wasm": { + "version": "0.14.22", + "dev": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/escalade": { + "version": "3.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-goat": { + "version": "2.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "dev": true, + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/eslint": { + "version": "8.16.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint/eslintrc": "^1.3.0", + "@humanwhocodes/config-array": "^0.9.2", + "ajv": "^6.10.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.1.1", + "eslint-utils": "^3.0.0", + "eslint-visitor-keys": "^3.3.0", + "espree": "^9.3.2", + "esquery": "^1.4.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "functional-red-black-tree": "^1.0.1", + "glob-parent": "^6.0.1", + "globals": "^13.15.0", + "ignore": "^5.2.0", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.1", + "regexpp": "^3.2.0", + "strip-ansi": "^6.0.1", + "strip-json-comments": "^3.1.0", + "text-table": "^0.2.0", + "v8-compile-cache": "^2.0.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-etc": { + "version": "5.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/experimental-utils": "^5.0.0", + "tsutils": "^3.17.1", + "tsutils-etc": "^1.4.1" + }, + "peerDependencies": { + "eslint": "^8.0.0", + "typescript": "^4.0.0" + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.6", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7", + "resolve": "^1.20.0" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-import-resolver-typescript": { + "version": "2.7.1", + "dev": true, + "license": "ISC", + "dependencies": { + "debug": "^4.3.4", + "glob": "^7.2.0", + "is-glob": "^4.0.3", + "resolve": "^1.22.0", + "tsconfig-paths": "^3.14.1" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "*", + "eslint-plugin-import": "*" + } + }, + "node_modules/eslint-import-resolver-typescript/node_modules/debug": { + "version": "4.3.4", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils": { + "version": "2.7.3", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7", + "find-up": "^2.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-module-utils/node_modules/find-up": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-module-utils/node_modules/locate-path": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-module-utils/node_modules/p-limit": { + "version": "1.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-module-utils/node_modules/p-locate": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-module-utils/node_modules/p-try": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-module-utils/node_modules/path-exists": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/eslint-plugin-cypress": { + "version": "2.12.1", + "dev": true, + "license": "MIT", + "dependencies": { + "globals": "^11.12.0" + }, + "peerDependencies": { + "eslint": ">= 3.2.1" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.26.0", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.4", + "array.prototype.flat": "^1.2.5", + "debug": "^2.6.9", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.6", + "eslint-module-utils": "^2.7.3", + "has": "^1.0.3", + "is-core-module": "^2.8.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.values": "^1.1.5", + "resolve": "^1.22.0", + "tsconfig-paths": "^3.14.1" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "2.6.9", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/eslint-plugin-import/node_modules/doctrine": { + "version": "2.1.0", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-import/node_modules/minimatch": { + "version": "3.1.2", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint-plugin-import/node_modules/ms": { + "version": "2.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint-plugin-jasmine": { + "version": "4.1.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8", + "npm": ">=6" + } + }, + "node_modules/eslint-plugin-jsdoc": { + "version": "39.3.2", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@es-joy/jsdoccomment": "~0.31.0", + "comment-parser": "1.3.1", + "debug": "^4.3.4", + "escape-string-regexp": "^4.0.0", + "esquery": "^1.4.0", + "semver": "^7.3.7", + "spdx-expression-parse": "^3.0.1" + }, + "engines": { + "node": "^14 || ^16 || ^17 || ^18" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + } + }, + "node_modules/eslint-plugin-jsdoc/node_modules/debug": { + "version": "4.3.4", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-jsdoc/node_modules/escape-string-regexp": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-plugin-jsdoc/node_modules/semver": { + "version": "7.3.7", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint-plugin-ngrx": { + "version": "2.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/schematics": "^13.0.3", + "@typescript-eslint/experimental-utils": "^5.4.0", + "eslint-etc": "^5.1.0", + "semver": "^7.3.5", + "strip-json-comments": "3.1.1" + }, + "peerDependencies": { + "eslint": ">=8.0.0", + "typescript": ">=4.4.0" + } + }, + "node_modules/eslint-plugin-prefer-arrow": { + "version": "1.2.3", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=2.0.0" + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "prettier-linter-helpers": "^1.0.0" + }, + "engines": { + "node": ">=6.0.0" + }, + "peerDependencies": { + "eslint": ">=7.28.0", + "prettier": ">=2.0.0" + }, + "peerDependenciesMeta": { + "eslint-config-prettier": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-rxjs": { + "version": "5.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/experimental-utils": "^5.0.0", + "common-tags": "^1.8.0", + "decamelize": "^5.0.0", + "eslint-etc": "^5.1.0", + "requireindex": "~1.2.0", + "rxjs-report-usage": "^1.0.4", + "tslib": "^2.0.0", + "tsutils": "^3.0.0", + "tsutils-etc": "^1.4.1" + }, + "peerDependencies": { + "eslint": "^8.0.0", + "typescript": "^4.0.0" + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/eslint-utils": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^2.0.0" + }, + "engines": { + "node": "^10.0.0 || ^12.0.0 || >= 14.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=5" + } + }, + "node_modules/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "2.1.0", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.3.0", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/eslint/node_modules/ajv": { + "version": "6.12.6", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/eslint/node_modules/ansi-styles": { + "version": "4.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/eslint/node_modules/argparse": { + "version": "2.0.1", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/eslint/node_modules/chalk": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/eslint/node_modules/color-convert": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/eslint/node_modules/color-name": { + "version": "1.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint/node_modules/escape-string-regexp": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/eslint-scope": { + "version": "7.1.1", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/eslint/node_modules/estraverse": { + "version": "5.3.0", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/eslint/node_modules/glob-parent": { + "version": "6.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/eslint/node_modules/globals": { + "version": "13.15.0", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/has-flag": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/js-yaml": { + "version": "4.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/eslint/node_modules/json-schema-traverse": { + "version": "0.4.1", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint/node_modules/supports-color": { + "version": "7.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/type-fest": { + "version": "0.20.2", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/espree": { + "version": "9.3.2", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.7.1", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.4.0", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esquery/node_modules/estraverse": { + "version": "5.3.0", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "1.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/esutils": { + "version": "2.0.3", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/event-stream": { + "version": "3.3.4", + "dev": true, + "license": "MIT", + "dependencies": { + "duplexer": "~0.1.1", + "from": "~0", + "map-stream": "~0.1.0", + "pause-stream": "0.0.11", + "split": "0.3", + "stream-combiner": "~0.0.4", + "through": "~2.3.1" + } + }, + "node_modules/eventemitter-asyncresource": { + "version": "1.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/eventemitter2": { + "version": "6.4.5", + "dev": true, + "license": "MIT" + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "dev": true, + "license": "MIT" + }, + "node_modules/events": { + "version": "3.3.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/execa": { + "version": "4.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.0", + "get-stream": "^5.0.0", + "human-signals": "^1.1.1", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.0", + "onetime": "^5.1.0", + "signal-exit": "^3.0.2", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/executable": { + "version": "4.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.2.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/express": { + "version": "4.18.1", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.0", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.5.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.10.3", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/express/node_modules/array-flatten": { + "version": "1.1.1", + "dev": true, + "license": "MIT" + }, + "node_modules/express/node_modules/cookie": { + "version": "0.5.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/finalhandler": { + "version": "1.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/express/node_modules/qs": { + "version": "6.10.3", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/express/node_modules/safe-buffer": { + "version": "5.2.1", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/express/node_modules/statuses": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/external-editor": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/external-editor/node_modules/tmp": { + "version": "0.0.33", + "dev": true, + "license": "MIT", + "dependencies": { + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/extract-zip": { + "version": "2.0.1", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/extsprintf": { + "version": "1.3.0", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-diff": { + "version": "1.2.0", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/fast-equals": { + "version": "2.0.4", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.2.11", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-sha256": { + "version": "1.3.0", + "dev": true, + "license": "Unlicense" + }, + "node_modules/fastq": { + "version": "1.13.0", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/faye-websocket": { + "version": "0.11.4", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/figures": { + "version": "3.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/filelist": { + "version": "1.0.4", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.0", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/finalhandler/node_modules/on-finished": { + "version": "2.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/find-cache-dir": { + "version": "3.3.2", + "dev": true, + "license": "MIT", + "dependencies": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/avajs/find-cache-dir?sponsor=1" + } + }, + "node_modules/find-test-names": { + "version": "1.17.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.16.5", + "acorn-walk": "^8.2.0", + "debug": "^4.3.3", + "globby": "^11.0.4", + "simple-bin-help": "^1.7.6" + }, + "bin": { + "find-test-names": "bin/find-test-names.js", + "print-tests": "bin/print-tests.js", + "update-test-count": "bin/update-test-count.js" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "dev": true, + "license": "BSD-3-Clause", + "bin": { + "flat": "cli.js" + } + }, + "node_modules/flat-cache": { + "version": "3.0.4", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.1.0", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.2.5", + "dev": true, + "license": "ISC" + }, + "node_modules/follow-redirects": { + "version": "1.15.0", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/forever-agent": { + "version": "0.6.1", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "*" + } + }, + "node_modules/form-data": { + "version": "2.3.3", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fraction.js": { + "version": "4.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://www.patreon.com/infusion" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/from": { + "version": "0.1.7", + "dev": true, + "license": "MIT" + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/fs-extra": { + "version": "9.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-monkey": { + "version": "1.0.3", + "dev": true, + "license": "Unlicense" + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.2", + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.1", + "dev": true, + "license": "MIT" + }, + "node_modules/function.prototype.name": { + "version": "1.1.5", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.0", + "functions-have-names": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functional-red-black-tree": { + "version": "1.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gauge": { + "version": "4.0.4", + "dev": true, + "license": "ISC", + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.3", + "console-control-strings": "^1.1.0", + "has-unicode": "^2.0.1", + "signal-exit": "^3.0.7", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-stream": { + "version": "5.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-symbol-description": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/getos": { + "version": "3.2.1", + "dev": true, + "license": "MIT", + "dependencies": { + "async": "^3.2.0" + } + }, + "node_modules/getpass": { + "version": "0.1.7", + "dev": true, + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0" + } + }, + "node_modules/glob": { + "version": "7.2.0", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/global-dirs": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ini": "2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globals": { + "version": "11.12.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/got": { + "version": "9.6.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^0.14.0", + "@szmarczak/http-timer": "^1.1.2", + "cacheable-request": "^6.0.0", + "decompress-response": "^3.3.0", + "duplexer3": "^0.1.4", + "get-stream": "^4.1.0", + "lowercase-keys": "^1.0.1", + "mimic-response": "^1.0.1", + "p-cancelable": "^1.0.0", + "to-readable-stream": "^1.0.0", + "url-parse-lax": "^3.0.0" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/got/node_modules/get-stream": { + "version": "4.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.10", + "dev": true, + "license": "ISC" + }, + "node_modules/graphlib": { + "version": "2.1.8", + "license": "MIT", + "dependencies": { + "lodash": "^4.17.15" + } + }, + "node_modules/handle-thing": { + "version": "2.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/has": { + "version": "1.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-bigints": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-unicode": { + "version": "2.0.1", + "dev": true, + "license": "ISC" + }, + "node_modules/has-yarn": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hdr-histogram-js": { + "version": "2.0.3", + "dev": true, + "license": "BSD", + "dependencies": { + "@assemblyscript/loader": "^0.10.1", + "base64-js": "^1.2.0", + "pako": "^1.0.3" + } + }, + "node_modules/hdr-histogram-percentiles-obj": { + "version": "3.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/hosted-git-info": { + "version": "4.1.0", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/hpack.js": { + "version": "2.1.6", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.1", + "obuf": "^1.0.0", + "readable-stream": "^2.0.1", + "wbuf": "^1.1.0" + } + }, + "node_modules/hpack.js/node_modules/readable-stream": { + "version": "2.3.7", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/hpack.js/node_modules/string_decoder": { + "version": "1.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/html-entities": { + "version": "2.3.3", + "dev": true, + "license": "MIT" + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/http-cache-semantics": { + "version": "4.1.0", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/http-deceiver": { + "version": "1.2.7", + "dev": true, + "license": "MIT" + }, + "node_modules/http-errors": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-errors/node_modules/statuses": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-parser-js": { + "version": "0.5.6", + "dev": true, + "license": "MIT" + }, + "node_modules/http-proxy": { + "version": "1.18.1", + "dev": true, + "license": "MIT", + "dependencies": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/http-proxy-agent": { + "version": "4.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/http-proxy-middleware": { + "version": "2.0.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-proxy": "^1.17.8", + "http-proxy": "^1.18.1", + "is-glob": "^4.0.1", + "is-plain-obj": "^3.0.0", + "micromatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "@types/express": "^4.17.13" + }, + "peerDependenciesMeta": { + "@types/express": { + "optional": true + } + } + }, + "node_modules/http-signature": { + "version": "1.3.6", + "dev": true, + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0", + "jsprim": "^2.0.2", + "sshpk": "^1.14.1" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/human-signals": { + "version": "1.1.1", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8.12.0" + } + }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.0.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/icss-utils": { + "version": "5.1.0", + "dev": true, + "license": "ISC", + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/ignore": { + "version": "5.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/ignore-walk": { + "version": "4.0.1", + "dev": true, + "license": "ISC", + "dependencies": { + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/image-size": { + "version": "0.5.5", + "dev": true, + "license": "MIT", + "optional": true, + "bin": { + "image-size": "bin/image-size.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/immutable": { + "version": "4.1.0", + "dev": true, + "license": "MIT" + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-fresh/node_modules/resolve-from": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/import-lazy": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/infer-owner": { + "version": "1.0.4", + "dev": true, + "license": "ISC" + }, + "node_modules/inflight": { + "version": "1.0.6", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "dev": true, + "license": "ISC" + }, + "node_modules/ini": { + "version": "2.0.0", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/injection-js": { + "version": "2.4.0", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + } + }, + "node_modules/inquirer": { + "version": "8.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^4.2.1", + "chalk": "^4.1.1", + "cli-cursor": "^3.1.0", + "cli-width": "^3.0.0", + "external-editor": "^3.0.3", + "figures": "^3.0.0", + "lodash": "^4.17.21", + "mute-stream": "0.0.8", + "ora": "^5.4.1", + "run-async": "^2.4.0", + "rxjs": "^7.2.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "through": "^2.3.6" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/inquirer/node_modules/ansi-styles": { + "version": "4.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/inquirer/node_modules/chalk": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/inquirer/node_modules/color-convert": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/inquirer/node_modules/color-name": { + "version": "1.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/inquirer/node_modules/has-flag": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/inquirer/node_modules/supports-color": { + "version": "7.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/internal-slot": { + "version": "1.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.1.0", + "has": "^1.0.3", + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ip": { + "version": "1.1.8", + "dev": true, + "license": "MIT" + }, + "node_modules/ipaddr.js": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/is-arguments": { + "version": "1.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "dev": true, + "license": "MIT" + }, + "node_modules/is-bigint": { + "version": "1.0.4", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-boolean-object": { + "version": "1.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-builtin-module": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "builtin-modules": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/is-callable": { + "version": "1.2.4", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-ci": { + "version": "3.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ci-info": "^3.2.0" + }, + "bin": { + "is-ci": "bin.js" + } + }, + "node_modules/is-core-module": { + "version": "2.9.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.0.5", + "dev": true, + "license": "MIT", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-docker": { + "version": "2.2.1", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-installed-globally": { + "version": "0.4.0", + "dev": true, + "license": "MIT", + "dependencies": { + "global-dirs": "^3.0.0", + "is-path-inside": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-interactive": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-lambda": { + "version": "1.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/is-module": { + "version": "1.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/is-negative-zero": { + "version": "2.0.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-npm": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.0.7", + "dev": true, + "license": "MIT", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-obj": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-path-cwd": { + "version": "2.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-obj": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "dev": true, + "license": "MIT", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-regex": { + "version": "1.1.4", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-string": { + "version": "1.0.7", + "dev": true, + "license": "MIT", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.0.4", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-weakref": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-what": { + "version": "3.14.1", + "dev": true, + "license": "MIT" + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-yarn-global": { + "version": "0.3.0", + "dev": true, + "license": "MIT" + }, + "node_modules/isarray": { + "version": "1.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/isbinaryfile": { + "version": "4.0.10", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "dev": true, + "license": "ISC" + }, + "node_modules/isobject": { + "version": "3.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isstream": { + "version": "0.1.2", + "dev": true, + "license": "MIT" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.0", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "5.2.0", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "6.3.0", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.0", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^3.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report/node_modules/has-flag": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report/node_modules/supports-color": { + "version": "7.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/source-map": { + "version": "0.6.1", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.4", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jake": { + "version": "10.8.5", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.1", + "minimatch": "^3.0.4" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jake/node_modules/ansi-styles": { + "version": "4.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jake/node_modules/chalk": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jake/node_modules/color-convert": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jake/node_modules/color-name": { + "version": "1.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/jake/node_modules/has-flag": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jake/node_modules/supports-color": { + "version": "7.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jasmine-core": { + "version": "4.1.1", + "dev": true, + "license": "MIT" + }, + "node_modules/jasmine-spec-reporter": { + "version": "7.0.0", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "colors": "1.4.0" + } + }, + "node_modules/jest-worker": { + "version": "27.5.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/jest-worker/node_modules/has-flag": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/joi": { + "version": "17.6.0", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.0.0", + "@hapi/topo": "^5.0.0", + "@sideway/address": "^4.1.3", + "@sideway/formula": "^3.0.0", + "@sideway/pinpoint": "^2.0.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "3.14.1", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsbn": { + "version": "0.1.1", + "dev": true, + "license": "MIT" + }, + "node_modules/jsdoc-type-pratt-parser": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/jsesc": { + "version": "2.5.2", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/json-buffer": { + "version": "3.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/json-parse-better-errors": { + "version": "1.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema": { + "version": "0.4.0", + "dev": true, + "license": "(AFL-2.1 OR BSD-3-Clause)" + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "dev": true, + "license": "ISC" + }, + "node_modules/json5": { + "version": "2.2.1", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonc-parser": { + "version": "3.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonparse": { + "version": "1.3.1", + "dev": true, + "engines": [ + "node >= 0.2.0" + ], + "license": "MIT" + }, + "node_modules/jsprim": { + "version": "2.0.2", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "license": "MIT", + "dependencies": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.4.0", + "verror": "1.10.0" + } + }, + "node_modules/karma": { + "version": "6.3.20", + "dev": true, + "license": "MIT", + "dependencies": { + "@colors/colors": "1.5.0", + "body-parser": "^1.19.0", + "braces": "^3.0.2", + "chokidar": "^3.5.1", + "connect": "^3.7.0", + "di": "^0.0.1", + "dom-serialize": "^2.2.1", + "glob": "^7.1.7", + "graceful-fs": "^4.2.6", + "http-proxy": "^1.18.1", + "isbinaryfile": "^4.0.8", + "lodash": "^4.17.21", + "log4js": "^6.4.1", + "mime": "^2.5.2", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.5", + "qjobs": "^1.2.0", + "range-parser": "^1.2.1", + "rimraf": "^3.0.2", + "socket.io": "^4.4.1", + "source-map": "^0.6.1", + "tmp": "^0.2.1", + "ua-parser-js": "^0.7.30", + "yargs": "^16.1.1" + }, + "bin": { + "karma": "bin/karma" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/karma-chrome-launcher": { + "version": "3.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "which": "^1.2.1" + } + }, + "node_modules/karma-chrome-launcher/node_modules/which": { + "version": "1.3.1", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/karma-coverage": { + "version": "2.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "istanbul-lib-coverage": "^3.2.0", + "istanbul-lib-instrument": "^5.1.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.1", + "istanbul-reports": "^3.0.5", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/karma-jasmine": { + "version": "5.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "jasmine-core": "^4.1.0" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "karma": "^6.0.0" + } + }, + "node_modules/karma-jasmine-html-reporter": { + "version": "1.7.0", + "dev": true, + "license": "MIT", + "peerDependencies": { + "jasmine-core": ">=3.8", + "karma": ">=0.9", + "karma-jasmine": ">=1.1" + } + }, + "node_modules/karma-junit-reporter": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "path-is-absolute": "^1.0.0", + "xmlbuilder": "12.0.0" + }, + "engines": { + "node": ">= 8" + }, + "peerDependencies": { + "karma": ">=0.9" + } + }, + "node_modules/karma-source-map-support": { + "version": "1.4.0", + "dev": true, + "license": "MIT", + "dependencies": { + "source-map-support": "^0.5.5" + } + }, + "node_modules/karma/node_modules/mkdirp": { + "version": "0.5.6", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/karma/node_modules/source-map": { + "version": "0.6.1", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/karma/node_modules/yargs": { + "version": "16.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/karma/node_modules/yargs-parser": { + "version": "20.2.9", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/keyv": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.0" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/klona": { + "version": "2.0.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/latest-version": { + "version": "5.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "package-json": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lazy-ass": { + "version": "1.6.0", + "dev": true, + "license": "MIT", + "engines": { + "node": "> 0.8" + } + }, + "node_modules/less": { + "version": "4.1.2", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "copy-anything": "^2.0.1", + "parse-node-version": "^1.0.1", + "tslib": "^2.3.0" + }, + "bin": { + "lessc": "bin/lessc" + }, + "engines": { + "node": ">=6" + }, + "optionalDependencies": { + "errno": "^0.1.1", + "graceful-fs": "^4.1.2", + "image-size": "~0.5.0", + "make-dir": "^2.1.0", + "mime": "^1.4.1", + "needle": "^2.5.2", + "source-map": "~0.6.0" + } + }, + "node_modules/less-loader": { + "version": "10.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "klona": "^2.0.4" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "less": "^3.5.0 || ^4.0.0", + "webpack": "^5.0.0" + } + }, + "node_modules/less/node_modules/make-dir": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "pify": "^4.0.1", + "semver": "^5.6.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/less/node_modules/mime": { + "version": "1.6.0", + "dev": true, + "license": "MIT", + "optional": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/less/node_modules/pify": { + "version": "4.0.1", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/less/node_modules/semver": { + "version": "5.7.1", + "dev": true, + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/less/node_modules/source-map": { + "version": "0.6.1", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/license-webpack-plugin": { + "version": "4.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "webpack-sources": "^3.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + }, + "webpack-sources": { + "optional": true + } + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "dev": true, + "license": "MIT" + }, + "node_modules/listr2": { + "version": "3.14.0", + "dev": true, + "license": "MIT", + "dependencies": { + "cli-truncate": "^2.1.0", + "colorette": "^2.0.16", + "log-update": "^4.0.0", + "p-map": "^4.0.0", + "rfdc": "^1.3.0", + "rxjs": "^7.5.1", + "through": "^2.3.8", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "enquirer": ">= 2.3.0 < 3" + }, + "peerDependenciesMeta": { + "enquirer": { + "optional": true + } + } + }, + "node_modules/lit": { + "version": "2.2.5", + "license": "BSD-3-Clause", + "dependencies": { + "@lit/reactive-element": "^1.3.0", + "lit-element": "^3.2.0", + "lit-html": "^2.2.0" + } + }, + "node_modules/lit-element": { + "version": "3.2.0", + "license": "BSD-3-Clause", + "dependencies": { + "@lit/reactive-element": "^1.3.0", + "lit-html": "^2.2.0" + } + }, + "node_modules/lit-html": { + "version": "2.2.5", + "license": "BSD-3-Clause", + "dependencies": { + "@types/trusted-types": "^2.0.2" + } + }, + "node_modules/load-json-file": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.2", + "parse-json": "^4.0.0", + "pify": "^3.0.0", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/load-json-file/node_modules/parse-json": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/load-json-file/node_modules/pify": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/loader-runner": { + "version": "4.3.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.11.5" + } + }, + "node_modules/loader-utils": { + "version": "3.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/locate-path": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "license": "MIT" + }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "dev": true, + "license": "MIT" + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-symbols/node_modules/ansi-styles": { + "version": "4.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/log-symbols/node_modules/chalk": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/log-symbols/node_modules/color-convert": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/log-symbols/node_modules/color-name": { + "version": "1.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/log-symbols/node_modules/has-flag": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/log-symbols/node_modules/supports-color": { + "version": "7.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/log-update": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^4.3.0", + "cli-cursor": "^3.1.0", + "slice-ansi": "^4.0.0", + "wrap-ansi": "^6.2.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-styles": { + "version": "4.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/log-update/node_modules/color-convert": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/log-update/node_modules/color-name": { + "version": "1.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/log-update/node_modules/slice-ansi": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/log-update/node_modules/wrap-ansi": { + "version": "6.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/log4js": { + "version": "6.5.1", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "date-format": "^4.0.10", + "debug": "^4.3.4", + "flatted": "^3.2.5", + "rfdc": "^1.3.0", + "streamroller": "^3.1.1" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/log4js/node_modules/debug": { + "version": "4.3.4", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/lowercase-keys": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/magic-string": { + "version": "0.25.7", + "dev": true, + "license": "MIT", + "dependencies": { + "sourcemap-codec": "^1.4.4" + } + }, + "node_modules/make-dir": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.0", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/make-fetch-happen": { + "version": "9.1.0", + "dev": true, + "license": "ISC", + "dependencies": { + "agentkeepalive": "^4.1.3", + "cacache": "^15.2.0", + "http-cache-semantics": "^4.1.0", + "http-proxy-agent": "^4.0.1", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^6.0.0", + "minipass": "^3.1.3", + "minipass-collect": "^1.0.2", + "minipass-fetch": "^1.3.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.2", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^6.0.0", + "ssri": "^8.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/map-stream": { + "version": "0.1.0", + "dev": true + }, + "node_modules/media-typer": { + "version": "0.3.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/memfs": { + "version": "3.4.3", + "dev": true, + "license": "Unlicense", + "dependencies": { + "fs-monkey": "1.0.3" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/memorystream": { + "version": "0.3.1", + "dev": true, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromatch": { + "version": "4.0.5", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "2.6.0", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/mimic-response": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/mini-css-extract-plugin": { + "version": "2.5.3", + "dev": true, + "license": "MIT", + "dependencies": { + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/mini-css-extract-plugin/node_modules/ajv-keywords": { + "version": "5.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/mini-css-extract-plugin/node_modules/schema-utils": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.8.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.0.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "dev": true, + "license": "ISC" + }, + "node_modules/minimatch": { + "version": "3.0.5", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.6", + "dev": true, + "license": "MIT" + }, + "node_modules/minipass": { + "version": "3.1.6", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-collect": { + "version": "1.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-fetch": { + "version": "1.4.1", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^3.1.0", + "minipass-sized": "^1.0.3", + "minizlib": "^2.0.0" + }, + "engines": { + "node": ">=8" + }, + "optionalDependencies": { + "encoding": "^0.1.12" + } + }, + "node_modules/minipass-flush": { + "version": "1.0.5", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-json-stream": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "jsonparse": "^1.3.1", + "minipass": "^3.0.0" + } + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized": { + "version": "1.0.3", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/modern-normalize": { + "version": "1.1.0", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "dev": true, + "license": "MIT" + }, + "node_modules/multicast-dns": { + "version": "6.2.3", + "dev": true, + "license": "MIT", + "dependencies": { + "dns-packet": "^1.3.1", + "thunky": "^1.0.2" + }, + "bin": { + "multicast-dns": "cli.js" + } + }, + "node_modules/multicast-dns-service-types": { + "version": "1.1.0", + "dev": true, + "license": "MIT" + }, + "node_modules/mute-stream": { + "version": "0.0.8", + "dev": true, + "license": "ISC" + }, + "node_modules/nanoid": { + "version": "3.3.4", + "dev": true, + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "dev": true, + "license": "MIT" + }, + "node_modules/needle": { + "version": "2.9.1", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "debug": "^3.2.6", + "iconv-lite": "^0.4.4", + "sax": "^1.2.4" + }, + "bin": { + "needle": "bin/needle" + }, + "engines": { + "node": ">= 4.4.x" + } + }, + "node_modules/needle/node_modules/debug": { + "version": "3.2.7", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/negotiator": { + "version": "0.6.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "dev": true, + "license": "MIT" + }, + "node_modules/ng-packagr": { + "version": "13.3.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/plugin-json": "^4.1.0", + "@rollup/plugin-node-resolve": "^13.0.0", + "ajv": "^8.0.0", + "ansi-colors": "^4.1.1", + "browserslist": "^4.16.1", + "cacache": "^15.0.6", + "chokidar": "^3.5.1", + "commander": "^8.0.0", + "dependency-graph": "^0.11.0", + "esbuild-wasm": "^0.14.0", + "find-cache-dir": "^3.3.1", + "glob": "^7.1.6", + "injection-js": "^2.4.0", + "jsonc-parser": "^3.0.0", + "less": "^4.1.0", + "ora": "^5.1.0", + "postcss": "^8.2.4", + "postcss-preset-env": "^7.0.0", + "postcss-url": "^10.1.1", + "rollup": "^2.45.1", + "rollup-plugin-sourcemaps": "^0.6.3", + "rxjs": "^7.0.0", + "sass": "^1.32.8", + "stylus": "^0.56.0" + }, + "bin": { + "ng-packagr": "cli/main.js" + }, + "engines": { + "node": "^12.20.0 || ^14.15.0 || >=16.10.0" + }, + "optionalDependencies": { + "esbuild": "^0.14.0" + }, + "peerDependencies": { + "@angular/compiler-cli": "^13.0.0", + "tslib": "^2.3.0", + "typescript": ">=4.4.0 <4.7" + } + }, + "node_modules/ng-packagr/node_modules/commander": { + "version": "8.3.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/ngx-clipboard": { + "version": "15.1.0", + "license": "MIT", + "dependencies": { + "ngx-window-token": ">=6.0.0", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@angular/common": ">=13.0.0", + "@angular/core": ">=13.0.0" + } + }, + "node_modules/ngx-cookie-service": { + "version": "13.2.1", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@angular/common": "^13.0.0", + "@angular/core": "^13.0.0" + } + }, + "node_modules/ngx-window-token": { + "version": "6.0.0", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=12.20.x" + }, + "peerDependencies": { + "@angular/common": ">=13.0.0", + "@angular/core": ">=13.0.0" + } + }, + "node_modules/nice-napi": { + "version": "1.0.2", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "!win32" + ], + "dependencies": { + "node-addon-api": "^3.0.0", + "node-gyp-build": "^4.2.2" + } + }, + "node_modules/nice-try": { + "version": "1.0.5", + "dev": true, + "license": "MIT" + }, + "node_modules/node-addon-api": { + "version": "3.2.1", + "dev": true, + "license": "MIT" + }, + "node_modules/node-forge": { + "version": "1.3.1", + "dev": true, + "license": "(BSD-3-Clause OR GPL-2.0)", + "engines": { + "node": ">= 6.13.0" + } + }, + "node_modules/node-gyp": { + "version": "8.4.1", + "dev": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.0", + "glob": "^7.1.4", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^9.1.0", + "nopt": "^5.0.0", + "npmlog": "^6.0.0", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.2", + "which": "^2.0.2" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": ">= 10.12.0" + } + }, + "node_modules/node-gyp-build": { + "version": "4.4.0", + "dev": true, + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/node-releases": { + "version": "2.0.5", + "dev": true, + "license": "MIT" + }, + "node_modules/nopt": { + "version": "5.0.0", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/normalize-package-data": { + "version": "2.5.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "node_modules/normalize-package-data/node_modules/hosted-git-info": { + "version": "2.8.9", + "dev": true, + "license": "ISC" + }, + "node_modules/normalize-package-data/node_modules/semver": { + "version": "5.7.1", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-url": { + "version": "4.5.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm-bundled": { + "version": "1.1.2", + "dev": true, + "license": "ISC", + "dependencies": { + "npm-normalize-package-bin": "^1.0.1" + } + }, + "node_modules/npm-install-checks": { + "version": "4.0.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "semver": "^7.1.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm-normalize-package-bin": { + "version": "1.0.1", + "dev": true, + "license": "ISC" + }, + "node_modules/npm-package-arg": { + "version": "8.1.5", + "dev": true, + "license": "ISC", + "dependencies": { + "hosted-git-info": "^4.0.1", + "semver": "^7.3.4", + "validate-npm-package-name": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm-packlist": { + "version": "3.0.0", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.6", + "ignore-walk": "^4.0.1", + "npm-bundled": "^1.1.1", + "npm-normalize-package-bin": "^1.0.1" + }, + "bin": { + "npm-packlist": "bin/index.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm-pick-manifest": { + "version": "6.1.1", + "dev": true, + "license": "ISC", + "dependencies": { + "npm-install-checks": "^4.0.0", + "npm-normalize-package-bin": "^1.0.1", + "npm-package-arg": "^8.1.2", + "semver": "^7.3.4" + } + }, + "node_modules/npm-registry-fetch": { + "version": "12.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "make-fetch-happen": "^10.0.1", + "minipass": "^3.1.6", + "minipass-fetch": "^1.4.1", + "minipass-json-stream": "^1.0.1", + "minizlib": "^2.1.2", + "npm-package-arg": "^8.1.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16" + } + }, + "node_modules/npm-registry-fetch/node_modules/@npmcli/fs": { + "version": "2.1.0", + "dev": true, + "license": "ISC", + "dependencies": { + "@gar/promisify": "^1.1.3", + "semver": "^7.3.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/npm-registry-fetch/node_modules/@npmcli/move-file": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/npm-registry-fetch/node_modules/@tootallnate/once": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/npm-registry-fetch/node_modules/brace-expansion": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/npm-registry-fetch/node_modules/cacache": { + "version": "16.1.0", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/fs": "^2.1.0", + "@npmcli/move-file": "^2.0.0", + "chownr": "^2.0.0", + "fs-minipass": "^2.1.0", + "glob": "^8.0.1", + "infer-owner": "^1.0.4", + "lru-cache": "^7.7.1", + "minipass": "^3.1.6", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "mkdirp": "^1.0.4", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^3.0.2", + "ssri": "^9.0.0", + "tar": "^6.1.11", + "unique-filename": "^1.1.1" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/npm-registry-fetch/node_modules/glob": { + "version": "8.0.3", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm-registry-fetch/node_modules/http-proxy-agent": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/npm-registry-fetch/node_modules/lru-cache": { + "version": "7.10.1", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/npm-registry-fetch/node_modules/make-fetch-happen": { + "version": "10.1.5", + "dev": true, + "license": "ISC", + "dependencies": { + "agentkeepalive": "^4.2.1", + "cacache": "^16.1.0", + "http-cache-semantics": "^4.1.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^7.7.1", + "minipass": "^3.1.6", + "minipass-collect": "^1.0.2", + "minipass-fetch": "^2.0.3", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.3", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^6.1.1", + "ssri": "^9.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/npm-registry-fetch/node_modules/make-fetch-happen/node_modules/minipass-fetch": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^3.1.6", + "minipass-sized": "^1.0.3", + "minizlib": "^2.1.2" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + }, + "optionalDependencies": { + "encoding": "^0.1.13" + } + }, + "node_modules/npm-registry-fetch/node_modules/minimatch": { + "version": "5.1.0", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm-registry-fetch/node_modules/ssri": { + "version": "9.0.1", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.1.1" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/npm-run-all": { + "version": "4.1.5", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "chalk": "^2.4.1", + "cross-spawn": "^6.0.5", + "memorystream": "^0.3.1", + "minimatch": "^3.0.4", + "pidtree": "^0.3.0", + "read-pkg": "^3.0.0", + "shell-quote": "^1.6.1", + "string.prototype.padend": "^3.0.0" + }, + "bin": { + "npm-run-all": "bin/npm-run-all/index.js", + "run-p": "bin/run-p/index.js", + "run-s": "bin/run-s/index.js" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/npm-run-all/node_modules/cross-spawn": { + "version": "6.0.5", + "dev": true, + "license": "MIT", + "dependencies": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "engines": { + "node": ">=4.8" + } + }, + "node_modules/npm-run-all/node_modules/path-key": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/npm-run-all/node_modules/semver": { + "version": "5.7.1", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/npm-run-all/node_modules/shebang-command": { + "version": "1.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-all/node_modules/shebang-regex": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-all/node_modules/which": { + "version": "1.3.1", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npmlog": { + "version": "6.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "are-we-there-yet": "^3.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^4.0.3", + "set-blocking": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/nx": { + "version": "13.1.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@nrwl/cli": "*" + }, + "bin": { + "nx": "bin/nx.js" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.12.1", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-is": { + "version": "1.1.5", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "has-symbols": "^1.0.1", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.values": { + "version": "1.1.5", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/obuf": { + "version": "1.1.2", + "dev": true, + "license": "MIT" + }, + "node_modules/on-finished": { + "version": "2.4.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open": { + "version": "8.4.0", + "dev": true, + "license": "MIT", + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.1", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.3" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/ora": { + "version": "5.4.1", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/ansi-styles": { + "version": "4.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ora/node_modules/chalk": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/ora/node_modules/color-convert": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/ora/node_modules/color-name": { + "version": "1.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/ora/node_modules/has-flag": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ora/node_modules/supports-color": { + "version": "7.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ospath": { + "version": "1.2.2", + "dev": true, + "license": "MIT" + }, + "node_modules/p-cancelable": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/p-limit": { + "version": "2.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-map": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-retry": { + "version": "4.6.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/retry": "0.12.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-retry/node_modules/retry": { + "version": "0.13.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/package-json": { + "version": "6.5.0", + "dev": true, + "license": "MIT", + "dependencies": { + "got": "^9.6.0", + "registry-auth-token": "^4.0.0", + "registry-url": "^5.0.0", + "semver": "^6.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/package-json/node_modules/semver": { + "version": "6.3.0", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/pacote": { + "version": "12.0.3", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/git": "^2.1.0", + "@npmcli/installed-package-contents": "^1.0.6", + "@npmcli/promise-spawn": "^1.2.0", + "@npmcli/run-script": "^2.0.0", + "cacache": "^15.0.5", + "chownr": "^2.0.0", + "fs-minipass": "^2.1.0", + "infer-owner": "^1.0.4", + "minipass": "^3.1.3", + "mkdirp": "^1.0.3", + "npm-package-arg": "^8.0.1", + "npm-packlist": "^3.0.0", + "npm-pick-manifest": "^6.0.0", + "npm-registry-fetch": "^12.0.0", + "promise-retry": "^2.0.1", + "read-package-json-fast": "^2.0.1", + "rimraf": "^3.0.2", + "ssri": "^8.0.1", + "tar": "^6.1.0" + }, + "bin": { + "pacote": "lib/bin.js" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16" + } + }, + "node_modules/pako": { + "version": "1.0.11", + "dev": true, + "license": "(MIT AND Zlib)" + }, + "node_modules/parent-module": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-node-version": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/parse5": { + "version": "5.1.1", + "license": "MIT", + "optional": true + }, + "node_modules/parse5-html-rewriting-stream": { + "version": "6.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "parse5": "^6.0.1", + "parse5-sax-parser": "^6.0.1" + } + }, + "node_modules/parse5-html-rewriting-stream/node_modules/parse5": { + "version": "6.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "6.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "parse5": "^6.0.1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter/node_modules/parse5": { + "version": "6.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/parse5-sax-parser": { + "version": "6.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "parse5": "^6.0.1" + } + }, + "node_modules/parse5-sax-parser/node_modules/parse5": { + "version": "6.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/parseurl": { + "version": "1.3.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "dev": true, + "license": "MIT" + }, + "node_modules/path-to-regexp": { + "version": "0.1.7", + "dev": true, + "license": "MIT" + }, + "node_modules/path-type": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pause-stream": { + "version": "0.0.11", + "dev": true, + "license": [ + "MIT", + "Apache2" + ], + "dependencies": { + "through": "~2.3" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "dev": true, + "license": "MIT" + }, + "node_modules/performance-now": { + "version": "2.1.0", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.0.0", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pidtree": { + "version": "0.3.1", + "dev": true, + "license": "MIT", + "bin": { + "pidtree": "bin/pidtree.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/piscina": { + "version": "3.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "eventemitter-asyncresource": "^1.0.0", + "hdr-histogram-js": "^2.0.1", + "hdr-histogram-percentiles-obj": "^3.0.0" + }, + "optionalDependencies": { + "nice-napi": "^1.0.2" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/portfinder": { + "version": "1.0.28", + "dev": true, + "license": "MIT", + "dependencies": { + "async": "^2.6.2", + "debug": "^3.1.1", + "mkdirp": "^0.5.5" + }, + "engines": { + "node": ">= 0.12.0" + } + }, + "node_modules/portfinder/node_modules/async": { + "version": "2.6.4", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash": "^4.17.14" + } + }, + "node_modules/portfinder/node_modules/debug": { + "version": "3.2.7", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/portfinder/node_modules/mkdirp": { + "version": "0.5.6", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/postcss": { + "version": "8.4.5", + "dev": true, + "license": "MIT", + "dependencies": { + "nanoid": "^3.1.30", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + } + }, + "node_modules/postcss-attribute-case-insensitive": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.0.2" + }, + "peerDependencies": { + "postcss": "^8.0.2" + } + }, + "node_modules/postcss-color-functional-notation": { + "version": "4.2.3", + "dev": true, + "license": "CC0-1.0", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-color-hex-alpha": { + "version": "8.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-color-rebeccapurple": { + "version": "7.0.2", + "dev": true, + "license": "CC0-1.0", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.3" + } + }, + "node_modules/postcss-custom-media": { + "version": "8.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-custom-properties": { + "version": "12.1.7", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-custom-selectors": { + "version": "6.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.0.4" + }, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "postcss": "^8.1.2" + } + }, + "node_modules/postcss-dir-pseudo-class": { + "version": "6.0.4", + "dev": true, + "license": "CC0-1.0", + "dependencies": { + "postcss-selector-parser": "^6.0.9" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-double-position-gradients": { + "version": "3.1.1", + "dev": true, + "license": "CC0-1.0", + "dependencies": { + "@csstools/postcss-progressive-custom-properties": "^1.1.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-env-function": { + "version": "4.0.6", + "dev": true, + "license": "CC0-1.0", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-focus-visible": { + "version": "6.0.4", + "dev": true, + "license": "CC0-1.0", + "dependencies": { + "postcss-selector-parser": "^6.0.9" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-focus-within": { + "version": "5.0.4", + "dev": true, + "license": "CC0-1.0", + "dependencies": { + "postcss-selector-parser": "^6.0.9" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-font-variant": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-gap-properties": { + "version": "3.0.3", + "dev": true, + "license": "CC0-1.0", + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-image-set-function": { + "version": "4.0.6", + "dev": true, + "license": "CC0-1.0", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-import": { + "version": "14.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-initial": { + "version": "4.0.1", + "dev": true, + "license": "MIT", + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-lab-function": { + "version": "4.2.0", + "dev": true, + "license": "CC0-1.0", + "dependencies": { + "@csstools/postcss-progressive-custom-properties": "^1.1.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-loader": { + "version": "6.2.1", + "dev": true, + "license": "MIT", + "dependencies": { + "cosmiconfig": "^7.0.0", + "klona": "^2.0.5", + "semver": "^7.3.5" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "postcss": "^7.0.0 || ^8.0.1", + "webpack": "^5.0.0" + } + }, + "node_modules/postcss-logical": { + "version": "5.0.4", + "dev": true, + "license": "CC0-1.0", + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-media-minmax": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-extract-imports": { + "version": "3.0.0", + "dev": true, + "license": "ISC", + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-local-by-default": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^6.0.2", + "postcss-value-parser": "^4.1.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-scope": { + "version": "3.0.0", + "dev": true, + "license": "ISC", + "dependencies": { + "postcss-selector-parser": "^6.0.4" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-values": { + "version": "4.0.0", + "dev": true, + "license": "ISC", + "dependencies": { + "icss-utils": "^5.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-nesting": { + "version": "10.1.7", + "dev": true, + "license": "CC0-1.0", + "dependencies": { + "@csstools/selector-specificity": "1.0.0", + "postcss-selector-parser": "^6.0.10" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-overflow-shorthand": { + "version": "3.0.3", + "dev": true, + "license": "CC0-1.0", + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-page-break": { + "version": "3.0.4", + "dev": true, + "license": "MIT", + "peerDependencies": { + "postcss": "^8" + } + }, + "node_modules/postcss-place": { + "version": "7.0.4", + "dev": true, + "license": "CC0-1.0", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-preset-env": { + "version": "7.2.3", + "dev": true, + "license": "CC0-1.0", + "dependencies": { + "autoprefixer": "^10.4.2", + "browserslist": "^4.19.1", + "caniuse-lite": "^1.0.30001299", + "css-blank-pseudo": "^3.0.2", + "css-has-pseudo": "^3.0.3", + "css-prefers-color-scheme": "^6.0.2", + "cssdb": "^5.0.0", + "postcss-attribute-case-insensitive": "^5.0.0", + "postcss-color-functional-notation": "^4.2.1", + "postcss-color-hex-alpha": "^8.0.2", + "postcss-color-rebeccapurple": "^7.0.2", + "postcss-custom-media": "^8.0.0", + "postcss-custom-properties": "^12.1.2", + "postcss-custom-selectors": "^6.0.0", + "postcss-dir-pseudo-class": "^6.0.3", + "postcss-double-position-gradients": "^3.0.4", + "postcss-env-function": "^4.0.4", + "postcss-focus-visible": "^6.0.3", + "postcss-focus-within": "^5.0.3", + "postcss-font-variant": "^5.0.0", + "postcss-gap-properties": "^3.0.2", + "postcss-image-set-function": "^4.0.4", + "postcss-initial": "^4.0.1", + "postcss-lab-function": "^4.0.3", + "postcss-logical": "^5.0.3", + "postcss-media-minmax": "^5.0.0", + "postcss-nesting": "^10.1.2", + "postcss-overflow-shorthand": "^3.0.2", + "postcss-page-break": "^3.0.4", + "postcss-place": "^7.0.3", + "postcss-pseudo-class-any-link": "^7.0.2", + "postcss-replace-overflow-wrap": "^4.0.0", + "postcss-selector-not": "^5.0.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-pseudo-class-any-link": { + "version": "7.1.4", + "dev": true, + "license": "CC0-1.0", + "dependencies": { + "postcss-selector-parser": "^6.0.10" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-replace-overflow-wrap": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "peerDependencies": { + "postcss": "^8.0.3" + } + }, + "node_modules/postcss-selector-not": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.0.10", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-url": { + "version": "10.1.3", + "dev": true, + "license": "MIT", + "dependencies": { + "make-dir": "~3.1.0", + "mime": "~2.5.2", + "minimatch": "~3.0.4", + "xxhashjs": "~0.2.2" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-url/node_modules/mime": { + "version": "2.5.2", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "dev": true, + "license": "MIT" + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prepend-http": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/prettier": { + "version": "2.6.2", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/pretty-bytes": { + "version": "5.6.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/promise-inflight": { + "version": "1.0.1", + "dev": true, + "license": "ISC" + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "dev": true, + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-addr/node_modules/ipaddr.js": { + "version": "1.9.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-from-env": { + "version": "1.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/prr": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/ps-tree": { + "version": "1.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "event-stream": "=3.3.4" + }, + "bin": { + "ps-tree": "bin/ps-tree.js" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/psl": { + "version": "1.8.0", + "dev": true, + "license": "MIT" + }, + "node_modules/pump": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pupa": { + "version": "2.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-goat": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qjobs": { + "version": "1.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.9" + } + }, + "node_modules/qs": { + "version": "6.5.3", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/ramda": { + "version": "0.28.0", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ramda" + } + }, + "node_modules/randombytes": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.1", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "dev": true, + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc/node_modules/ini": { + "version": "1.3.8", + "dev": true, + "license": "ISC" + }, + "node_modules/rc/node_modules/strip-json-comments": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/read-package-json-fast": { + "version": "2.0.3", + "dev": true, + "license": "ISC", + "dependencies": { + "json-parse-even-better-errors": "^2.3.0", + "npm-normalize-package-bin": "^1.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/read-pkg": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "load-json-file": "^4.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/read-pkg/node_modules/path-type": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/read-pkg/node_modules/pify": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/readable-stream": { + "version": "3.6.0", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/reflect-metadata": { + "version": "0.1.13", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/regenerate": { + "version": "1.4.2", + "dev": true, + "license": "MIT" + }, + "node_modules/regenerate-unicode-properties": { + "version": "10.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "regenerate": "^1.4.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.13.9", + "dev": true, + "license": "MIT" + }, + "node_modules/regenerator-transform": { + "version": "0.15.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.8.4" + } + }, + "node_modules/regex-parser": { + "version": "2.2.11", + "dev": true, + "license": "MIT" + }, + "node_modules/regexp.prototype.flags": { + "version": "1.4.3", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "functions-have-names": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexpp": { + "version": "3.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + } + }, + "node_modules/regexpu-core": { + "version": "5.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.0.1", + "regjsgen": "^0.6.0", + "regjsparser": "^0.8.2", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/registry-auth-token": { + "version": "4.2.1", + "dev": true, + "license": "MIT", + "dependencies": { + "rc": "^1.2.8" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/registry-url": { + "version": "5.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "rc": "^1.2.8" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/regjsgen": { + "version": "0.6.0", + "dev": true, + "license": "MIT" + }, + "node_modules/regjsparser": { + "version": "0.8.4", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "jsesc": "~0.5.0" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/regjsparser/node_modules/jsesc": { + "version": "0.5.0", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + } + }, + "node_modules/request-progress": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "throttleit": "^1.0.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/requireindex": { + "version": "1.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.5" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/resolve": { + "version": "1.22.0", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.8.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-url-loader": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "adjust-sourcemap-loader": "^4.0.0", + "convert-source-map": "^1.7.0", + "loader-utils": "^2.0.0", + "postcss": "^8.2.14", + "source-map": "0.6.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/resolve-url-loader/node_modules/loader-utils": { + "version": "2.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/resolve-url-loader/node_modules/source-map": { + "version": "0.6.1", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/responselike": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "lowercase-keys": "^1.0.0" + } + }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rfdc": { + "version": "1.3.0", + "dev": true, + "license": "MIT" + }, + "node_modules/rimraf": { + "version": "3.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rollup": { + "version": "2.74.1", + "dev": true, + "license": "MIT", + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=10.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/rollup-plugin-sourcemaps": { + "version": "0.6.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^3.0.9", + "source-map-resolve": "^0.6.0" + }, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "@types/node": ">=10.0.0", + "rollup": ">=0.31.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/run-async": { + "version": "2.4.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/rw": { + "version": "1.3.3", + "license": "BSD-3-Clause" + }, + "node_modules/rxjs": { + "version": "7.5.5", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/rxjs-for-await": { + "version": "0.0.2", + "dev": true, + "license": "MIT", + "peerDependencies": { + "rxjs": "^6.0.0" + } + }, + "node_modules/rxjs-marbles": { + "version": "7.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-equals": "^2.0.0", + "rxjs-report-usage": "^1.0.4" + }, + "peerDependencies": { + "rxjs": "^7.0.0" + } + }, + "node_modules/rxjs-report-usage": { + "version": "1.0.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.10.3", + "@babel/traverse": "^7.10.3", + "@babel/types": "^7.10.3", + "bent": "~7.3.6", + "chalk": "~4.1.0", + "glob": "~7.2.0", + "prompts": "~2.4.2" + }, + "bin": { + "rxjs-report-usage": "bin/rxjs-report-usage" + } + }, + "node_modules/rxjs-report-usage/node_modules/ansi-styles": { + "version": "4.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/rxjs-report-usage/node_modules/chalk": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/rxjs-report-usage/node_modules/color-convert": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/rxjs-report-usage/node_modules/color-name": { + "version": "1.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/rxjs-report-usage/node_modules/has-flag": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/rxjs-report-usage/node_modules/supports-color": { + "version": "7.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "dev": true, + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "license": "MIT" + }, + "node_modules/sass": { + "version": "1.49.9", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": ">=3.0.0 <4.0.0", + "immutable": "^4.0.0", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/sass-loader": { + "version": "12.4.0", + "dev": true, + "license": "MIT", + "dependencies": { + "klona": "^2.0.4", + "neo-async": "^2.6.2" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "fibers": ">= 3.1.0", + "node-sass": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0", + "sass": "^1.3.0", + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "fibers": { + "optional": true + }, + "node-sass": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/sax": { + "version": "1.2.4", + "dev": true, + "license": "ISC" + }, + "node_modules/schema-utils": { + "version": "2.7.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.5", + "ajv": "^6.12.4", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 8.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/schema-utils/node_modules/ajv": { + "version": "6.12.6", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/schema-utils/node_modules/json-schema-traverse": { + "version": "0.4.1", + "dev": true, + "license": "MIT" + }, + "node_modules/select-hose": { + "version": "2.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/selfsigned": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "node-forge": "^1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver": { + "version": "7.3.5", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver-diff": { + "version": "3.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/semver-diff/node_modules/semver": { + "version": "6.3.0", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/send": { + "version": "0.18.0", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/send/node_modules/mime": { + "version": "1.6.0", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "dev": true, + "license": "MIT" + }, + "node_modules/send/node_modules/statuses": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.0", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/serve-index": { + "version": "1.9.1", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "~1.3.4", + "batch": "0.6.1", + "debug": "2.6.9", + "escape-html": "~1.0.3", + "http-errors": "~1.6.2", + "mime-types": "~2.1.17", + "parseurl": "~1.3.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/serve-index/node_modules/debug": { + "version": "2.6.9", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/serve-index/node_modules/depd": { + "version": "1.1.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/http-errors": { + "version": "1.6.3", + "dev": true, + "license": "MIT", + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/inherits": { + "version": "2.0.3", + "dev": true, + "license": "ISC" + }, + "node_modules/serve-index/node_modules/ms": { + "version": "2.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/serve-index/node_modules/setprototypeof": { + "version": "1.1.0", + "dev": true, + "license": "ISC" + }, + "node_modules/serve-static": { + "version": "1.15.0", + "dev": true, + "license": "MIT", + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "dev": true, + "license": "ISC" + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "dev": true, + "license": "ISC" + }, + "node_modules/shallow-clone": { + "version": "3.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/shell-quote": { + "version": "1.7.3", + "dev": true, + "license": "MIT" + }, + "node_modules/side-channel": { + "version": "1.0.4", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "dev": true, + "license": "ISC" + }, + "node_modules/simple-bin-help": { + "version": "1.7.7", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "3.2.7", + "update-notifier": "5.1.0", + "word-wrap": "1.2.3" + } + }, + "node_modules/simple-bin-help/node_modules/debug": { + "version": "3.2.7", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "dev": true, + "license": "MIT" + }, + "node_modules/slash": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/slice-ansi": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/slice-ansi/node_modules/ansi-styles": { + "version": "4.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/color-convert": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/slice-ansi/node_modules/color-name": { + "version": "1.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socket.io": { + "version": "4.5.1", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "debug": "~4.3.2", + "engine.io": "~6.2.0", + "socket.io-adapter": "~2.4.0", + "socket.io-parser": "~4.0.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.4.0", + "dev": true, + "license": "MIT" + }, + "node_modules/socket.io-parser": { + "version": "4.0.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/component-emitter": "^1.2.10", + "component-emitter": "~1.3.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/sockjs": { + "version": "0.3.24", + "dev": true, + "license": "MIT", + "dependencies": { + "faye-websocket": "^0.11.3", + "uuid": "^8.3.2", + "websocket-driver": "^0.7.4" + } + }, + "node_modules/socks": { + "version": "2.6.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ip": "^1.1.5", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.13.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "6.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^6.0.2", + "debug": "^4.3.3", + "socks": "^2.6.2" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/source-map": { + "version": "0.7.3", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 8" + } + }, + "node_modules/source-map-js": { + "version": "1.0.2", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-loader": { + "version": "3.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "abab": "^2.0.5", + "iconv-lite": "^0.6.3", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/source-map-loader/node_modules/iconv-lite": { + "version": "0.6.3", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-resolve": { + "version": "0.6.0", + "dev": true, + "license": "MIT", + "dependencies": { + "atob": "^2.1.2", + "decode-uri-component": "^0.2.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sourcemap-codec": { + "version": "1.4.8", + "dev": true, + "license": "MIT" + }, + "node_modules/spdx-correct": { + "version": "3.1.1", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.3.0", + "dev": true, + "license": "CC-BY-3.0" + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.11", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/spdy": { + "version": "4.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.0", + "handle-thing": "^2.0.0", + "http-deceiver": "^1.2.7", + "select-hose": "^2.0.0", + "spdy-transport": "^3.0.0" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/spdy-transport": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.0", + "detect-node": "^2.0.4", + "hpack.js": "^2.1.6", + "obuf": "^1.1.2", + "readable-stream": "^3.0.6", + "wbuf": "^1.7.3" + } + }, + "node_modules/split": { + "version": "0.3.3", + "dev": true, + "license": "MIT", + "dependencies": { + "through": "2" + }, + "engines": { + "node": "*" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/sshpk": { + "version": "1.17.0", + "dev": true, + "license": "MIT", + "dependencies": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + }, + "bin": { + "sshpk-conv": "bin/sshpk-conv", + "sshpk-sign": "bin/sshpk-sign", + "sshpk-verify": "bin/sshpk-verify" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ssri": { + "version": "8.0.1", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.1.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/start-server-and-test": { + "version": "1.14.0", + "dev": true, + "license": "MIT", + "dependencies": { + "bluebird": "3.7.2", + "check-more-types": "2.24.0", + "debug": "4.3.2", + "execa": "5.1.1", + "lazy-ass": "1.6.0", + "ps-tree": "1.2.0", + "wait-on": "6.0.0" + }, + "bin": { + "server-test": "src/bin/start.js", + "start-server-and-test": "src/bin/start.js", + "start-test": "src/bin/start.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/start-server-and-test/node_modules/debug": { + "version": "4.3.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/start-server-and-test/node_modules/execa": { + "version": "5.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/start-server-and-test/node_modules/get-stream": { + "version": "6.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/start-server-and-test/node_modules/human-signals": { + "version": "2.1.0", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/statuses": { + "version": "1.5.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/stream-combiner": { + "version": "0.0.4", + "dev": true, + "license": "MIT", + "dependencies": { + "duplexer": "~0.1.1" + } + }, + "node_modules/streamroller": { + "version": "3.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "date-format": "^4.0.10", + "debug": "^4.3.4", + "fs-extra": "^10.1.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/streamroller/node_modules/debug": { + "version": "4.3.4", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/streamroller/node_modules/fs-extra": { + "version": "10.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.2.1", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/string-width": { + "version": "4.2.3", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string.prototype.padend": { + "version": "3.1.3", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.5", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.19.5" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.5", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.19.5" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/stylus": { + "version": "0.56.0", + "dev": true, + "license": "MIT", + "dependencies": { + "css": "^3.0.0", + "debug": "^4.3.2", + "glob": "^7.1.6", + "safer-buffer": "^2.1.2", + "sax": "~1.2.4", + "source-map": "^0.7.3" + }, + "bin": { + "stylus": "bin/stylus" + }, + "engines": { + "node": "*" + } + }, + "node_modules/stylus-loader": { + "version": "6.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-glob": "^3.2.7", + "klona": "^2.0.4", + "normalize-path": "^3.0.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "stylus": ">=0.52.4", + "webpack": "^5.0.0" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/symbol-observable": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/tapable": { + "version": "2.2.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/tar": { + "version": "6.1.11", + "dev": true, + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^3.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/terser": { + "version": "5.11.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.5.0", + "commander": "^2.20.0", + "source-map": "~0.7.2", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.3.1", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-worker": "^27.4.5", + "schema-utils": "^3.1.1", + "serialize-javascript": "^6.0.0", + "source-map": "^0.6.1", + "terser": "^5.7.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/terser-webpack-plugin/node_modules/ajv": { + "version": "6.12.6", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/terser-webpack-plugin/node_modules/json-schema-traverse": { + "version": "0.4.1", + "dev": true, + "license": "MIT" + }, + "node_modules/terser-webpack-plugin/node_modules/schema-utils": { + "version": "3.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/terser-webpack-plugin/node_modules/source-map": { + "version": "0.6.1", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "dev": true, + "license": "MIT" + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "dev": true, + "license": "MIT" + }, + "node_modules/throttleit": { + "version": "1.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/through": { + "version": "2.3.8", + "dev": true, + "license": "MIT" + }, + "node_modules/thunky": { + "version": "1.1.0", + "dev": true, + "license": "MIT" + }, + "node_modules/tmp": { + "version": "0.2.1", + "dev": true, + "license": "MIT", + "dependencies": { + "rimraf": "^3.0.0" + }, + "engines": { + "node": ">=8.17.0" + } + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/to-readable-stream": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tough-cookie": { + "version": "2.5.0", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/transformation-matrix": { + "version": "1.15.3", + "license": "MIT" + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/tsconfig-paths": { + "version": "3.14.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.1", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/tsconfig-paths/node_modules/json5": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/tslib": { + "version": "2.3.1", + "license": "0BSD" + }, + "node_modules/tsutils": { + "version": "3.21.0", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^1.8.1" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" + } + }, + "node_modules/tsutils-etc": { + "version": "1.4.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs": "^17.0.0", + "yargs": "^17.0.0" + }, + "bin": { + "ts-flags": "bin/ts-flags", + "ts-kind": "bin/ts-kind" + }, + "peerDependencies": { + "tsutils": "^3.0.0", + "typescript": "^4.0.0" + } + }, + "node_modules/tsutils/node_modules/tslib": { + "version": "1.14.1", + "dev": true, + "license": "0BSD" + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/tv4": { + "version": "1.3.0", + "dev": true, + "license": [ + { + "type": "Public Domain", + "url": "http://geraintluff.github.io/tv4/LICENSE.txt" + }, + { + "type": "MIT", + "url": "http://jsonary.com/LICENSE.txt" + } + ], + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/tweetnacl": { + "version": "0.14.5", + "dev": true, + "license": "Unlicense" + }, + "node_modules/type-check": { + "version": "0.4.0", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "dev": true, + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typed-assert": { + "version": "1.0.9", + "dev": true, + "license": "MIT" + }, + "node_modules/typedarray-to-buffer": { + "version": "3.1.5", + "dev": true, + "license": "MIT", + "dependencies": { + "is-typedarray": "^1.0.0" + } + }, + "node_modules/typescript": { + "version": "4.6.4", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/ua-parser-js": { + "version": "0.7.31", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + } + ], + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/unbox-primitive": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", + "which-boxed-primitive": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/unicode-canonical-property-names-ecmascript": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-ecmascript": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-value-ecmascript": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-property-aliases-ecmascript": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unique-filename": { + "version": "1.1.1", + "dev": true, + "license": "ISC", + "dependencies": { + "unique-slug": "^2.0.0" + } + }, + "node_modules/unique-slug": { + "version": "2.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4" + } + }, + "node_modules/unique-string": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "crypto-random-string": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/universalify": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/untildify": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/update-notifier": { + "version": "5.1.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boxen": "^5.0.0", + "chalk": "^4.1.0", + "configstore": "^5.0.1", + "has-yarn": "^2.1.0", + "import-lazy": "^2.1.0", + "is-ci": "^2.0.0", + "is-installed-globally": "^0.4.0", + "is-npm": "^5.0.0", + "is-yarn-global": "^0.3.0", + "latest-version": "^5.1.0", + "pupa": "^2.1.1", + "semver": "^7.3.4", + "semver-diff": "^3.1.1", + "xdg-basedir": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/yeoman/update-notifier?sponsor=1" + } + }, + "node_modules/update-notifier/node_modules/ansi-styles": { + "version": "4.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/update-notifier/node_modules/chalk": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/update-notifier/node_modules/ci-info": { + "version": "2.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/update-notifier/node_modules/color-convert": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/update-notifier/node_modules/color-name": { + "version": "1.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/update-notifier/node_modules/has-flag": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/update-notifier/node_modules/is-ci": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ci-info": "^2.0.0" + }, + "bin": { + "is-ci": "bin.js" + } + }, + "node_modules/update-notifier/node_modules/supports-color": { + "version": "7.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/url-parse-lax": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "prepend-http": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "dev": true, + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/v8-compile-cache": { + "version": "2.3.0", + "dev": true, + "license": "MIT" + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/validate-npm-package-name": { + "version": "3.0.0", + "dev": true, + "license": "ISC", + "dependencies": { + "builtins": "^1.0.3" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/verror": { + "version": "1.10.0", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "node_modules/void-elements": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wait-on": { + "version": "6.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "axios": "^0.21.1", + "joi": "^17.4.0", + "lodash": "^4.17.21", + "minimist": "^1.2.5", + "rxjs": "^7.1.0" + }, + "bin": { + "wait-on": "bin/wait-on" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/watchpack": { + "version": "2.3.1", + "dev": true, + "license": "MIT", + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/wbuf": { + "version": "1.7.3", + "dev": true, + "license": "MIT", + "dependencies": { + "minimalistic-assert": "^1.0.0" + } + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/webcola": { + "version": "3.4.0", + "license": "MIT", + "dependencies": { + "d3-dispatch": "^1.0.3", + "d3-drag": "^1.0.4", + "d3-shape": "^1.3.5", + "d3-timer": "^1.0.5" + } + }, + "node_modules/webpack": { + "version": "5.70.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint-scope": "^3.7.3", + "@types/estree": "^0.0.51", + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/wasm-edit": "1.11.1", + "@webassemblyjs/wasm-parser": "1.11.1", + "acorn": "^8.4.1", + "acorn-import-assertions": "^1.7.6", + "browserslist": "^4.14.5", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.9.2", + "es-module-lexer": "^0.9.0", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.9", + "json-parse-better-errors": "^1.0.2", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^3.1.0", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.1.3", + "watchpack": "^2.3.1", + "webpack-sources": "^3.2.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-dev-middleware": { + "version": "5.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "colorette": "^2.0.10", + "memfs": "^3.2.2", + "mime-types": "^2.1.31", + "range-parser": "^1.2.1", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/webpack-dev-middleware/node_modules/ajv-keywords": { + "version": "5.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/webpack-dev-middleware/node_modules/schema-utils": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.8.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.0.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/webpack-dev-server": { + "version": "4.7.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/bonjour": "^3.5.9", + "@types/connect-history-api-fallback": "^1.3.5", + "@types/serve-index": "^1.9.1", + "@types/sockjs": "^0.3.33", + "@types/ws": "^8.2.2", + "ansi-html-community": "^0.0.8", + "bonjour": "^3.5.0", + "chokidar": "^3.5.2", + "colorette": "^2.0.10", + "compression": "^1.7.4", + "connect-history-api-fallback": "^1.6.0", + "default-gateway": "^6.0.3", + "del": "^6.0.0", + "express": "^4.17.1", + "graceful-fs": "^4.2.6", + "html-entities": "^2.3.2", + "http-proxy-middleware": "^2.0.0", + "ipaddr.js": "^2.0.1", + "open": "^8.0.9", + "p-retry": "^4.5.0", + "portfinder": "^1.0.28", + "schema-utils": "^4.0.0", + "selfsigned": "^2.0.0", + "serve-index": "^1.9.1", + "sockjs": "^0.3.21", + "spdy": "^4.0.2", + "strip-ansi": "^7.0.0", + "webpack-dev-middleware": "^5.3.0", + "ws": "^8.1.0" + }, + "bin": { + "webpack-dev-server": "bin/webpack-dev-server.js" + }, + "engines": { + "node": ">= 12.13.0" + }, + "peerDependencies": { + "webpack": "^4.37.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-dev-server/node_modules/ajv-keywords": { + "version": "5.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/webpack-dev-server/node_modules/ansi-regex": { + "version": "6.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/webpack-dev-server/node_modules/schema-utils": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.8.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.0.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/webpack-dev-server/node_modules/strip-ansi": { + "version": "7.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/webpack-dev-server/node_modules/ws": { + "version": "8.7.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/webpack-merge": { + "version": "5.8.0", + "dev": true, + "license": "MIT", + "dependencies": { + "clone-deep": "^4.0.1", + "wildcard": "^2.0.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/webpack-sources": { + "version": "3.2.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack-subresource-integrity": { + "version": "5.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "typed-assert": "^1.0.8" + }, + "engines": { + "node": ">= 12" + }, + "peerDependencies": { + "html-webpack-plugin": ">= 5.0.0-beta.1 < 6", + "webpack": "^5.12.0" + }, + "peerDependenciesMeta": { + "html-webpack-plugin": { + "optional": true + } + } + }, + "node_modules/webpack/node_modules/@types/estree": { + "version": "0.0.51", + "dev": true, + "license": "MIT" + }, + "node_modules/webpack/node_modules/ajv": { + "version": "6.12.6", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/webpack/node_modules/json-schema-traverse": { + "version": "0.4.1", + "dev": true, + "license": "MIT" + }, + "node_modules/webpack/node_modules/schema-utils": { + "version": "3.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/websocket-driver": { + "version": "0.7.4", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/websocket-extensions": { + "version": "0.1.4", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/wide-align": { + "version": "1.1.5", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/widest-line": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "string-width": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wildcard": { + "version": "2.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/word-wrap": { + "version": "1.2.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "4.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/color-convert": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/wrap-ansi/node_modules/color-name": { + "version": "1.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/wrappy": { + "version": "1.0.2", + "dev": true, + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "3.0.3", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" + } + }, + "node_modules/ws": { + "version": "7.5.8", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xdg-basedir": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/xmlbuilder": { + "version": "12.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0" + } + }, + "node_modules/xxhashjs": { + "version": "0.2.2", + "dev": true, + "license": "MIT", + "dependencies": { + "cuint": "^0.2.2" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "dev": true, + "license": "ISC" + }, + "node_modules/yaml": { + "version": "1.10.2", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, + "node_modules/yargs": { + "version": "17.5.1", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "20.0.0", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs/node_modules/yargs-parser": { + "version": "21.0.1", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "node_modules/zone.js": { + "version": "0.11.7", + "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.11.7.tgz", + "integrity": "sha512-e39K2EdK5JfA3FDuUTVRvPlYV4aBfnOOcGuILhQAT7nzeV12uSrLBzImUM9CDVoncDSX4brR/gwqu0heQ3BQ0g==", + "dependencies": { + "tslib": "^2.3.0" + } + } + } +} diff --git a/projects/frontend/shared-components/gui/package.json b/projects/frontend/shared-components/gui/package.json new file mode 100644 index 0000000000..e605fa413f --- /dev/null +++ b/projects/frontend/shared-components/gui/package.json @@ -0,0 +1,106 @@ +{ + "name": "shared-wrapper", + "version": "0.0.1", + "engines": { + "node": "^14.15.0 || >=16.10.0", + "npm": ">=8.5.5" + }, + "private": true, + "scripts": { + "ng": "node --max_old_space_size=2048 ./node_modules/@angular/cli/bin/ng", + "start": "npm run ng -- serve", + "build": "npm run ng -- build", + "build:dev": "npm run ng -- build --configuration=development", + "build:watch": "npm run build -- --watch", + "build:documentation": "npm run build -- documentation-ui --configuration=production", + "test": "npm run ng -- test --watch=false", + "test:headless": "npm run test -- --browsers=ChromeHeadless_No_Sandbox", + "lint": "npm run ng -- lint", + "prettier-format": "prettier --config .prettierrc.json 'projects/**/src/**/*.ts' --write", + "clean:dist": "rimraf dist", + "clean:reports": "rimraf reports", + "clean": "npm-run-all --parallel clean:*", + "setup": "npm ci --omit=optional" + }, + "dependencies": { + "@angular/animations": "13.3.10", + "@angular/cdk": "13.3.8", + "@angular/common": "13.3.10", + "@angular/compiler": "13.3.10", + "@angular/core": "13.3.10", + "@angular/forms": "13.3.10", + "@angular/platform-browser": "13.3.10", + "@angular/platform-browser-dynamic": "13.3.10", + "@angular/router": "13.3.10", + "@cds/core": "5.7.2", + "@clr/angular": "13.3.1", + "@clr/icons": "13.0.2", + "@clr/ui": "13.3.1", + "@ngrx/effects": "13.2.0", + "@ngrx/router-store": "13.2.0", + "@ngrx/store": "13.2.0", + "@swimlane/ngx-graph": "8.0.0", + "@webcomponents/custom-elements": "1.5.0", + "angular-ng-autocomplete": "2.0.8", + "d3": "5.16.0", + "dompurify": "2.3.8", + "lodash": "4.17.21", + "ngx-clipboard": "15.1.0", + "ngx-cookie-service": "13.2.1", + "rxjs": "7.5.5", + "tslib": "2.3.1", + "zone.js": "0.11.7" + }, + "optionalDependencies": { + "@vdk/shared": "0.0.0" + }, + "devDependencies": { + "@angular-devkit/build-angular": "13.3.7", + "@angular-devkit/schematics": "13.3.7", + "@angular-eslint/builder": "13.2.1", + "@angular-eslint/eslint-plugin": "13.2.1", + "@angular-eslint/eslint-plugin-template": "13.2.1", + "@angular-eslint/schematics": "13.2.1", + "@angular-eslint/template-parser": "13.2.1", + "@angular/cli": "13.3.7", + "@angular/compiler-cli": "13.3.10", + "@neuralegion/cypress-har-generator": "5.3.0", + "@ngrx/schematics": "13.2.0", + "@ngrx/store-devtools": "13.2.0", + "@types/jasmine": "4.0.3", + "@types/lodash": "4.14.182", + "@types/node": "17.0.35", + "@typescript-eslint/eslint-plugin": "5.26.0", + "@typescript-eslint/parser": "5.26.0", + "angular-oauth2-oidc": "13.0.1", + "cypress": "9.7.0", + "cypress-grep": "2.13.1", + "cypress-localstorage-commands": "1.7.0", + "cypress-terminal-report": "3.5.2", + "eslint": "8.16.0", + "eslint-import-resolver-typescript": "2.7.1", + "eslint-plugin-cypress": "2.12.1", + "eslint-plugin-import": "2.26.0", + "eslint-plugin-jasmine": "4.1.3", + "eslint-plugin-jsdoc": "39.3.2", + "eslint-plugin-ngrx": "2.1.2", + "eslint-plugin-prefer-arrow": "1.2.3", + "eslint-plugin-prettier": "4.0.0", + "eslint-plugin-rxjs": "5.0.2", + "jasmine-core": "4.1.1", + "jasmine-spec-reporter": "7.0.0", + "karma": "6.3.20", + "karma-chrome-launcher": "3.1.1", + "karma-coverage": "2.2.0", + "karma-jasmine": "5.0.1", + "karma-jasmine-html-reporter": "1.7.0", + "karma-junit-reporter": "2.0.1", + "ng-packagr": "13.3.1", + "npm-run-all": "4.1.5", + "prettier": "2.6.2", + "rimraf": "3.0.2", + "rxjs-marbles": "7.0.1", + "start-server-and-test": "1.14.0", + "typescript": "4.6.4" + } +} diff --git a/projects/frontend/shared-components/gui/projects/documentation-ui/.browserslistrc b/projects/frontend/shared-components/gui/projects/documentation-ui/.browserslistrc new file mode 100644 index 0000000000..427441dc93 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/documentation-ui/.browserslistrc @@ -0,0 +1,17 @@ +# This file is used by the build system to adjust CSS and JS output to support the specified browsers below. +# For additional information regarding the format and rule options, please see: +# https://github.com/browserslist/browserslist#queries + +# For the full list of supported browsers by the Angular framework, please see: +# https://angular.io/guide/browser-support + +# You can see what browsers were selected by your queries by running: +# npx browserslist + +last 1 Chrome version +last 1 Firefox version +last 2 Edge major versions +last 2 Safari major versions +last 2 iOS major versions +Firefox ESR +not IE 11 # Angular supports IE 11 only as an opt-in. To opt-in, remove the 'not' prefix on this line. diff --git a/projects/frontend/shared-components/gui/projects/documentation-ui/.eslintrc.json b/projects/frontend/shared-components/gui/projects/documentation-ui/.eslintrc.json new file mode 100644 index 0000000000..6df02389b9 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/documentation-ui/.eslintrc.json @@ -0,0 +1,87 @@ +{ + "extends": "../../.eslintrc.json", + "ignorePatterns": [ + "!**/*" + ], + "overrides": [ + { + "files": [ + "*.ts" + ], + "parserOptions": { + "project": [ + "projects/documentation-ui/tsconfig.app.json" + ], + "createDefaultProgram": true + }, + "rules": { + "@angular-eslint/component-selector": [ + "error", + { + "type": "element", + "prefix": "app", + "style": "kebab-case" + } + ], + "@angular-eslint/directive-selector": [ + "error", + { + "type": "attribute", + "prefix": "app", + "style": "camelCase" + } + ], + "@typescript-eslint/naming-convention": [ + "warn", + { + "selector": "property", + "format": [ + "camelCase" + ], + "leadingUnderscore": "allow", + "filter": { + "regex": "^((\\$\\..+))$", + "match": false + } + }, + { + "selector": "classProperty", + "modifiers": [ + "static" + ], + "format": [ + "camelCase", + "UPPER_CASE" + ], + "leadingUnderscore": "allow" + } + ], + "@typescript-eslint/no-shadow": "error", + "no-shadow": "off" + } + }, + { + "files": [ + "*.spec.ts" + ], + "parserOptions": { + "project": [ + "projects/shared/tsconfig.spec.json" + ] + }, + "rules": { + "@typescript-eslint/ban-ts-comment": "off", + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-floating-promises": "off", + "@typescript-eslint/no-unsafe-assignment": "off", + "@typescript-eslint/unbound-method": "off" + } + }, + { + "files": [ + "*.html" + ], + "rules": {} + } + ] +} diff --git a/projects/frontend/shared-components/gui/projects/documentation-ui/karma.conf.js b/projects/frontend/shared-components/gui/projects/documentation-ui/karma.conf.js new file mode 100644 index 0000000000..7b134b5021 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/documentation-ui/karma.conf.js @@ -0,0 +1,68 @@ +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function(config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-junit-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + coverageReporter: { + dir: require('path').join(__dirname, '../../reports/coverage/documentation-ui'), + subdir: '.', + reporters: [ + //Code coverage - output only in HTML file + { type: 'html' }, + { type: 'text-summary' }, + { type: 'lcovonly' } + ], + check: { + global: { + lines: 20 + } + } + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + junitReporter: { + outputDir: require('path').join(__dirname, '../../reports/test-results/documentation-ui'), + outputFile: 'unit-tests.xml', + useBrowserName: false + }, + reporters: ['progress', 'junit', 'coverage'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['ChromeHeadless'], + customLaunchers: { + ChromeHeadless_No_Sandbox: { + base: 'ChromeHeadless', + flags: ['--no-sandbox'] + } + }, + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/projects/frontend/shared-components/gui/projects/documentation-ui/src/app/app-routing.module.ts b/projects/frontend/shared-components/gui/projects/documentation-ui/src/app/app-routing.module.ts new file mode 100644 index 0000000000..8b2fe0e3e3 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/documentation-ui/src/app/app-routing.module.ts @@ -0,0 +1,28 @@ +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; + +const routes: Routes = [ + { + path: '', + redirectTo: 'overview', + pathMatch: 'full' + }, + // Overview + { + path: 'overview', + loadChildren: () => import('./core/core.module').then((m) => m.CoreModule) + }, + { path: '**', redirectTo: 'overview' } +]; + +@NgModule({ + imports: [RouterModule.forRoot(routes)], + exports: [RouterModule] +}) +export class AppRoutingModule { +} diff --git a/projects/frontend/shared-components/gui/projects/documentation-ui/src/app/app.component.html b/projects/frontend/shared-components/gui/projects/documentation-ui/src/app/app.component.html new file mode 100644 index 0000000000..37b1d7f209 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/documentation-ui/src/app/app.component.html @@ -0,0 +1,61 @@ + + +
+ +
+ +
+ + + +
Logout
+
+
+
+
+ +
+ + + + Overview + +
+ Components +
+
+ +
+ + +
+
+ +
+
+
+ +
+
+
+

Loading Shared Components Documentation UIs

+ +
+
+ + diff --git a/projects/frontend/shared-components/gui/projects/documentation-ui/src/app/app.component.scss b/projects/frontend/shared-components/gui/projects/documentation-ui/src/app/app.component.scss new file mode 100644 index 0000000000..1afbdcdb34 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/documentation-ui/src/app/app.component.scss @@ -0,0 +1,39 @@ +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +.main-container { + .content-container { + .content-area { + .csp-content-area-margin { + margin: 0 30px; + padding-bottom: 30px; + height: 100%; + } + } + } +} + +::ng-deep .table-documentation { + &.table th, + &.table td { + text-align: left; + } +} + +.checking-user { + display: table; + height: 26.5rem; + width: 100%; + + .loading-spinner { + text-align: center; + display: table-cell; + vertical-align: middle; + + .loading-title { + margin-bottom: .6rem; + } + } +} diff --git a/projects/frontend/shared-components/gui/projects/documentation-ui/src/app/app.component.spec.ts b/projects/frontend/shared-components/gui/projects/documentation-ui/src/app/app.component.spec.ts new file mode 100644 index 0000000000..bd83efc951 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/documentation-ui/src/app/app.component.spec.ts @@ -0,0 +1,32 @@ +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { AppComponent } from './app.component'; +import { OAuthModule } from 'angular-oauth2-oidc'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; + +describe('AppComponent', () => { + let component: AppComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [AppComponent], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + imports: [OAuthModule.forRoot(), HttpClientTestingModule] + }) + .compileComponents() + .then(() => { + fixture = TestBed.createComponent(AppComponent); + component = fixture.debugElement.componentInstance; + }); + }); + + it('should create the app', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/projects/frontend/shared-components/gui/projects/documentation-ui/src/app/app.component.ts b/projects/frontend/shared-components/gui/projects/documentation-ui/src/app/app.component.ts new file mode 100644 index 0000000000..7e855baa9f --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/documentation-ui/src/app/app.component.ts @@ -0,0 +1,37 @@ +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Component } from '@angular/core'; +import { OAuthService } from 'angular-oauth2-oidc'; +import { authCodeFlowConfig } from './auth'; + +@Component({ + selector: 'app-root', + templateUrl: './app.component.html', + styleUrls: ['./app.component.scss'] +}) +export class AppComponent { + constructor(private oauthService: OAuthService) { + this.oauthService.configure(authCodeFlowConfig); + // eslint-disable-next-line @typescript-eslint/no-floating-promises + this.oauthService.loadDiscoveryDocumentAndLogin(); + } + + logout(): void { + this.oauthService.logOut(); + } + + get idToken(): string { + return this.oauthService.getIdToken(); + } + + get userName(): string { + return (this.oauthService.getIdentityClaims()) ? this.getIdentityClaim('username') : 'N/A'; + } + + private getIdentityClaim(userNamePropName: string): string { + return this.oauthService.getIdentityClaims()[userNamePropName] as string; + } +} diff --git a/projects/frontend/shared-components/gui/projects/documentation-ui/src/app/app.module.ts b/projects/frontend/shared-components/gui/projects/documentation-ui/src/app/app.module.ts new file mode 100644 index 0000000000..4a7e36c41d --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/documentation-ui/src/app/app.module.ts @@ -0,0 +1,64 @@ +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { NgModule } from '@angular/core'; +import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http'; +import { BrowserModule } from '@angular/platform-browser'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; + +import { AuthConfig, OAuthModule, OAuthStorage } from 'angular-oauth2-oidc'; + +import { ClarityModule } from '@clr/angular'; + +import { VdkComponentsModule } from '@vdk/shared'; + +import { TaurusSharedCoreModule, TaurusSharedFeaturesModule, TaurusSharedNgRxModule } from '@vdk/shared'; + +import { AppRoutingModule } from './app-routing.module'; +import { AppComponent } from './app.component'; +import { AuthorizationInterceptor } from './http.interceptor'; +import { authCodeFlowConfig } from './auth'; + +@NgModule({ + imports: [ + AppRoutingModule, + BrowserModule, + ClarityModule, + BrowserAnimationsModule, + TaurusSharedCoreModule.forRoot(), + TaurusSharedFeaturesModule.forRoot(), + TaurusSharedNgRxModule.forRootWithDevtools(), + VdkComponentsModule.forRoot(), + HttpClientModule, + OAuthModule.forRoot({ + resourceServer: { + allowedUrls: [ + authCodeFlowConfig.issuer, + '/metadata' + ], + sendAccessToken: true + } + }) + ], + declarations: [AppComponent], + providers: [ + { + provide: OAuthStorage, + useValue: localStorage + }, + { + provide: AuthConfig, + useValue: authCodeFlowConfig + }, + { + provide: HTTP_INTERCEPTORS, + useClass: AuthorizationInterceptor, + multi: true + } + ], + bootstrap: [AppComponent] +}) +export class AppModule { +} diff --git a/projects/frontend/shared-components/gui/projects/documentation-ui/src/app/auth.ts b/projects/frontend/shared-components/gui/projects/documentation-ui/src/app/auth.ts new file mode 100644 index 0000000000..48fec45ef1 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/documentation-ui/src/app/auth.ts @@ -0,0 +1,26 @@ +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { AuthConfig } from 'angular-oauth2-oidc'; + +export const authCodeFlowConfig: AuthConfig = { + issuer: 'https://console-stg.cloud.vmware.com/csp/gateway/am/api/', + redirectUri: window.location.origin + '/index.html', + skipIssuerCheck: true, + requestAccessToken: true, + oidc: true, + strictDiscoveryDocumentValidation: false, + clientId: 'Lt44bhN5yMowdEHuxO3v1SBDKsS3aXW4GcJ', + responseType: 'code', + scope: 'openid ALL_PERMISSIONS customer_number group_names', + showDebugInformation: true, + silentRefreshRedirectUri: window.location.origin + '/silent-refresh.html', + useSilentRefresh: true, // Needed for Code Flow to suggest using iframe-based refreshes + silentRefreshTimeout: 5000, // For faster testing + timeoutFactor: 0.25, // For faster testing + sessionChecksEnabled: true, + clearHashAfterLogin: false, // https://github.com/manfredsteyer/angular-oauth2-oidc/issues/457#issuecomment-431807040, + nonceStateSeparator: 'semicolon' // Real semicolon gets mangled by IdentityServer's URI encoding +}; diff --git a/projects/frontend/shared-components/gui/projects/documentation-ui/src/app/core/core.component.html b/projects/frontend/shared-components/gui/projects/documentation-ui/src/app/core/core.component.html new file mode 100644 index 0000000000..4c09e4db07 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/documentation-ui/src/app/core/core.component.html @@ -0,0 +1,12 @@ + + +
+

Shared Components Overview

+ +
+
+
+
diff --git a/projects/frontend/shared-components/gui/projects/documentation-ui/src/app/core/core.component.scss b/projects/frontend/shared-components/gui/projects/documentation-ui/src/app/core/core.component.scss new file mode 100644 index 0000000000..836dffb8dc --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/documentation-ui/src/app/core/core.component.scss @@ -0,0 +1,21 @@ +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +.core-component { + .card { + height: 100%; + + .card-block { + display: flex; + justify-content: center; + align-items: center; + + img { + width: 100%; + } + } + } + +} diff --git a/projects/frontend/shared-components/gui/projects/documentation-ui/src/app/core/core.component.spec.ts b/projects/frontend/shared-components/gui/projects/documentation-ui/src/app/core/core.component.spec.ts new file mode 100644 index 0000000000..e2d1536521 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/documentation-ui/src/app/core/core.component.spec.ts @@ -0,0 +1,29 @@ +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { CoreComponent } from './core.component'; + +describe('CoreComponent', () => { + let component: CoreComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [CoreComponent] + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(CoreComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/projects/frontend/shared-components/gui/projects/documentation-ui/src/app/core/core.component.ts b/projects/frontend/shared-components/gui/projects/documentation-ui/src/app/core/core.component.ts new file mode 100644 index 0000000000..cd79f06ad7 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/documentation-ui/src/app/core/core.component.ts @@ -0,0 +1,14 @@ +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-core', + templateUrl: './core.component.html', + styleUrls: ['./core.component.scss'] +}) +export class CoreComponent { +} diff --git a/projects/frontend/shared-components/gui/projects/documentation-ui/src/app/core/core.module.ts b/projects/frontend/shared-components/gui/projects/documentation-ui/src/app/core/core.module.ts new file mode 100644 index 0000000000..dd3e55220a --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/documentation-ui/src/app/core/core.module.ts @@ -0,0 +1,24 @@ +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { CoreComponent } from './core.component'; +import { RouterModule, Routes } from '@angular/router'; +import { ClarityModule } from '@clr/angular'; + +const routes: Routes = [ + { + path: '', + component: CoreComponent + } +]; + +@NgModule({ + declarations: [CoreComponent], + imports: [RouterModule.forChild(routes), CommonModule, ClarityModule] +}) +export class CoreModule { +} diff --git a/projects/frontend/shared-components/gui/projects/documentation-ui/src/app/http.interceptor.ts b/projects/frontend/shared-components/gui/projects/documentation-ui/src/app/http.interceptor.ts new file mode 100644 index 0000000000..936095a5e8 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/documentation-ui/src/app/http.interceptor.ts @@ -0,0 +1,63 @@ +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Injectable, Optional } from '@angular/core'; +import { OAuthModuleConfig, OAuthResourceServerErrorHandler, OAuthStorage } from 'angular-oauth2-oidc'; +import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http'; + +import { Observable } from 'rxjs'; +import { authCodeFlowConfig } from './auth'; + +@Injectable() +export class AuthorizationInterceptor implements HttpInterceptor { + + constructor( + private authStorage: OAuthStorage, + private errorHandler: OAuthResourceServerErrorHandler, + @Optional() private moduleConfig: OAuthModuleConfig) { + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public intercept(req: HttpRequest, next: HttpHandler): Observable> { + const url = req.url.toLowerCase(); + if (!this.moduleConfig) { + return next.handle(req); + } + if (!this.moduleConfig.resourceServer) { + return next.handle(req); + } + if (!this.moduleConfig.resourceServer.allowedUrls) { + return next.handle(req); + } + if (!this.checkUrl(url)) { + return next.handle(req); + } + + const sendAccessToken = this.moduleConfig.resourceServer.sendAccessToken; + + if (sendAccessToken && url.startsWith(authCodeFlowConfig.issuer)) { + + const headers = req.headers + .set('Authorization', 'Basic ' + btoa(authCodeFlowConfig.clientId + ':')); + req = req.clone({ headers }); + + } else if (sendAccessToken) { + + const token = this.authStorage.getItem('access_token'); + const header = 'Bearer ' + token; + const headers = req.headers + .set('Authorization', header); + req = req.clone({ headers }); + + } + + return next.handle(req); + } + + private checkUrl(url: string): boolean { + const found = this.moduleConfig.resourceServer.allowedUrls.find(u => url.startsWith(u)); + return !!found; + } +} diff --git a/projects/frontend/shared-components/gui/projects/documentation-ui/src/assets/.gitkeep b/projects/frontend/shared-components/gui/projects/documentation-ui/src/assets/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/projects/frontend/shared-components/gui/projects/documentation-ui/src/environments/environment.prod.ts b/projects/frontend/shared-components/gui/projects/documentation-ui/src/environments/environment.prod.ts new file mode 100644 index 0000000000..bdc8e7c053 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/documentation-ui/src/environments/environment.prod.ts @@ -0,0 +1,8 @@ +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +export const environment = { + production: true +}; diff --git a/projects/frontend/shared-components/gui/projects/documentation-ui/src/environments/environment.ts b/projects/frontend/shared-components/gui/projects/documentation-ui/src/environments/environment.ts new file mode 100644 index 0000000000..6bf2099149 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/documentation-ui/src/environments/environment.ts @@ -0,0 +1,21 @@ +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +// This file can be replaced during build by using the `fileReplacements` array. +// `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. +// The list of file replacements can be found in `angular.json`. + +export const environment = { + production: false +}; + +/* + * For easier debugging in development mode, you can import the following file + * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. + * + * This import should be commented out in production mode because it will have a negative impact + * on performance if an error is thrown. + */ +import 'zone.js/plugins/zone-error'; // Included with Angular CLI. diff --git a/projects/frontend/shared-components/gui/projects/documentation-ui/src/favicon.ico b/projects/frontend/shared-components/gui/projects/documentation-ui/src/favicon.ico new file mode 100644 index 0000000000..997406ad22 Binary files /dev/null and b/projects/frontend/shared-components/gui/projects/documentation-ui/src/favicon.ico differ diff --git a/projects/frontend/shared-components/gui/projects/documentation-ui/src/index.html b/projects/frontend/shared-components/gui/projects/documentation-ui/src/index.html new file mode 100644 index 0000000000..daccc8173e --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/documentation-ui/src/index.html @@ -0,0 +1,19 @@ + + + + + + + DocumentationUi + + + + + + + + + diff --git a/projects/frontend/shared-components/gui/projects/documentation-ui/src/main.ts b/projects/frontend/shared-components/gui/projects/documentation-ui/src/main.ts new file mode 100644 index 0000000000..34f515dc75 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/documentation-ui/src/main.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { enableProdMode } from '@angular/core'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +import { AppModule } from './app/app.module'; +import { environment } from './environments/environment'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err) => console.error(err)); diff --git a/projects/frontend/shared-components/gui/projects/documentation-ui/src/polyfills.ts b/projects/frontend/shared-components/gui/projects/documentation-ui/src/polyfills.ts new file mode 100644 index 0000000000..ebd6d2cbc1 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/documentation-ui/src/polyfills.ts @@ -0,0 +1,58 @@ +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * This file includes polyfills needed by Angular and is loaded before the app. + * You can add your own extra polyfills to this file. + * + * This file is divided into 2 sections: + * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. + * 2. Application imports. Files imported after ZoneJS that should be loaded before your main + * file. + * + * The current setup is for so-called "evergreen" browsers; the last versions of browsers that + * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), + * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. + * + * Learn more in https://angular.io/guide/browser-support + */ + +/*************************************************************************************************** + * BROWSER POLYFILLS + */ + +import '@webcomponents/custom-elements'; +/** + * By default, zone.js will patch all possible macroTask and DomEvents + * user can disable parts of macroTask/DomEvents patch by setting following flags + * because those flags need to be set before `zone.js` being loaded, and webpack + * will put import in the top of bundle, so user need to create a separate file + * in this directory (for example: zone-flags.ts), and put the following flags + * into that file, and then add the following code before importing zone.js. + * import './zone-flags'; + * + * The flags allowed in zone-flags.ts are listed here. + * + * The following flags will work for all browsers. + * + * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame + * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick + * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames + * + * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js + * with the following flag, it will bypass `zone.js` patch for IE/Edge + * + * (window as any).__Zone_enable_cross_context_check = true; + * + */ +/*************************************************************************************************** + * Zone JS is required by default for Angular itself. + */ +import 'zone.js'; // Included with Angular CLI. +(window as any).global = window; + +/*************************************************************************************************** + * APPLICATION IMPORTS + */ diff --git a/projects/frontend/shared-components/gui/projects/documentation-ui/src/styles.scss b/projects/frontend/shared-components/gui/projects/documentation-ui/src/styles.scss new file mode 100644 index 0000000000..7bc758add5 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/documentation-ui/src/styles.scss @@ -0,0 +1,292 @@ +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +/* You can add global styles to this file, and also import other style files */ + +/** + Placeholder + **/ + +:root { + --taurus-placeholder-color: var(--clr-color-neutral-500, #b3b3b3); +} + +/* _normalize overwrite */ +::-webkit-input-placeholder { + /* clr-forms-placeholder-color */ + color: var(--taurus-placeholder-color) !important; +} + +/* Internet Explorer 10-11 */ +:-ms-input-placeholder { + /* clr-forms-placeholder-color */ + color: var(--taurus-placeholder-color) !important; +} + +/* Microsoft Edge */ +::-ms-input-placeholder { + /* clr-forms-placeholder-color */ + color: var(--taurus-placeholder-color) !important; +} + +/* Chrome, Firefox, Opera, Safari 10.1+ */ +::placeholder { + /* clr-forms-placeholder-color */ + color: var(--taurus-placeholder-color) !important; +} + +.fade-to-dark.dark { + --taurus-placeholder-color: #b3b3b3; +} + +html { + scroll-behavior: smooth; +} + +.content-container.switch-btn-bottom .nav-content { + margin-bottom: 1.5rem; +} + +/* combo box */ +.clr-popover-content { + margin-top: 10px; +} + +/* vmw search component */ +vdk-search .search-container cds-icon[shape=search] { + top: 2px !important; +} + +/* forms */ +.destination-modal-body .custom-header-item .custom-header-input { + display: inline-block; + max-width: 10rem; +} + +.modal-submit { + position: absolute; + left: -9999px; + z-index: -9999; + visibility: hidden; +} + +vdk-form-section-container.p-header-section .form-header { + padding-bottom: 0 !important; + margin-top: 0; +} + +vdk-form-section-container.p-header-section .form-header .section-title { + font-size: .7rem; + font-weight: 500; +} + +vdk-form-section-container.p-header-section .vdk-vertical-line { + margin-top: 0 !important; +} + +vdk-form-section-container.p-header-section .csp-edit-button { + margin-left: 1rem !important; +} + +/* links */ +a.disabled { + pointer-events: none; + cursor: default; + opacity: 0.5; +} + +/* theme */ +.toogle-theme { + position: absolute; + bottom: 0; + background-color: transparent !important; + padding-left: 0 !important; +} + +.toogle-theme:hover { + background-color: transparent !important; +} + +.toogle-theme .nav-link { + background-color: transparent !important; +} + +.page-title { + margin-top: 0; +} + +.tc { + text-align: center !important; +} + +/* margins */ +.m-0 { + margin: 0 !important; +} + +.mt-0 { + margin-top: 0 !important; +} + +.mb-12 { + margin-bottom: 12px !important; +} + +.pl-35 { + padding-left: 35px !important; +} + +.w-100 { + width: 100% !important; +} + +/* modals */ +.modal-body > :first-child { + margin-top: 0; +} + +input.modal-form-control { + width: 12rem; +} + +select.modal-form-control { + width: 8rem; +} + +.modal-actions { + margin-top: -20px !important; +} + +/* color styles */ +.red { + color: #EB5757; +} + +/* position */ +.right { + float: right; +} + +.left { + float: left; +} + +/* lists */ +ul.idented { + text-indent: 2em; +} + +/* media */ +@media (max-width: 600px) { + .right { + float: none !important; + } +} + +/* datagrid cells */ +.cell-img-container { + min-width: 30px !important; + width: 60px !important; + text-align: center !important; +} + +.cell-center { + margin: auto !important; +} + +.enabled-column { + width: 6rem !important; +} + +/* limit-height */ +.limit-height { + height: 50vh; +} + +.limit-height clr-datagrid { + height: 50vh !important; +} + +.limit-height clr-datagrid .datagrid { + margin-top: 0; +} + +.limit-height clr-datagrid clr-dg-cell { + margin: auto; +} + +/* team-list-component */ +.team-list-container .clr-form-control { + margin-top: 0 !important; +} + +/* CSP-HEADER */ +/* Hide header assets */ +.header .header-actions .nav-icon.app-switcher { + width: 0; +} + +.header .header-actions .nav-icon.app-switcher cds-icon { + display: none !important; +} + +.header .header-actions .nav-icon.user-menu span.org-name { + display: none; +} + +@media (min-width: 768px) { + .header .header-actions .nav-icon.user-menu div { + margin-top: .5rem; + } +} + +.clr-vertical-nav .nav-group-trigger .nav-group-trigger-icon { + height: 0.8rem; +} + +.page-wrapper { + padding-bottom: 2.4rem; +} + +pre { + padding: 0.5rem; + overflow: auto; +} + +p code { + color: #c21d00; +} + +.table-documentation { + caption { + display: none; + } +} + +.token { + &.attr-name, &.builtin, &.char &.inserted &.selector &.string { + color: #aa5d00; + } + + &.atrule, &.attr-value, &.function { + color: #aa5d00; + } + + &.constant, &.deleted, &.property, &.symbol, &.tag { + color: #007faa; + } + + &.punctuation { + color: #545454; + } + + &.class { + color: #306B00; + } + + &.access { + color: #00567A; + } +} diff --git a/projects/frontend/shared-components/gui/projects/documentation-ui/src/test.ts b/projects/frontend/shared-components/gui/projects/documentation-ui/src/test.ts new file mode 100644 index 0000000000..d57e73ade7 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/documentation-ui/src/test.ts @@ -0,0 +1,30 @@ +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context(path: string, deep?: boolean, filter?: RegExp): { + keys(): string[]; + (id: string): T; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment( + BrowserDynamicTestingModule, + platformBrowserDynamicTesting(), + { + teardown: { destroyAfterEach: false } + } +); +// Then we find all the tests. +const context = require.context('./', true, /\.spec\.ts$/); +// And load the modules. +context.keys().map(context); diff --git a/projects/frontend/shared-components/gui/projects/documentation-ui/tsconfig.app.json b/projects/frontend/shared-components/gui/projects/documentation-ui/tsconfig.app.json new file mode 100644 index 0000000000..809c09ca71 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/documentation-ui/tsconfig.app.json @@ -0,0 +1,14 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "../../out-tsc/app", + "types": [] + }, + "files": [ + "src/main.ts", + "src/polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} diff --git a/projects/frontend/shared-components/gui/projects/documentation-ui/tsconfig.spec.json b/projects/frontend/shared-components/gui/projects/documentation-ui/tsconfig.spec.json new file mode 100644 index 0000000000..aab257ed7d --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/documentation-ui/tsconfig.spec.json @@ -0,0 +1,17 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "../../out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "src/test.ts", + "src/polyfills.ts" + ], + "include": [ + "src/**/*.spec.ts", + "src/**/*.d.ts" + ] +} diff --git a/projects/frontend/shared-components/gui/projects/shared/.eslintrc.json b/projects/frontend/shared-components/gui/projects/shared/.eslintrc.json new file mode 100644 index 0000000000..50742c8cfe --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/.eslintrc.json @@ -0,0 +1,112 @@ +{ + "extends": "../../.eslintrc.json", + "ignorePatterns": [ + "!**/*" + ], + "overrides": [ + { + "files": [ + "*.ts" + ], + "parserOptions": { + "project": [ + "projects/shared/tsconfig.lib.json" + ], + "createDefaultProgram": true + }, + "rules": { + "@angular-eslint/component-selector": [ + "error", + { + "type": "element", + "prefix": "shared", + "style": "kebab-case" + } + ], + "@angular-eslint/directive-selector": [ + "error", + { + "type": "attribute", + "prefix": "shared", + "style": "camelCase" + } + ], + "@typescript-eslint/naming-convention": [ + "warn", + { + "selector": "property", + "format": [ + "camelCase" + ], + "leadingUnderscore": "allow", + "filter": { + "regex": "^((\\$\\..+))$", + "match": false + } + }, + { + "selector": "classProperty", + "modifiers": [ + "static" + ], + "format": [ + "camelCase", + "UPPER_CASE" + ], + "leadingUnderscore": "allow" + } + ], + "@typescript-eslint/no-shadow": "error", + "no-shadow": "off" + } + }, + { + "files": [ + "*.spec.ts" + ], + "parserOptions": { + "project": [ + "projects/shared/tsconfig.spec.json" + ] + }, + "rules": { + "@typescript-eslint/naming-convention": [ + "warn", + { + "selector": "objectLiteralProperty", + "format": [ + "camelCase" + ], + "leadingUnderscore": "allow", + "filter": { + "regex": "^((\\$\\..+)|(test_))", + "match": false + } + }, + { + "selector": "classProperty", + "modifiers": [ + "static" + ], + "format": [ + "camelCase", + "UPPER_CASE" + ], + "leadingUnderscore": "allow" + } + ], + "@typescript-eslint/ban-ts-comment": "off", + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-floating-promises": "off", + "@typescript-eslint/no-unsafe-assignment": "off", + "@typescript-eslint/unbound-method": "off" + } + }, + { + "files": [ + "*.html" + ], + "rules": {} + } + ] +} diff --git a/projects/frontend/shared-components/gui/projects/shared/assets/animations/botour-card.svg b/projects/frontend/shared-components/gui/projects/shared/assets/animations/botour-card.svg new file mode 100644 index 0000000000..f3ee5a6061 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/assets/animations/botour-card.svg @@ -0,0 +1,198 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/projects/frontend/shared-components/gui/projects/shared/assets/animations/botour-popup.svg b/projects/frontend/shared-components/gui/projects/shared/assets/animations/botour-popup.svg new file mode 100644 index 0000000000..75dbebccf5 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/assets/animations/botour-popup.svg @@ -0,0 +1,169 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/projects/frontend/shared-components/gui/projects/shared/assets/animations/botour-steps.svg b/projects/frontend/shared-components/gui/projects/shared/assets/animations/botour-steps.svg new file mode 100644 index 0000000000..079a7b70c1 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/assets/animations/botour-steps.svg @@ -0,0 +1,178 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/projects/frontend/shared-components/gui/projects/shared/assets/images/botour-background.png b/projects/frontend/shared-components/gui/projects/shared/assets/images/botour-background.png new file mode 100644 index 0000000000..25d990ab13 Binary files /dev/null and b/projects/frontend/shared-components/gui/projects/shared/assets/images/botour-background.png differ diff --git a/projects/frontend/shared-components/gui/projects/shared/assets/images/botour-success.svg b/projects/frontend/shared-components/gui/projects/shared/assets/images/botour-success.svg new file mode 100644 index 0000000000..059a5ed6fd --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/assets/images/botour-success.svg @@ -0,0 +1,141 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + success_01 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/projects/frontend/shared-components/gui/projects/shared/assets/images/coming-soon-plan.svg b/projects/frontend/shared-components/gui/projects/shared/assets/images/coming-soon-plan.svg new file mode 100644 index 0000000000..bc2d12480b --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/assets/images/coming-soon-plan.svg @@ -0,0 +1,116 @@ + + + + + + + + + 1-plan + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/projects/frontend/shared-components/gui/projects/shared/assets/images/desktop-B001.svg b/projects/frontend/shared-components/gui/projects/shared/assets/images/desktop-B001.svg new file mode 100644 index 0000000000..891bdf7232 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/assets/images/desktop-B001.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + desktop-B001 + + + + + + + + + + + + + diff --git a/projects/frontend/shared-components/gui/projects/shared/assets/images/robot-magnifying-glass.svg b/projects/frontend/shared-components/gui/projects/shared/assets/images/robot-magnifying-glass.svg new file mode 100644 index 0000000000..eba817c461 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/assets/images/robot-magnifying-glass.svg @@ -0,0 +1,186 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + robot magnifying glass + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/projects/frontend/shared-components/gui/projects/shared/karma.conf.js b/projects/frontend/shared-components/gui/projects/shared/karma.conf.js new file mode 100644 index 0000000000..a1d9c95ef0 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/karma.conf.js @@ -0,0 +1,68 @@ +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function(config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-junit-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, '../../reports/coverage/shared'), + subdir: '.', + reporters: [ + //Code coverage - output in HTML file and Console(to be parsed in the CI/CD badge) + { type: 'html' }, + { type: 'text-summary' }, + { type: 'lcovonly' } + ], + check: { + global: { + lines: 80 + } + } + }, + reporters: ['progress', 'junit', 'coverage'], + junitReporter: { + outputDir: require('path').join(__dirname, '../../reports/test-results/shared'), + outputFile: 'unit-tests.xml', + useBrowserName: false + }, + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['ChromeHeadless'], + customLaunchers: { + ChromeHeadless_No_Sandbox: { + base: 'ChromeHeadless', + flags: ['--no-sandbox'] + } + }, + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/projects/frontend/shared-components/gui/projects/shared/ng-package.json b/projects/frontend/shared-components/gui/projects/shared/ng-package.json new file mode 100644 index 0000000000..62b983c8bb --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/ng-package.json @@ -0,0 +1,10 @@ +{ + "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../dist/shared", + "assets": [ + "./assets" + ], + "lib": { + "entryFile": "src/public-api.ts" + } +} diff --git a/projects/frontend/shared-components/gui/projects/shared/package.json b/projects/frontend/shared-components/gui/projects/shared/package.json new file mode 100644 index 0000000000..9698b9cfb4 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/package.json @@ -0,0 +1,51 @@ +{ + "name": "@vdk/shared", + "version": "0.0.1", + "engines": { + "node": "^14.15.0 || >=16.10.0", + "npm": ">=8.5.5" + }, + "license": "Apache-2.0", + "peerDependencies": { + "@angular/animations": "~13.3.10", + "@angular/cdk": "~13.3.8", + "@angular/common": "~13.3.10", + "@angular/compiler": "~13.3.10", + "@angular/core": "~13.3.10", + "@angular/forms": "~13.3.10", + "@angular/platform-browser": "~13.3.10", + "@angular/platform-browser-dynamic": "~13.3.10", + "@angular/router": "~13.3.10", + "@cds/core": "~5.7.2", + "@clr/angular": "~13.3.1", + "@clr/icons": "~13.0.2", + "@clr/ui": "~13.3.1", + "@ngrx/effects": "~13.2.0", + "@ngrx/router-store": "~13.2.0", + "@ngrx/store": "~13.2.0", + "@swimlane/ngx-graph": "~8.0.0", + "@webcomponents/custom-elements": "~1.5.0", + "angular-ng-autocomplete": "~2.0.8", + "d3": "~5.16.0", + "dompurify": "~2.3.8", + "lodash": "~4.17.21", + "ngx-clipboard": "~15.1.0", + "ngx-cookie-service": "~13.2.1", + "rxjs": "~7.5.5", + "zone.js": "~0.11.7" + }, + "dependencies": { + "tslib": "2.3.1" + }, + "description": "Taurus Shared library enables reusability of shared features like: NgRx Redux, Error Handlers, Utils, Generic Components etc.", + "repository": { + "type": "git", + "url": "-" + }, + "keywords": [ + "vdk", + "shared", + "versatile-data-kit" + ], + "author": "Versatile Data Kit (https://github.com/vmware/versatile-data-kit)" +} diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/http/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/http/index.ts new file mode 100644 index 0000000000..412a9d8bd1 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/http/index.ts @@ -0,0 +1,8 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './request'; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/http/public-api.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/http/public-api.ts new file mode 100644 index 0000000000..ba142389bd --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/http/public-api.ts @@ -0,0 +1,8 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './index'; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/http/request/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/http/request/index.ts new file mode 100644 index 0000000000..d37bcae73a --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/http/request/index.ts @@ -0,0 +1,8 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './request.model'; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/http/request/request.model.spec.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/http/request/request.model.spec.ts new file mode 100644 index 0000000000..c3e677beec --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/http/request/request.model.spec.ts @@ -0,0 +1,376 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { ApiPredicate, RequestFilterImpl, RequestOrderImpl, RequestPageImpl } from './request.model'; + +describe('RequestPage', () => { + it('should verify instance is created', () => { + // When + const instance = new RequestPageImpl(0, 0); + + // Then + expect(instance).toBeDefined(); + expect(instance).toBeInstanceOf(RequestPageImpl); + }); + + it('should verify correct value are assigned', () => { + // Given + const pageNumber = 4; + const pageSize = 10; + + // When + const instance = new RequestPageImpl(pageNumber, pageSize); + + // Then + expect(instance.page).toEqual(pageNumber); + expect(instance.size).toEqual(pageSize); + }); + + it('should verify on Nil parameters default value will be assigned', () => { + // When + const instance = new RequestPageImpl(null, undefined); + + // Then + expect(instance.page).toEqual(1); + expect(instance.size).toEqual(25); + }); + + describe('Statics::', () => { + describe('Methods::', () => { + describe('|of|', () => { + it('should verify factory method will create instance', () => { + // When + const instance = RequestPageImpl.of(8, 20); + + // Then + expect(instance).toBeInstanceOf(RequestPageImpl); + expect(instance.page).toEqual(8); + expect(instance.size).toEqual(20); + }); + }); + + describe('|empty|', () => { + it('should verify will create empty instance with default values', () => { + // When + const instance = RequestPageImpl.empty(); + + // Then + expect(instance).toBeInstanceOf(RequestPageImpl); + expect(instance.page).toEqual(1); + expect(instance.size).toEqual(25); + }); + }); + + describe('|fromLiteral|', () => { + it('should verify will create new instance from given literal object', () => { + // Given + const literal = { pageNumber: 7, pageSize: 52 }; + + // When + const instance = RequestPageImpl.fromLiteral(literal); + + // Then + expect(instance).toBeInstanceOf(RequestPageImpl); + expect(instance.page).toEqual(7); + expect(instance.size).toEqual(52); + }); + }); + }); + }); + + describe('Methods::', () => { + describe('|toLiteral|', () => { + it('should verify will create literal object from RequestPage object ', () => { + // Given + const requestPage = RequestPageImpl.of(5, 17); + + // When + const literal = requestPage.toLiteral(); + + // Then + expect(literal).not.toBeInstanceOf(RequestPageImpl); + expect(literal).toBeInstanceOf(Object); + expect(literal.pageNumber).toEqual(5); + expect(literal.pageSize).toEqual(17); + }); + }); + + describe('|toLiteralDeepClone|', () => { + it('should verify will create literal deep cloned object from RequestPage object ', () => { + // Given + const requestPage = RequestPageImpl.of(9, 13); + + // When + const literal = requestPage.toLiteralDeepClone(); + + // Then + expect(literal).not.toBeInstanceOf(RequestPageImpl); + expect(literal).toBeInstanceOf(Object); + expect(literal.pageNumber).toEqual(9); + expect(literal.pageSize).toEqual(13); + }); + }); + }); +}); + +describe('RequestOrder', () => { + let apiPredicate1: ApiPredicate; + let apiPredicate2: ApiPredicate; + let apiPredicate3: ApiPredicate; + + beforeEach(() => { + apiPredicate1 = { pattern: 'test*', property: 'config.team', sort: 'DESC' }; + apiPredicate2 = { pattern: 'mock*', property: 'config.job', sort: 'ASC' }; + apiPredicate3 = { pattern: 'prod*', property: 'config.status', sort: 'DESC' }; + }); + + it('should verify instance is created', () => { + // When + const instance = new RequestOrderImpl(); + + // Then + expect(instance).toBeDefined(); + expect(instance).toBeInstanceOf(RequestOrderImpl); + }); + + it('should verify correct value are assigned', () => { + // When + const instance = new RequestOrderImpl(apiPredicate1, apiPredicate2, apiPredicate3); + + // Then + expect(instance.criteria).toBeInstanceOf(Array); + expect(instance.criteria.length).toEqual(3); + expect(instance.criteria[0]).toBe(apiPredicate1); + expect(instance.criteria[1]).toBe(apiPredicate2); + expect(instance.criteria[2]).toBe(apiPredicate3); + }); + + it('should verify wont assign Nil parameters', () => { + // When + const instance = new RequestOrderImpl(null, apiPredicate2, undefined, apiPredicate1, null, apiPredicate3, undefined); + + // Then + expect(instance.criteria.length).toEqual(3); + expect(instance.criteria[0]).toBe(apiPredicate2); + expect(instance.criteria[1]).toBe(apiPredicate1); + expect(instance.criteria[2]).toBe(apiPredicate3); + }); + + describe('Statics::', () => { + describe('Methods::', () => { + describe('|of|', () => { + it('should verify factory method will create instance', () => { + // When + const instance = RequestOrderImpl.of(apiPredicate1, null, apiPredicate2, apiPredicate3); + + // Then + expect(instance).toBeInstanceOf(RequestOrderImpl); + expect(instance.criteria.length).toEqual(3); + expect(instance.criteria[0]).toBe(apiPredicate1); + expect(instance.criteria[1]).toBe(apiPredicate2); + expect(instance.criteria[2]).toBe(apiPredicate3); + }); + }); + + describe('|empty|', () => { + it('should verify will create empty instance with no criteria', () => { + // When + const instance = RequestOrderImpl.empty(); + + // Then + expect(instance).toBeInstanceOf(RequestOrderImpl); + expect(instance.criteria).toEqual([]); + }); + }); + + describe('|fromLiteral|', () => { + it('should verify will create new instance from given literal Array of ApiPredicates', () => { + // Given + const literal: ApiPredicate[] = [apiPredicate1, apiPredicate2, apiPredicate3]; + + // When + const instance = RequestOrderImpl.fromLiteral(literal); + + // Then + expect(instance).toBeInstanceOf(RequestOrderImpl); + expect(instance.criteria.length).toEqual(3); + expect(instance.criteria[0]).toBe(apiPredicate1); + expect(instance.criteria[1]).toBe(apiPredicate2); + expect(instance.criteria[2]).toBe(apiPredicate3); + }); + }); + }); + }); + + describe('Methods::', () => { + describe('|toLiteral|', () => { + it('should verify will create literal from RequestOrder object ', () => { + // Given + const requestOrder = RequestOrderImpl.of(apiPredicate1, null, apiPredicate2, undefined, apiPredicate3); + + // When + const literal = requestOrder.toLiteral(); + + // Then + expect(literal).not.toBeInstanceOf(RequestOrderImpl); + expect(literal).toBeInstanceOf(Array); + expect(literal[0]).toBe(apiPredicate1); + expect(literal[1]).toBe(apiPredicate2); + expect(literal[2]).toBe(apiPredicate3); + }); + }); + + describe('|toLiteralDeepClone|', () => { + it('should verify will create literal deep cloned from RequestPage object ', () => { + // Given + const requestOrder = RequestOrderImpl.of(apiPredicate3, null, apiPredicate1, undefined, apiPredicate2); + + // When + const literal = requestOrder.toLiteralDeepClone(); + + // Then + expect(literal).not.toBeInstanceOf(RequestOrderImpl); + expect(literal).toBeInstanceOf(Array); + expect(literal[0]).not.toBe(apiPredicate3); + expect(literal[1]).not.toBe(apiPredicate1); + expect(literal[2]).not.toBe(apiPredicate2); + expect(literal[0]).toEqual(apiPredicate3); + expect(literal[1]).toEqual(apiPredicate1); + expect(literal[2]).toEqual(apiPredicate2); + }); + }); + }); +}); + +describe('RequestFilter', () => { + let apiPredicate1: ApiPredicate; + let apiPredicate2: ApiPredicate; + let apiPredicate3: ApiPredicate; + + beforeEach(() => { + apiPredicate1 = { pattern: 'test*', property: 'config.team', sort: 'DESC' }; + apiPredicate2 = { pattern: 'mock*', property: 'config.job', sort: 'ASC' }; + apiPredicate3 = { pattern: 'prod*', property: 'config.status', sort: 'DESC' }; + }); + + it('should verify instance is created', () => { + // When + const instance = new RequestFilterImpl(); + + // Then + expect(instance).toBeDefined(); + expect(instance).toBeInstanceOf(RequestFilterImpl); + }); + + it('should verify correct value are assigned', () => { + // When + const instance = new RequestFilterImpl(apiPredicate1, apiPredicate2, apiPredicate3); + + // Then + expect(instance.criteria).toBeInstanceOf(Array); + expect(instance.criteria.length).toEqual(3); + expect(instance.criteria[0]).toBe(apiPredicate1); + expect(instance.criteria[1]).toBe(apiPredicate2); + expect(instance.criteria[2]).toBe(apiPredicate3); + }); + + it('should verify wont assign Nil parameters', () => { + // When + const instance = new RequestFilterImpl(null, apiPredicate2, undefined, apiPredicate1, null, apiPredicate3, undefined); + + // Then + expect(instance.criteria.length).toEqual(3); + expect(instance.criteria[0]).toBe(apiPredicate2); + expect(instance.criteria[1]).toBe(apiPredicate1); + expect(instance.criteria[2]).toBe(apiPredicate3); + }); + + describe('Statics::', () => { + describe('Methods::', () => { + describe('|of|', () => { + it('should verify factory method will create instance', () => { + // When + const instance = RequestFilterImpl.of(apiPredicate1, null, apiPredicate2, apiPredicate3); + + // Then + expect(instance).toBeInstanceOf(RequestFilterImpl); + expect(instance.criteria.length).toEqual(3); + expect(instance.criteria[0]).toBe(apiPredicate1); + expect(instance.criteria[1]).toBe(apiPredicate2); + expect(instance.criteria[2]).toBe(apiPredicate3); + }); + }); + + describe('|empty|', () => { + it('should verify will create empty instance with no criteria', () => { + // When + const instance = RequestFilterImpl.empty(); + + // Then + expect(instance).toBeInstanceOf(RequestFilterImpl); + expect(instance.criteria).toEqual([]); + }); + }); + + describe('|fromLiteral|', () => { + it('should verify will create new instance from given literal Array of ApiPredicates', () => { + // Given + const literal: ApiPredicate[] = [apiPredicate1, apiPredicate2, apiPredicate3]; + + // When + const instance = RequestFilterImpl.fromLiteral(literal); + + // Then + expect(instance).toBeInstanceOf(RequestFilterImpl); + expect(instance.criteria.length).toEqual(3); + expect(instance.criteria[0]).toBe(apiPredicate1); + expect(instance.criteria[1]).toBe(apiPredicate2); + expect(instance.criteria[2]).toBe(apiPredicate3); + }); + }); + }); + }); + + describe('Methods::', () => { + describe('|toLiteral|', () => { + it('should verify will create literal from RequestFilter object ', () => { + // Given + const requestFilter = RequestFilterImpl.of(apiPredicate1, null, apiPredicate2, undefined, apiPredicate3); + + // When + const literal = requestFilter.toLiteral(); + + // Then + expect(literal).not.toBeInstanceOf(RequestFilterImpl); + expect(literal).toBeInstanceOf(Array); + expect(literal[0]).toBe(apiPredicate1); + expect(literal[1]).toBe(apiPredicate2); + expect(literal[2]).toBe(apiPredicate3); + }); + }); + + describe('|toLiteralDeepClone|', () => { + it('should verify will create literal deep cloned from RequestFilter object ', () => { + // Given + const requestFilter = RequestFilterImpl.of(apiPredicate3, null, apiPredicate1, undefined, apiPredicate2); + + // When + const literal = requestFilter.toLiteralDeepClone(); + + // Then + expect(literal).not.toBeInstanceOf(RequestFilterImpl); + expect(literal).toBeInstanceOf(Array); + expect(literal[0]).not.toBe(apiPredicate3); + expect(literal[1]).not.toBe(apiPredicate1); + expect(literal[2]).not.toBe(apiPredicate2); + expect(literal[0]).toEqual(apiPredicate3); + expect(literal[1]).toEqual(apiPredicate1); + expect(literal[2]).toEqual(apiPredicate2); + }); + }); + }); +}); diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/http/request/request.model.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/http/request/request.model.ts new file mode 100644 index 0000000000..219ec59109 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/http/request/request.model.ts @@ -0,0 +1,197 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Literal } from '../../interfaces'; +import { CollectionsUtil } from '../../../utils'; + +// Page DTO + +export type LiteralRequestPage = { pageNumber: number; pageSize: number }; + +export interface RequestPage extends Literal { + readonly page: number; + readonly size: number; +} + +/** + * ** Request Page DTO. + * + * + */ +export class RequestPageImpl implements RequestPage { + public readonly page: number; + public readonly size: number; + + constructor(page: number, size: number) { + this.page = page ?? 1; + this.size = size ?? 25; + } + + /** + * ** Factory method. + */ + static of(page: number, size: number): RequestPageImpl { + return new RequestPageImpl(page, size); + } + + /** + * ** Factory method for empty RequestPageDTO. + */ + static empty(): RequestPageImpl { + return new RequestPageImpl(null, null); + } + + /** + * ** Creates DTO from literal. + */ + static fromLiteral(literalDTO: { pageNumber: number; pageSize: number }): RequestPageImpl { + return RequestPageImpl.of(literalDTO.pageNumber, literalDTO.pageSize); + } + + /** + * @inheritDoc + */ + toLiteral(): LiteralRequestPage { + return { + pageNumber: this.page ?? 1, + pageSize: this.size ?? 25 + }; + } + + /** + * @inheritDoc + */ + toLiteralDeepClone(): LiteralRequestPage { + return this.toLiteral(); + } +} + +// Order DTO + +export interface RequestOrder extends Literal { + readonly criteria: ApiPredicate[]; +} + +/** + * ** Request Order DTO. + * + * + */ +export class RequestOrderImpl implements RequestOrder { + public readonly criteria: ApiPredicate[]; + + constructor(...criteria: ApiPredicate[]) { + // eslint-disable-next-line @typescript-eslint/unbound-method + this.criteria = [...criteria.filter(CollectionsUtil.isDefined)]; + } + + /** + * ** Factory method. + */ + static of(...criteria: ApiPredicate[]): RequestOrderImpl { + return new RequestOrderImpl(...criteria); + } + + /** + * ** Factory method for empty RequestOrderDTO. + */ + static empty(): RequestOrderImpl { + return new RequestOrderImpl(); + } + + /** + * ** Creates DTO from literal. + */ + static fromLiteral(literalCriteria: Array): RequestOrderImpl { + return RequestOrderImpl.of(...literalCriteria); + } + + /** + * @inheritDoc + */ + toLiteral(): LiteralApiPredicates { + return [...this.criteria]; + } + + /** + * @inheritDoc + */ + toLiteralDeepClone(): LiteralApiPredicates { + return this.criteria + .map((c) => ({ ...c })); + } +} + +// Filter DTO + +export interface RequestFilter extends Literal { + readonly criteria: ApiPredicate[]; +} + +/** + * ** Request Filter DTO. + * + * + */ +export class RequestFilterImpl implements RequestFilter { + public readonly criteria: ApiPredicate[]; + + constructor(...criteria: ApiPredicate[]) { + // eslint-disable-next-line @typescript-eslint/unbound-method + this.criteria = [...criteria.filter(CollectionsUtil.isDefined)]; + } + + /** + * ** Factory method. + */ + static of(...criteria: ApiPredicate[]): RequestFilterImpl { + return new RequestFilterImpl(...criteria); + } + + /** + * ** Factory method for empty RequestFilterDTO. + */ + static empty(): RequestFilterImpl { + return new RequestFilterImpl(); + } + + /** + * ** Creates DTO from literal. + */ + static fromLiteral(literalCriteria: Array): RequestFilterImpl { + return RequestFilterImpl.of(...literalCriteria); + } + + /** + * @inheritDoc + */ + toLiteral(): LiteralApiPredicates { + return [...this.criteria]; + } + + /** + * @inheritDoc + */ + toLiteralDeepClone(): LiteralApiPredicates { + return this.criteria + .map((c) => ({ ...c })); + } +} + +// Generic Predicate for API + +export type LiteralApiPredicates = Array; + +export const ASC = 'ASC'; +export const DESC = 'DESC'; +export type DirectionType = typeof ASC | typeof DESC; + +export interface ApiPredicate { + readonly property: string; + readonly pattern: string; + readonly sort: DirectionType; +} diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/index.ts new file mode 100644 index 0000000000..8698258268 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/index.ts @@ -0,0 +1,13 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './http'; +export * from './interfaces'; +export * from './object'; +export * from './predicate'; +export * from './route'; +export * from './tasks'; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/interfaces/comparable.interface.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/interfaces/comparable.interface.ts new file mode 100644 index 0000000000..147d9d9e97 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/interfaces/comparable.interface.ts @@ -0,0 +1,72 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * ** Interface for Comparison data. + * + * + */ +export interface Comparable { + /** + * ** Value stored in Comparable for Comparison. + */ + readonly value: T; + + /** + * ** Compares stored data with the provided one. + * + * -1 if the stored value is less than the provided value. + * 0 if both values are equal. + * 1 if stored value is bigger than the provided value. + */ + compare(comparable: Comparable): number; + + /** + * ** Verify if stored value is null or undefined and returns true. + */ + isNil(): boolean; + + /** + * ** Verify if stored value is not null and not undefined. + */ + notNil(): boolean; + + /** + * ** Compare if stored value is similar to provided value. + */ + like(comparable: Comparable): boolean; + + /** + * ** Verify if stored value is equals with provided value. + */ + equal(comparable: Comparable): boolean; + + /** + * ** Verify if stored value is different than provided value. + */ + notEqual(comparable: Comparable): boolean; + + /** + * ** Verify if stored value is less than provided value. + */ + lessThan(comparable: Comparable): boolean; + + /** + * ** Verify if stored value is less or equal than provided value. + */ + lessThanInclusive(comparable: Comparable): boolean; + + /** + * ** Verify if stored value is greater than provided value. + */ + greaterThan(comparable: Comparable): boolean; + + /** + * ** Verify if stored value is greater or equal than provided value. + */ + greaterThanInclusive(comparable: Comparable): boolean; +} diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/interfaces/copy.interface.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/interfaces/copy.interface.ts new file mode 100644 index 0000000000..ad29bc74cc --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/interfaces/copy.interface.ts @@ -0,0 +1,21 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +/* eslint-disable @typescript-eslint/no-explicit-any */ + +/** + * ** Interface for Copy of Object. + * + * + */ +export interface Copy> { + /** + * ** Make shallow copy of current Object. + * - Optionally provide partial Object to merge on top of the current one. + */ + copy(partial?: Partial); +} diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/interfaces/expression.interface.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/interfaces/expression.interface.ts new file mode 100644 index 0000000000..492aca817c --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/interfaces/expression.interface.ts @@ -0,0 +1,26 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Comparable } from './comparable.interface'; +import { Predicate } from './predicate.interface'; + +/** + * ** Interface for Expression. + * + * + */ +export interface Expression { + /** + * ** Predicates Array. + */ + readonly predicates: T[]; + + /** + * ** Evaluate Expression to boolean (true or false). + */ + evaluate(comparable?: Comparable): boolean; +} diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/interfaces/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/interfaces/index.ts new file mode 100644 index 0000000000..7ffff61d6d --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/interfaces/index.ts @@ -0,0 +1,14 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './comparable.interface'; +export * from './copy.interface'; +export * from './expression.interface'; +export * from './literal.interface'; +export * from './predicate.interface'; +export * from './replacer.interface'; +export * from './serializable.interface'; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/interfaces/literal.interface.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/interfaces/literal.interface.ts new file mode 100644 index 0000000000..d81fe5c61d --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/interfaces/literal.interface.ts @@ -0,0 +1,30 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { PrimitivesNil, PrimitivesNilArrays, PrimitivesNilObject } from '../../utils'; + +type SerializedType = PrimitivesNil | PrimitivesNilArrays | PrimitivesNilObject | unknown; + +/** + * ** This interface gives boundaries for Class instances to get converted into Literals. + * + * + */ +export interface Literal { + /** + * ** Implements this method and return data you want to be serialized into Literals. + */ + toLiteral(): T; + + /** + * ** Implements this method and return data you want to be serialized into Literals. + *

+ * - Data should be deep clone before return, to comply with immutability. + *

+ */ + toLiteralDeepClone(): T; +} diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/interfaces/predicate.interface.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/interfaces/predicate.interface.ts new file mode 100644 index 0000000000..8531fa0644 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/interfaces/predicate.interface.ts @@ -0,0 +1,25 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Comparable } from './comparable.interface'; + +/** + * ** Interface for Predicate Classes. + * + * + */ +export interface Predicate { + /** + * ** Stored comparable that have to be compared with provided comparable. + */ + readonly comparable: T; + + /** + * ** Evaluate Predicate to boolean (true or false). + */ + evaluate(comparable: C): boolean; +} diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/interfaces/public-api.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/interfaces/public-api.ts new file mode 100644 index 0000000000..ba142389bd --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/interfaces/public-api.ts @@ -0,0 +1,8 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './index'; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/interfaces/replacer.interface.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/interfaces/replacer.interface.ts new file mode 100644 index 0000000000..462712e1c7 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/interfaces/replacer.interface.ts @@ -0,0 +1,16 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * ** Interface for generic replacer. + * + * + */ +export interface Replacer { + searchValue: T; + replaceValue: T; +} diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/interfaces/serializable.interface.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/interfaces/serializable.interface.ts new file mode 100644 index 0000000000..452af94619 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/interfaces/serializable.interface.ts @@ -0,0 +1,22 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { PrimitivesNil, PrimitivesNilArrays, PrimitivesNilObject } from '../../utils'; + +type SerializedType = PrimitivesNil | PrimitivesNilArrays | PrimitivesNilObject | unknown; + +/** + * ** This interface gives boundaries for Objects that we want to be serializable for JSON. + * + * + */ +export interface Serializable { + /** + * ** Implements this method and return data you want to be serialized when JSON.stringify(...) is executed. + */ + toJSON(): T; +} diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/object/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/object/index.ts new file mode 100644 index 0000000000..787b0abb40 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/object/index.ts @@ -0,0 +1,8 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './model'; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/object/model/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/object/model/index.ts new file mode 100644 index 0000000000..dcc4045eb1 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/object/model/index.ts @@ -0,0 +1,8 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './taurus-object.model'; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/object/model/taurus-object.model.spec.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/object/model/taurus-object.model.spec.ts new file mode 100644 index 0000000000..3824703266 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/object/model/taurus-object.model.spec.ts @@ -0,0 +1,158 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +/* eslint-disable @typescript-eslint/dot-notation */ + +import { Subscription } from 'rxjs'; + +import { CallFake } from '../../../unit-testing'; + +import { TaurusObject } from './taurus-object.model'; + +describe('TaurusObject', () => { + it('should verify instance is created', () => { + // When + const instance = new TaurusObject(); + + // Then + expect(instance).toBeDefined(); + expect(instance).toBeInstanceOf(TaurusObject); + }); + + describe('Properties::', () => { + describe('|subscriptions|', () => { + it('should verify default value is empty Array', () => { + // Given + const instance = new TaurusObject(); + + // Then + expect(instance['subscriptions']).toEqual([]); + }); + }); + }); + + describe('Methods::', () => { + describe('|dispose|', () => { + it('should verify will clean all subscriptions', () => { + // Given + const subscription1 = jasmine.createSpyObj('subscription1', ['unsubscribe']); + const subscription2 = jasmine.createSpyObj('subscription2', ['unsubscribe']); + const subscription3 = jasmine.createSpyObj('subscription3', ['unsubscribe']); + const instance = new TaurusObject(); + instance['subscriptions'].push(subscription1, null, subscription2, undefined, subscription3); + + // When + instance.dispose(); + + // Then + expect(subscription1.unsubscribe).toHaveBeenCalled(); + expect(subscription2.unsubscribe).toHaveBeenCalled(); + expect(subscription3.unsubscribe).toHaveBeenCalled(); + }); + }); + + describe('|removeSubscriptionRef|', () => { + let subscription1: Subscription; + let subscription2: Subscription; + let subscription3: Subscription; + let instance: TaurusObject; + + beforeEach(() => { + subscription1 = new Subscription(); + subscription2 = new Subscription(); + subscription3 = new Subscription(); + instance = new TaurusObject(); + }); + + it('should verify will remove provided subscription from buffer', () => { + // Given + instance['subscriptions'].push(subscription1, null, undefined, subscription2, subscription3); + const unsubscribeSpy = spyOn(subscription2, 'unsubscribe').and.callThrough(); + + // Then 1 + expect(instance['subscriptions'].length).toEqual(5); + + // When + // @ts-ignore + const value = instance.removeSubscriptionRef(subscription2); + + // Then 2 + expect(value).toBeTrue(); + expect(unsubscribeSpy).toHaveBeenCalled(); + expect(instance['subscriptions'].length).toEqual(4); + }); + + it('should verify will remove provided subscription from buffer and unsubscribe error will be logged in console', () => { + // Given + instance['subscriptions'].push(subscription1); + const error = new Error('Error'); + const unsubscribeSpy = spyOn(subscription1, 'unsubscribe').and.throwError(error); + const consoleErrorSpy = spyOn(console, 'error').and.callFake(CallFake); + + // When + // @ts-ignore + const value = instance.removeSubscriptionRef(subscription1); + + // Then 2 + expect(value).toBeFalse(); + expect(unsubscribeSpy).toHaveBeenCalled(); + expect(consoleErrorSpy).toHaveBeenCalledWith(`Taurus Object failed to unsubscribe from rxjs stream!`, error); + }); + + it(`should verify will unsubscribe even reference doesn't exist in buffer`, () => { + // Given + instance['subscriptions'].push(subscription2, null, undefined); + const unsubscribeSpy = spyOn(subscription1, 'unsubscribe').and.callThrough(); + + // Then 1 + expect(instance['subscriptions'].length).toEqual(3); + + // When + // @ts-ignore + const value = instance.removeSubscriptionRef(subscription1); + + // Then 2 + expect(value).toBeTrue(); + expect(unsubscribeSpy).toHaveBeenCalled(); + expect(instance['subscriptions'].length).toEqual(3); + }); + + it(`should verify will return false when provided reference is not Subscription instance`, () => { + // Given + instance['subscriptions'].push(subscription1, subscription2); + + // Then 1 + expect(instance['subscriptions'].length).toEqual(2); + + // When + // @ts-ignore + const value1 = instance.removeSubscriptionRef(null); + // @ts-ignore + const value2 = instance.removeSubscriptionRef(undefined); + + // Then 2 + expect(value1).toBeFalse(); + expect(value2).toBeFalse(); + expect(instance['subscriptions'].length).toEqual(2); + }); + }); + + describe('|ngOnDestroy|', () => { + it('should verify will invoke correct method', () => { + // Given + const instance = new TaurusObject(); + const disposeSpy = spyOn(instance, 'dispose').and.callFake(CallFake); + + // When + instance.ngOnDestroy(); + + // Then + expect(disposeSpy).toHaveBeenCalled(); + }); + }); + }); +}); diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/object/model/taurus-object.model.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/object/model/taurus-object.model.ts new file mode 100644 index 0000000000..00ebef9bd5 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/object/model/taurus-object.model.ts @@ -0,0 +1,92 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Directive, OnDestroy } from '@angular/core'; + +import { Subscription } from 'rxjs'; +import { CollectionsUtil } from '../../../utils'; + +/** + * ** Base Class for all Angular related Objects. + * + * - Cleans all rxjs subscriptions on object destroy. + * + * + */ +@Directive() +// eslint-disable-next-line @angular-eslint/directive-class-suffix +export class TaurusObject implements OnDestroy { + + /** + * ** Store for Subscriptions references. + */ + protected subscriptions: Subscription[]; + + /** + * ** Constructor. + */ + constructor() { + this.subscriptions = []; + } + + /** + * ** Methods that will dispose Object. + * - Clean all Subscriptions. + */ + dispose(): void { + this.cleanSubscriptions(); + } + + /** + * @inheritDoc + */ + ngOnDestroy() { + this.dispose(); + } + + /** + * ** Clean all Subscriptions. + */ + protected cleanSubscriptions(): void { + // unsubscribe all valid subscriptions + this.subscriptions + // eslint-disable-next-line @typescript-eslint/unbound-method + .filter(CollectionsUtil.isDefined) + // eslint-disable-next-line @typescript-eslint/unbound-method + .forEach(TaurusObject._unsubscribeFromStream); + } + + protected removeSubscriptionRef(subscriptionRef: Subscription): boolean { + const subscriptionIndex = this.subscriptions.findIndex((s) => s === subscriptionRef); + + if (subscriptionIndex === -1) { + if (subscriptionRef instanceof Subscription) { + TaurusObject._unsubscribeFromStream(subscriptionRef); + + return true; + } + + return false; + } + + const removedSubscription = this.subscriptions.splice(subscriptionIndex, 1); + + return TaurusObject._unsubscribeFromStream(removedSubscription[0]); + } + + private static _unsubscribeFromStream(s: Subscription): boolean { + try { + s.unsubscribe(); + + return true; + } catch (e) { + console.error(`Taurus Object failed to unsubscribe from rxjs stream!`, e); + + return false; + } + } +} diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/object/public-api.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/object/public-api.ts new file mode 100644 index 0000000000..ba142389bd --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/object/public-api.ts @@ -0,0 +1,8 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './index'; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/comparable/comparable.impl.spec.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/comparable/comparable.impl.spec.ts new file mode 100644 index 0000000000..4ded4754d3 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/comparable/comparable.impl.spec.ts @@ -0,0 +1,305 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Comparable } from '../../interfaces'; + +import { ComparableImpl } from './comparable.impl'; + +class ComparableStub implements Comparable { + public readonly value: unknown; + + constructor(value: unknown) { + this.value = value; + } + + compare(_comparable: Comparable): number { + return 0; + } + + equal(_comparable: Comparable): boolean { + return true; + } + + like(_comparable: Comparable): boolean { + return true; + } + + notEqual(_comparable: Comparable): boolean { + return false; + } + + isNil(): boolean { + return false; + } + + notNil(): boolean { + return true; + } + + greaterThan(_comparable: Comparable): boolean { + return false; + } + + greaterThanInclusive(_comparable: Comparable): boolean { + return false; + } + + lessThan(_comparable: Comparable): boolean { + return false; + } + + lessThanInclusive(_comparable: Comparable): boolean { + return false; + } +} + +describe('ComparableImpl', () => { + it('should verify instance is created', () => { + // Given + const value = 'VDK'; + + // When + const instance = new ComparableImpl(value); + + // Then + expect(instance).toBeDefined(); + }); + + it('should verify value is correctly assigned', () => { + // Given + const value = 'VDK'; + + // When + const instance = new ComparableImpl(value); + + // Then + expect(instance.value).toBe(value); + }); + + describe('Statics::', () => { + describe('Methods::()', () => { + describe('|of|', () => { + it('should verify factory method will create instance', () => { + // Given + const value = 'VDK'; + + // When + const instance = ComparableImpl.of(value); + + // Then + expect(instance).toBeDefined(); + expect(instance).toBeInstanceOf(ComparableImpl); + }); + }); + }); + }); + + describe('Methods::()', () => { + let v1: unknown; + let v2: unknown; + let v3: unknown; + let v4: unknown; + let v5: unknown; + let v6: unknown; + let v7: unknown; + let v8: unknown; + + let c1: ComparableImpl; + let c2: ComparableImpl; + let c3: ComparableImpl; + let c4: ComparableImpl; + let c5: ComparableImpl; + let c6: ComparableImpl; + let c7: ComparableImpl; + let c8: ComparableImpl; + + beforeEach(() => { + v1 = null; + v2 = undefined; + v3 = 'Saggitarius'; + v4 = 10; + v5 = 'Saggitarius'; + v6 = 11; + v7 = 10; + v8 = 'Taurus'; + + c1 = ComparableImpl.of(v1); + c2 = ComparableImpl.of(v2); + c3 = ComparableImpl.of(v3); + c4 = ComparableImpl.of(v4); + c5 = ComparableImpl.of(v5); + c6 = ComparableImpl.of(v6); + c7 = ComparableImpl.of(v7); + c8 = ComparableImpl.of(v8); + }); + + describe('|compare|', () => { + it('should verify will return 0 for equal', () => { + // When + const comparison = c3.compare(c5); + + // Then + expect(comparison).toEqual(0); + }); + + it('should verify will return 1 for greaterThan', () => { + // When + const comparison = c8.compare(c5); + + // Then + expect(comparison).toEqual(1); + }); + + it('should verify will return -1 for lessThan', () => { + // When + const comparison = c5.compare(c8); + + // Then + expect(comparison).toEqual(-1); + }); + + it('should verify will return -1 given comparable is not instance of the current Constructor', () => { + // Given + const c10 = new ComparableStub(v2); + + // When + const comparison = c5.compare(c10); + + // Then + expect(comparison).toEqual(-1); + }); + }); + + describe('|isNil|', () => { + it('should verify will return true if value is null or undefined, otherwise false', () => { + // When + const r1 = c1.isNil(); + const r2 = c2.isNil(); + const r3 = c3.isNil(); + const r4 = c4.isNil(); + + // Then + expect(r1).toBeTrue(); + expect(r2).toBeTrue(); + expect(r3).toBeFalse(); + expect(r4).toBeFalse(); + }); + }); + + describe('|notNil|', () => { + it('should verify will return false if value is null or undefined, otherwise true', () => { + // When + const r1 = c1.notNil(); + const r2 = c2.notNil(); + const r3 = c3.notNil(); + const r4 = c4.notNil(); + + // Then + expect(r1).toBeFalse(); + expect(r2).toBeFalse(); + expect(r3).toBeTrue(); + expect(r4).toBeTrue(); + }); + }); + + describe('|like|', () => { + it('should verify will return true if values are similar, otherwise false', () => { + // When + const r1 = c3.like(c5); + const r2 = c3.like(c8); + + // Then + expect(r1).toBeTrue(); + expect(r2).toBeFalse(); + }); + }); + + describe('|equal|', () => { + it('should verify will return true if values are equal, otherwise false', () => { + // When + const r1 = c3.equal(c5); + const r2 = c3.equal(c8); + const r3 = c4.equal(c7); + const r4 = c4.equal(c6); + + // Then + expect(r1).toBeTrue(); + expect(r2).toBeFalse(); + expect(r3).toBeTrue(); + expect(r4).toBeFalse(); + }); + }); + + describe('|notEqual|', () => { + it('should verify will return true if values are not equal, otherwise false', () => { + // When + const r1 = c3.notEqual(c5); + const r2 = c3.notEqual(c8); + const r3 = c4.notEqual(c7); + const r4 = c4.notEqual(c6); + + // Then + expect(r1).toBeFalse(); + expect(r2).toBeTrue(); + expect(r3).toBeFalse(); + expect(r4).toBeTrue(); + }); + }); + + describe('|lessThan|', () => { + it('should verify will return true if value is less than provided, otherwise false', () => { + // When + const r1 = c4.lessThan(c6); + const r2 = c6.lessThan(c4); + + // Then + expect(r1).toBeTrue(); + expect(r2).toBeFalse(); + }); + }); + + describe('|lessThanInclusive|', () => { + it('should verify will return true if value is less than or equal to provided, otherwise false', () => { + // When + const r1 = c4.lessThanInclusive(c6); + const r2 = c6.lessThanInclusive(c4); + const r3 = c4.lessThanInclusive(c7); + + // Then + expect(r1).toBeTrue(); + expect(r2).toBeFalse(); + expect(r3).toBeTrue(); + }); + }); + + describe('|greaterThan|', () => { + it('should verify will return true if value is greater than provided, otherwise false', () => { + // When + const r1 = c6.greaterThan(c4); + const r2 = c4.greaterThan(c6); + + // Then + expect(r1).toBeTrue(); + expect(r2).toBeFalse(); + }); + }); + + describe('|greaterThanInclusive|', () => { + it('should verify will return true if value is greater than or equal to provided, otherwise false', () => { + // When + const r1 = c6.greaterThanInclusive(c4); + const r2 = c4.greaterThanInclusive(c6); + const r3 = c7.greaterThanInclusive(c4); + + // Then + expect(r1).toBeTrue(); + expect(r2).toBeFalse(); + expect(r3).toBeTrue(); + }); + }); + }); +}); diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/comparable/comparable.impl.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/comparable/comparable.impl.ts new file mode 100644 index 0000000000..e5a485f457 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/comparable/comparable.impl.ts @@ -0,0 +1,119 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import { Comparable } from '../../interfaces'; + +import { CollectionsUtil } from '../../../utils'; + +/** + * ** Comparable. + * + * + */ +export class ComparableImpl implements Comparable { + + /** + * @inheritDoc + */ + public readonly value: T; + + /** + * ** Constructor. + */ + constructor(value: T) { + this.value = value; + } + + /** + * ** Factory method. + */ + static of(value: any): ComparableImpl { + return new ComparableImpl(value); + } + + /** + * @inheritDoc + */ + compare(comparable: Comparable): number { + if (comparable instanceof ComparableImpl) { + const evaluateSecondStatement = () => this.value > comparable.value + ? 1 + : -1; + + return this.value === comparable.value + ? 0 + : evaluateSecondStatement(); + } else { + return -1; + } + } + + /** + * @inheritDoc + */ + isNil(): boolean { + return CollectionsUtil.isNil(this.value); + } + + /** + * @inheritDoc + */ + notNil(): boolean { + return CollectionsUtil.isDefined(this.value); + } + + /** + * @inheritDoc + */ + like(comparable: Comparable): boolean { + return this.compare(comparable) === 0; + } + + /** + * @inheritDoc + */ + equal(comparable: Comparable): boolean { + return this.compare(comparable) === 0; + } + + /** + * @inheritDoc + */ + notEqual(comparable: Comparable): boolean { + return this.compare(comparable) !== 0; + } + + /** + * @inheritDoc + */ + lessThan(comparable: Comparable): boolean { + return this.compare(comparable) < 0; + } + + /** + * @inheritDoc + */ + lessThanInclusive(comparable: Comparable): boolean { + return this.compare(comparable) <= 0; + } + + /** + * @inheritDoc + */ + greaterThan(comparable: Comparable): boolean { + return this.compare(comparable) > 0; + } + + /** + * @inheritDoc + */ + greaterThanInclusive(comparable: Comparable): boolean { + return this.compare(comparable) >= 0; + } +} diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/comparable/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/comparable/index.ts new file mode 100644 index 0000000000..785b6e3731 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/comparable/index.ts @@ -0,0 +1,9 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './comparable.impl'; +export * from './predicates-comparable.impl'; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/comparable/predicates-comparable.impl.spec.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/comparable/predicates-comparable.impl.spec.ts new file mode 100644 index 0000000000..d6647a0008 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/comparable/predicates-comparable.impl.spec.ts @@ -0,0 +1,320 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { CallFake } from '../../../unit-testing'; + +import { Comparable, Predicate } from '../../interfaces'; + +import { PredicatesComparable } from './predicates-comparable.impl'; + +class ComparableStub implements Comparable { + public readonly value: any; + + constructor(value: any) { + this.value = value; + } + + compare(_comparable: Comparable): number { + return 0; + } + + equal(_comparable: Comparable): boolean { + return true; + } + + like(_comparable: Comparable): boolean { + return true; + } + + notEqual(_comparable: Comparable): boolean { + return false; + } + + isNil(): boolean { + return false; + } + + notNil(): boolean { + return true; + } + + greaterThan(_comparable: Comparable): boolean { + return false; + } + + greaterThanInclusive(_comparable: Comparable): boolean { + return false; + } + + lessThan(_comparable: Comparable): boolean { + return false; + } + + lessThanInclusive(_comparable: Comparable): boolean { + return false; + } +} + +class PredicateEqualStub implements Predicate { + comparable: ComparableStub; + + constructor(comparable: ComparableStub) { + this.comparable = comparable; + } + + evaluate(comparable: Comparable): boolean { + return this.comparable.equal(comparable); + } +} + +describe('PredicatesComparable', () => { + let predicate1: Predicate; + let predicate2: Predicate; + let predicate3: Predicate; + + beforeEach(() => { + predicate1 = new PredicateEqualStub(new ComparableStub('A')); + predicate2 = new PredicateEqualStub(new ComparableStub('B')); + predicate3 = new PredicateEqualStub(new ComparableStub('1')); + }); + + it('should verify instance is created', () => { + // When + const instance = new PredicatesComparable(predicate1, predicate2); + + // Then + expect(instance).toBeDefined(); + }); + + it('should verify value is correctly assigned', () => { + // When + const instance = new PredicatesComparable(predicate1, predicate2); + + // Then + expect(instance.value[0]).toBe(predicate1); + expect(instance.value[1]).toBe(predicate2); + }); + + describe('Statics::', () => { + describe('Methods::()', () => { + describe('|of|', () => { + it('should verify factory method will create instance', () => { + // When + const instance = PredicatesComparable.of(predicate1, predicate2); + + // Then + expect(instance).toBeDefined(); + expect(instance).toBeInstanceOf(PredicatesComparable); + expect(instance.value[0]).toBe(predicate1); + expect(instance.value[1]).toBe(predicate2); + }); + }); + }); + }); + + describe('Methods::()', () => { + let predicatesComparable: PredicatesComparable; + let injectedComparable: ComparableStub; + let consoleWarnSpy: jasmine.Spy; + + beforeEach(() => { + predicatesComparable = PredicatesComparable.of(predicate1, predicate2); + injectedComparable = new ComparableStub('C'); + consoleWarnSpy = spyOn(console, 'warn').and.callFake(CallFake); + }); + + describe('|compare|', () => { + it('should verify will return -1 and log warn to console', () => { + // When + const comparison = predicatesComparable.compare(injectedComparable); + + // Then + expect(comparison).toEqual(-1); + expect(consoleWarnSpy).toHaveBeenCalledWith('PredicatesComparable, unsupported comparison.'); + }); + }); + + describe('|isNil|', () => { + it('should verify will return false all the time', () => { + // Given + const c1 = PredicatesComparable.of(null); + const c2 = PredicatesComparable.of(undefined); + const c3 = PredicatesComparable.of(predicate1); + + // When + const r1 = c1.isNil(); + const r2 = c2.isNil(); + const r3 = c3.isNil(); + + // Then + expect(r1).toBeFalse(); + expect(r2).toBeFalse(); + expect(r3).toBeFalse(); + }); + }); + + describe('|notNil|', () => { + it('should verify will return true all the time', () => { + // Given + const c1 = PredicatesComparable.of(null); + const c2 = PredicatesComparable.of(undefined); + const c3 = PredicatesComparable.of(predicate1); + + // When + const r1 = c1.notNil(); + const r2 = c2.notNil(); + const r3 = c3.notNil(); + + // Then + expect(r1).toBeTrue(); + expect(r2).toBeTrue(); + expect(r3).toBeTrue(); + }); + }); + + describe('|like|', () => { + it('should verify will return true if one or more predicates return true, otherwise false', () => { + // Given + const predicate1EvaluateSpy = spyOn(predicate1, 'evaluate').and.returnValues(false, false); + const predicate2EvaluateSpy = spyOn(predicate2, 'evaluate').and.returnValues(true, false); + const predicate3EvaluateSpy = spyOn(predicate3, 'evaluate').and.returnValues(false); + const comparableStub = new ComparableStub('D'); + predicatesComparable = PredicatesComparable.of(predicate1, predicate2, predicate3); + + // When + const r1 = predicatesComparable.like(injectedComparable); + const r2 = predicatesComparable.like(comparableStub); + + // Then + expect(r1).toBeTrue(); + expect(predicate1EvaluateSpy.calls.argsFor(0)).toEqual([injectedComparable]); + expect(predicate2EvaluateSpy.calls.argsFor(0)).toEqual([injectedComparable]); + + expect(r2).toBeFalse(); + expect(predicate1EvaluateSpy.calls.argsFor(1)).toEqual([comparableStub]); + expect(predicate2EvaluateSpy.calls.argsFor(1)).toEqual([comparableStub]); + expect(predicate3EvaluateSpy.calls.argsFor(0)).toEqual([comparableStub]); + + expect(predicate1EvaluateSpy).toHaveBeenCalledTimes(2); + expect(predicate2EvaluateSpy).toHaveBeenCalledTimes(2); + expect(predicate3EvaluateSpy).toHaveBeenCalledTimes(1); + }); + }); + + describe('|equal|', () => { + it('should verify will return true if all predicates return true, otherwise false', () => { + // Given + const predicate1EvaluateSpy = spyOn(predicate1, 'evaluate').and.returnValues(true, false, true); + const predicate2EvaluateSpy = spyOn(predicate2, 'evaluate').and.returnValues(false, true); + const predicate3EvaluateSpy = spyOn(predicate3, 'evaluate').and.returnValues(true); + const comparableStub1 = new ComparableStub('F'); + const comparableStub2 = new ComparableStub('G'); + predicatesComparable = PredicatesComparable.of(predicate1, predicate2, predicate3); + + // When + const r1 = predicatesComparable.equal(injectedComparable); + const r2 = predicatesComparable.equal(comparableStub1); + const r3 = predicatesComparable.equal(comparableStub2); + + // Then + expect(r1).toBeFalse(); + expect(predicate1EvaluateSpy.calls.argsFor(0)).toEqual([injectedComparable]); + expect(predicate2EvaluateSpy.calls.argsFor(0)).toEqual([injectedComparable]); + + expect(r2).toBeFalse(); + expect(predicate1EvaluateSpy.calls.argsFor(1)).toEqual([comparableStub1]); + + expect(r3).toBeTrue(); + expect(predicate1EvaluateSpy.calls.argsFor(2)).toEqual([comparableStub2]); + expect(predicate2EvaluateSpy.calls.argsFor(1)).toEqual([comparableStub2]); + expect(predicate3EvaluateSpy.calls.argsFor(0)).toEqual([comparableStub2]); + + expect(predicate1EvaluateSpy).toHaveBeenCalledTimes(3); + expect(predicate2EvaluateSpy).toHaveBeenCalledTimes(2); + expect(predicate3EvaluateSpy).toHaveBeenCalledTimes(1); + }); + }); + + describe('|notEqual|', () => { + it('should verify will return true if one or more predicates return false, otherwise false', () => { + // Given + const predicate1EvaluateSpy = spyOn(predicate1, 'evaluate').and.returnValues(false, true, true); + const predicate2EvaluateSpy = spyOn(predicate2, 'evaluate').and.returnValues(false, true); + const predicate3EvaluateSpy = spyOn(predicate3, 'evaluate').and.returnValues(true); + const comparableStub1 = new ComparableStub('H'); + const comparableStub2 = new ComparableStub('I'); + predicatesComparable = PredicatesComparable.of(predicate1, predicate2, predicate3); + + // When + const r1 = predicatesComparable.notEqual(injectedComparable); + const r2 = predicatesComparable.notEqual(comparableStub1); + const r3 = predicatesComparable.notEqual(comparableStub2); + + // Then + expect(r1).toBeTrue(); + expect(predicate1EvaluateSpy.calls.argsFor(0)).toEqual([injectedComparable]); + + expect(r2).toBeTrue(); + expect(predicate1EvaluateSpy.calls.argsFor(1)).toEqual([comparableStub1]); + expect(predicate2EvaluateSpy.calls.argsFor(0)).toEqual([comparableStub1]); + + expect(r3).toBeFalse(); + expect(predicate1EvaluateSpy.calls.argsFor(2)).toEqual([comparableStub2]); + expect(predicate2EvaluateSpy.calls.argsFor(1)).toEqual([comparableStub2]); + expect(predicate3EvaluateSpy.calls.argsFor(0)).toEqual([comparableStub2]); + + expect(predicate1EvaluateSpy).toHaveBeenCalledTimes(3); + expect(predicate2EvaluateSpy).toHaveBeenCalledTimes(2); + expect(predicate3EvaluateSpy).toHaveBeenCalledTimes(1); + }); + }); + + describe('|lessThan|', () => { + it('should verify will return -1 and log warn to console', () => { + // When + const comparison = predicatesComparable.lessThan(injectedComparable); + + // Then + expect(comparison).toBeFalse(); + expect(consoleWarnSpy).toHaveBeenCalledWith('PredicatesComparable, unsupported comparison.'); + }); + }); + + describe('|lessThanInclusive|', () => { + it('should verify will return -1 and log warn to console', () => { + // When + const comparison = predicatesComparable.lessThanInclusive(injectedComparable); + + // Then + expect(comparison).toBeFalse(); + expect(consoleWarnSpy).toHaveBeenCalledWith('PredicatesComparable, unsupported comparison.'); + }); + }); + + describe('|greaterThan|', () => { + it('should verify will return -1 and log warn to console', () => { + // When + const comparison = predicatesComparable.greaterThan(injectedComparable); + + // Then + expect(comparison).toBeFalse(); + expect(consoleWarnSpy).toHaveBeenCalledWith('PredicatesComparable, unsupported comparison.'); + }); + }); + + describe('|greaterThanInclusive|', () => { + it('should verify will return -1 and log warn to console', () => { + // When + const comparison = predicatesComparable.greaterThanInclusive(injectedComparable); + + // Then + expect(comparison).toBeFalse(); + expect(consoleWarnSpy).toHaveBeenCalledWith('PredicatesComparable, unsupported comparison.'); + }); + }); + }); +}); diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/comparable/predicates-comparable.impl.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/comparable/predicates-comparable.impl.ts new file mode 100644 index 0000000000..af1f0109c9 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/comparable/predicates-comparable.impl.ts @@ -0,0 +1,114 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { CollectionsUtil } from '../../../utils'; + +import { Comparable, Predicate } from '../../interfaces'; + +export class PredicatesComparable implements Comparable { + + /** + * @inheritDoc + */ + public readonly value: T; + + /** + * ** Constructor. + */ + constructor(...predicates: T) { + this.value = predicates ?? [] as T; + } + + /** + * ** Factory method. + */ + static of(...predicates: Predicate[]): PredicatesComparable { + return new PredicatesComparable(...predicates); + } + + /** + * @inheritDoc + */ + compare(_comparable: Comparable): number { + console.warn('PredicatesComparable, unsupported comparison.'); + + return -1; + } + + /** + * @inheritDoc + */ + isNil(): boolean { + return CollectionsUtil.isNil(this.value); + } + + /** + * @inheritDoc + */ + notNil(): boolean { + return CollectionsUtil.isDefined(this.value); + } + + /** + * @inheritDoc + */ + like(comparable: Comparable): boolean { + return this.value + .some((predicate) => predicate.evaluate(comparable)); + } + + /** + * @inheritDoc + */ + equal(comparable: Comparable): boolean { + return this.value + .every((predicate) => predicate.evaluate(comparable)); + } + + /** + * @inheritDoc + */ + notEqual(comparable: Comparable): boolean { + return !this.value + .every((predicate) => predicate.evaluate(comparable)); + } + + /** + * @inheritDoc + */ + lessThan(_comparable: Comparable): boolean { + return PredicatesComparable._defaultUnsupported(); + } + + /** + * @inheritDoc + */ + lessThanInclusive(_comparable: Comparable): boolean { + return PredicatesComparable._defaultUnsupported(); + } + + /** + * @inheritDoc + */ + greaterThan(_comparable: Comparable): boolean { + return PredicatesComparable._defaultUnsupported(); + } + + /** + * @inheritDoc + */ + greaterThanInclusive(_comparable: Comparable): boolean { + return PredicatesComparable._defaultUnsupported(); + } + + // eslint-disable-next-line @typescript-eslint/member-ordering + private static _defaultUnsupported(): boolean { + console.warn('PredicatesComparable, unsupported comparison.'); + + return false; + } +} diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/compound/and.predicate.spec.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/compound/and.predicate.spec.ts new file mode 100644 index 0000000000..86a7e534eb --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/compound/and.predicate.spec.ts @@ -0,0 +1,120 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { CallFake } from '../../../unit-testing'; + +import { Comparable, Predicate } from '../../interfaces'; + +import { PredicatesComparable } from '../comparable'; + +import { CompoundPredicate } from './base-compound.predicate'; +import { And } from './and.predicate'; + +describe('And', () => { + it('should verify instance is created from one predicate', () => { + // Given + const predicate1: Predicate = { comparable: {} as Comparable, evaluate: CallFake }; + + // When + const instance = new And(predicate1); + + // Then + expect(instance).toBeDefined(); + expect(instance).toBeInstanceOf(CompoundPredicate); + expect(instance.comparable).toBeInstanceOf(PredicatesComparable); + expect(instance.comparable.value[0]).toBe(predicate1); + }); + + it('should verify instance is created from predicates', () => { + // Given + const predicate1: Predicate = { comparable: {} as Comparable, evaluate: CallFake }; + const predicate2: Predicate = { comparable: {} as Comparable, evaluate: CallFake }; + + // When + const instance = new And(predicate1, predicate2); + + // Then + expect(instance).toBeDefined(); + expect(instance).toBeInstanceOf(CompoundPredicate); + expect(instance.comparable).toBeInstanceOf(PredicatesComparable); + expect(instance.comparable.value[0]).toBe(predicate1); + expect(instance.comparable.value[1]).toBe(predicate2); + }); + + it('should verify instance is created from comparable', () => { + // Given + const predicate1: Predicate = { comparable: {} as Comparable, evaluate: CallFake }; + const predicate2: Predicate = { comparable: {} as Comparable, evaluate: CallFake }; + const comparable = new PredicatesComparable(predicate1, predicate2); + + // When + const instance = new And(comparable); + + // Then + expect(instance).toBeDefined(); + expect(instance).toBeInstanceOf(CompoundPredicate); + expect(instance.comparable).toBe(comparable); + }); + + describe('Statics::', () => { + describe('Methods::', () => { + describe('|of|', () => { + it('should verify factory method will create instance from predicates', () => { + // Given + const predicate1: Predicate = { comparable: {} as Comparable, evaluate: CallFake }; + const predicate2: Predicate = { comparable: {} as Comparable, evaluate: CallFake }; + + // When + const instance = And.of(predicate1, predicate2); + + // Then + expect(instance).toBeInstanceOf(And); + expect(instance).toBeInstanceOf(CompoundPredicate); + expect(instance.comparable).toBeInstanceOf(PredicatesComparable); + expect(instance.comparable.value[0]).toBe(predicate1); + expect(instance.comparable.value[1]).toBe(predicate2); + }); + + it('should verify factory method will create instance from comparable', () => { + // Given + const predicate1: Predicate = { comparable: {} as Comparable, evaluate: CallFake }; + const predicate2: Predicate = { comparable: {} as Comparable, evaluate: CallFake }; + const comparable = new PredicatesComparable(predicate1, predicate2); + + // When + const instance = And.of(comparable); + + // Then + expect(instance).toBeInstanceOf(And); + expect(instance).toBeInstanceOf(CompoundPredicate); + expect(instance.comparable).toBe(comparable); + }); + }); + }); + }); + + describe('Methods::', () => { + describe('|evaluate|', () => { + it('should verify will invoke correct methods', () => { + // Given + const comparableEqualSpy = spyOn(PredicatesComparable.prototype, 'equal').and.returnValue(true); + const predicate1: Predicate = { comparable: {} as Comparable, evaluate: CallFake }; + const predicate2: Predicate = { comparable: {} as Comparable, evaluate: CallFake }; + const predicatesComparable = new PredicatesComparable(predicate1, predicate2); + const instance = new And(predicatesComparable); + const standardComparable = {} as Comparable; + + // When + const result = instance.evaluate(standardComparable); + + // Then + expect(result).toBeTrue(); + expect(comparableEqualSpy).toHaveBeenCalledWith(standardComparable); + }); + }); + }); +}); diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/compound/and.predicate.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/compound/and.predicate.ts new file mode 100644 index 0000000000..efb876ecdc --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/compound/and.predicate.ts @@ -0,0 +1,35 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Comparable, Predicate } from '../../interfaces'; + +import { PredicatesComparable } from '../comparable'; + +import { CompoundPredicate } from './base-compound.predicate'; + +export class And extends CompoundPredicate { + + /** + * ** Factory method. + */ + static override of(comparable: PredicatesComparable): And; + static override of(...predicates: Predicate[]): And; + static override of(...values: Predicate[] | [PredicatesComparable]): And { + if (values[0] instanceof PredicatesComparable) { + return new And(values[0]); + } else { + return new And(...values as Predicate[]); + } + } + + /** + * @inheritDoc + */ + evaluate(comparable: Comparable): boolean { + return this.comparable.equal(comparable); + } +} diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/compound/base-compound.predicate.spec.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/compound/base-compound.predicate.spec.ts new file mode 100644 index 0000000000..7153e2ff9c --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/compound/base-compound.predicate.spec.ts @@ -0,0 +1,28 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { CallFake } from '../../../unit-testing'; + +import { Comparable, Predicate } from '../../interfaces'; + +import { CompoundPredicate } from './base-compound.predicate'; + +describe('CompoundPredicate', () => { + describe('Statics::', () => { + describe('|of|', () => { + it('should verify will throw Error', () => { + // Given + const predicate1: Predicate = { comparable: {} as Comparable, evaluate: CallFake }; + const predicate2: Predicate = { comparable: {} as Comparable, evaluate: CallFake }; + + // When/Then + expect(() => CompoundPredicate.of(predicate1, predicate2)) + .toThrowError('Method have to be overridden in Subclasses.'); + }); + }); + }); +}); diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/compound/base-compound.predicate.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/compound/base-compound.predicate.ts new file mode 100644 index 0000000000..40efaf16a4 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/compound/base-compound.predicate.ts @@ -0,0 +1,46 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Comparable, Predicate } from '../../interfaces'; + +import { PredicatesComparable } from '../comparable'; + +export abstract class CompoundPredicate implements Predicate { + /** + * @inheritDoc + */ + readonly comparable: PredicatesComparable; + + /** + * ** Constructor. + */ + constructor(comparable: PredicatesComparable); + constructor(...predicates: Predicate[]); + constructor(...values: Predicate[] | [PredicatesComparable]) { + if (values.length === 1) { + if (values[0] instanceof PredicatesComparable) { + this.comparable = values[0]; + } else { + this.comparable = PredicatesComparable.of(values[0]); + } + } else { + this.comparable = PredicatesComparable.of(...values as Predicate[]); + } + } + + /** + * ** Factory method. + */ + static of(..._args: unknown[]): CompoundPredicate { + throw new Error('Method have to be overridden in Subclasses.'); + } + + /** + * @inheritDoc + */ + abstract evaluate(value: Comparable): boolean; +} diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/compound/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/compound/index.ts new file mode 100644 index 0000000000..45222c6bbf --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/compound/index.ts @@ -0,0 +1,10 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './base-compound.predicate'; +export * from './and.predicate'; +export * from './or.predicate'; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/compound/or.predicate.spec.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/compound/or.predicate.spec.ts new file mode 100644 index 0000000000..a52a52cb98 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/compound/or.predicate.spec.ts @@ -0,0 +1,120 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { CallFake } from '../../../unit-testing'; + +import { Comparable, Predicate } from '../../interfaces'; + +import { PredicatesComparable } from '../comparable'; + +import { CompoundPredicate } from './base-compound.predicate'; +import { Or } from './or.predicate'; + +describe('Or', () => { + it('should verify instance is created from one predicate', () => { + // Given + const predicate1: Predicate = { comparable: {} as Comparable, evaluate: CallFake }; + + // When + const instance = new Or(predicate1); + + // Then + expect(instance).toBeDefined(); + expect(instance).toBeInstanceOf(CompoundPredicate); + expect(instance.comparable).toBeInstanceOf(PredicatesComparable); + expect(instance.comparable.value[0]).toBe(predicate1); + }); + + it('should verify instance is created from predicates', () => { + // Given + const predicate1: Predicate = { comparable: {} as Comparable, evaluate: CallFake }; + const predicate2: Predicate = { comparable: {} as Comparable, evaluate: CallFake }; + + // When + const instance = new Or(predicate1, predicate2); + + // Then + expect(instance).toBeDefined(); + expect(instance).toBeInstanceOf(CompoundPredicate); + expect(instance.comparable).toBeInstanceOf(PredicatesComparable); + expect(instance.comparable.value[0]).toBe(predicate1); + expect(instance.comparable.value[1]).toBe(predicate2); + }); + + it('should verify instance is created from comparable', () => { + // Given + const predicate1: Predicate = { comparable: {} as Comparable, evaluate: CallFake }; + const predicate2: Predicate = { comparable: {} as Comparable, evaluate: CallFake }; + const comparable = new PredicatesComparable(predicate1, predicate2); + + // When + const instance = new Or(comparable); + + // Then + expect(instance).toBeDefined(); + expect(instance).toBeInstanceOf(CompoundPredicate); + expect(instance.comparable).toBe(comparable); + }); + + describe('Statics::', () => { + describe('Methods::', () => { + describe('|of|', () => { + it('should verify factory method will create instance from predicates', () => { + // Given + const predicate1: Predicate = { comparable: {} as Comparable, evaluate: CallFake }; + const predicate2: Predicate = { comparable: {} as Comparable, evaluate: CallFake }; + + // When + const instance = Or.of(predicate1, predicate2); + + // Then + expect(instance).toBeInstanceOf(Or); + expect(instance).toBeInstanceOf(CompoundPredicate); + expect(instance.comparable).toBeInstanceOf(PredicatesComparable); + expect(instance.comparable.value[0]).toBe(predicate1); + expect(instance.comparable.value[1]).toBe(predicate2); + }); + + it('should verify factory method will create instance from comparable', () => { + // Given + const predicate1: Predicate = { comparable: {} as Comparable, evaluate: CallFake }; + const predicate2: Predicate = { comparable: {} as Comparable, evaluate: CallFake }; + const comparable = new PredicatesComparable(predicate1, predicate2); + + // When + const instance = Or.of(comparable); + + // Then + expect(instance).toBeInstanceOf(Or); + expect(instance).toBeInstanceOf(CompoundPredicate); + expect(instance.comparable).toBe(comparable); + }); + }); + }); + }); + + describe('Methods::', () => { + describe('|evaluate|', () => { + it('should verify will invoke correct methods', () => { + // Given + const comparableLikeSpy = spyOn(PredicatesComparable.prototype, 'like').and.returnValue(true); + const predicate1: Predicate = { comparable: {} as Comparable, evaluate: CallFake }; + const predicate2: Predicate = { comparable: {} as Comparable, evaluate: CallFake }; + const predicatesComparable = new PredicatesComparable(predicate1, predicate2); + const instance = new Or(predicatesComparable); + const standardComparable = {} as Comparable; + + // When + const result = instance.evaluate(standardComparable); + + // Then + expect(result).toBeTrue(); + expect(comparableLikeSpy).toHaveBeenCalledWith(standardComparable); + }); + }); + }); +}); diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/compound/or.predicate.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/compound/or.predicate.ts new file mode 100644 index 0000000000..621bfd720b --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/compound/or.predicate.ts @@ -0,0 +1,35 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Comparable, Predicate } from '../../interfaces'; + +import { PredicatesComparable } from '../comparable'; + +import { CompoundPredicate } from './base-compound.predicate'; + +export class Or extends CompoundPredicate { + + /** + * ** Factory method. + */ + static override of(comparable: PredicatesComparable): Or; + static override of(...predicates: Predicate[]): Or; + static override of(...values: Predicate[] | [PredicatesComparable]): Or { + if (values[0] instanceof PredicatesComparable) { + return new Or(values[0]); + } else { + return new Or(...values as Predicate[]); + } + } + + /** + * @inheritDoc + */ + evaluate(comparable: Comparable): boolean { + return this.comparable.like(comparable); + } +} diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/index.ts new file mode 100644 index 0000000000..8b47b57bfd --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/index.ts @@ -0,0 +1,10 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './comparable'; +export * from './compound'; +export * from './simple'; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/public-api.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/public-api.ts new file mode 100644 index 0000000000..ba142389bd --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/public-api.ts @@ -0,0 +1,8 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './index'; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/simple/base-simple.predicate.spec.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/simple/base-simple.predicate.spec.ts new file mode 100644 index 0000000000..69a4f97aad --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/simple/base-simple.predicate.spec.ts @@ -0,0 +1,28 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { CallFake } from '../../../unit-testing'; + +import { Comparable, Predicate } from '../../interfaces'; + +import { SimplePredicate } from './base-simple.predicate'; + +describe('SimplePredicate', () => { + describe('Statics::', () => { + describe('|of|', () => { + it('should verify will throw Error', () => { + // Given + const predicate1: Predicate = { comparable: {} as Comparable, evaluate: CallFake }; + const predicate2: Predicate = { comparable: {} as Comparable, evaluate: CallFake }; + + // When/Then + expect(() => SimplePredicate.of(predicate1, predicate2)) + .toThrowError('Method have to be overridden in Subclasses.'); + }); + }); + }); +}); diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/simple/base-simple.predicate.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/simple/base-simple.predicate.ts new file mode 100644 index 0000000000..ea7676fa72 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/simple/base-simple.predicate.ts @@ -0,0 +1,34 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Comparable, Predicate } from '../../interfaces'; + +export abstract class SimplePredicate implements Predicate { + /** + * @inheritDoc + */ + readonly comparable: T; + + /** + * ** Constructor. + */ + protected constructor(comparable: T) { + this.comparable = comparable; + } + + /** + * ** Factory method. + */ + static of(..._args: unknown[]): SimplePredicate { + throw new Error('Method have to be overridden in Subclasses.'); + } + + /** + * @inheritDoc + */ + abstract evaluate(comparable: T): boolean; +} diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/simple/equal.predicate.spec.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/simple/equal.predicate.spec.ts new file mode 100644 index 0000000000..e9883a0d25 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/simple/equal.predicate.spec.ts @@ -0,0 +1,82 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Comparable } from '../../interfaces'; + +import { ComparableImpl } from '../comparable'; + +import { Equal } from './equal.predicate'; + +describe('Equal', () => { + let v1: string; + let v2: string; + let v3: string; + + let c1: Comparable; + let c2: Comparable; + let c3: Comparable; + + beforeEach(() => { + v1 = 'Taurus'; + v2 = 'Taurus'; + v3 = 'VDK'; + + c1 = ComparableImpl.of(v1); + c2 = ComparableImpl.of(v2); + c3 = ComparableImpl.of(v3); + }); + + it('should verify instance is created', () => { + // When + const p = new Equal(c1); + + // Then + expect(p).toBeDefined(); + }); + + it('should verify value (Comparable) is correctly assigned', () => { + // When + const p = Equal.of(c1); + + // Then + expect(p.comparable).toBe(c1); + }); + + describe('Statics::', () => { + describe('Methods::()', () => { + describe('|of|', () => { + it('should verify factory method will create instance', () => { + // When + const p = Equal.of(c1); + + // Then + expect(p).toBeDefined(); + expect(p).toBeInstanceOf(Equal); + }); + }); + }); + }); + + describe('Methods::()', () => { + describe('|evaluate|', () => { + it('should verify will return true if both comparables have same values, otherwise false', () => { + // Given + const p = Equal.of(c1); + + // When + const r1 = p.evaluate(c2); + const r2 = p.evaluate(c3); + const r3 = p.evaluate(c1); + + // Then + expect(r1).toBeTrue(); + expect(r2).toBeFalse(); + expect(r3).toBeTrue(); + }); + }); + }); +}); diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/simple/equal.predicate.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/simple/equal.predicate.ts new file mode 100644 index 0000000000..2f2183cecb --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/simple/equal.predicate.ts @@ -0,0 +1,38 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Comparable } from '../../interfaces'; + +import { SimplePredicate } from './base-simple.predicate'; + +/** + * ** Equal Predicate that accepts Comparable and make equality evaluation. + * + * + */ +export class Equal extends SimplePredicate { + /** + * ** Constructor. + */ + constructor(comparable: T) { + super(comparable); + } + + /** + * ** Factory method. + */ + static override of(comparable: Comparable): Equal { + return new Equal(comparable); + } + + /** + * @inheritDoc + */ + evaluate(comparable: Comparable): boolean { + return this.comparable.equal(comparable); + } +} diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/simple/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/simple/index.ts new file mode 100644 index 0000000000..5ee54f15ed --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/predicate/simple/index.ts @@ -0,0 +1,9 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './base-simple.predicate'; +export * from './equal.predicate'; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/public-api.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/public-api.ts new file mode 100644 index 0000000000..1188800ed1 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/public-api.ts @@ -0,0 +1,13 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './http/public-api'; +export * from './interfaces/public-api'; +export * from './object/public-api'; +export * from './predicate/public-api'; +export * from './route/public-api'; +export * from './tasks/public-api'; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/route/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/route/index.ts new file mode 100644 index 0000000000..db0bb6637c --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/route/index.ts @@ -0,0 +1,8 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './route.model'; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/route/public-api.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/route/public-api.ts new file mode 100644 index 0000000000..ba142389bd --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/route/public-api.ts @@ -0,0 +1,8 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './index'; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/route/route.model.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/route/route.model.ts new file mode 100644 index 0000000000..68e3ffceec --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/route/route.model.ts @@ -0,0 +1,118 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import { Data, NavigationExtras, Params, Route } from '@angular/router'; + +import { Replacer } from '../interfaces'; + +/** + * ** Interface for Navigate back command. + */ +export interface TaurusNavigateAction { + /** + * ** Path / Path commands where user should be navigated, returned or redirected. + * + * - If path is '$.current' - current loaded path will be use. + * - If path is '$.requested' - requested path will be use. + * - If path is '$.parent' - parent of the current path will be use. + * - Any other string - use at is and assume it as path for navigation (URL path) + * + */ + path: string | '$.current' | '$.requested' | '$.parent'; + + /** + * ** Replacers to tune and finalize navigate back Path / Path commands. + */ + replacers?: Array>; + + /** + * ** Optional query params for navigation action. + */ + queryParams?: Params; + + /** + * ** Optional instruction for queryParams handling. + * + * - If not provided will fallback to default one 'merge'. + * - If provided and its value is null, it won't do any handling for queryParams. + */ + queryParamsHandling?: NavigationExtras['queryParamsHandling']; + + /** + * ** Optional instruction whether to use resolved path or config path. + * + * - TRUE - will use config path, which means use it as it's configured in the routing modules. + * - FALSE - will use resolved path to the current point. + */ + useConfigPath?: boolean; +} + +/** + * ** Taurus Route data with navigate to command. + */ +export interface TaurusRouteNavigateToData { + /** + * ** Field that has Navigate to command. + */ + navigateTo?: TaurusNavigateAction; +} + +/** + * ** Taurus Route data with navigate back command. + */ +export interface TaurusRouteNavigateBackData { + /** + * ** Field that has Navigate back command. + */ + navigateBack?: TaurusNavigateAction; +} + +/** + * ** Taurus Route data with redirect command. + */ +export interface TaurusRouteRedirectData { + /** + * ** Field that has Redirect command. + */ + redirect?: TaurusNavigateAction; +} + +/** + * ** Custom type for Route Data with Generics. + */ +export type TaurusRouteData = Record> = + T + & TaurusRouteNavigateToData + & TaurusRouteNavigateBackData + & TaurusRouteRedirectData + & Data; + +/** + * ** Taurus Route config. + */ +export interface TaurusRoute extends Route { + /** + * @inheritDoc + * + * ** Static data configured per Route. + */ + data?: T; + + /** + * @inheritDoc + * + * ** Children Route configs object. + */ + children?: TaurusRoutes; +} + +/** + * ** Taurus Routes configs. + */ +export type TaurusRoutes = TaurusRoute[]; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/tasks/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/tasks/index.ts new file mode 100644 index 0000000000..a9552dbc58 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/tasks/index.ts @@ -0,0 +1,8 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './task.model'; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/tasks/public-api.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/tasks/public-api.ts new file mode 100644 index 0000000000..ba142389bd --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/tasks/public-api.ts @@ -0,0 +1,8 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './index'; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/tasks/task.model.spec.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/tasks/task.model.spec.ts new file mode 100644 index 0000000000..cf5f85815c --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/tasks/task.model.spec.ts @@ -0,0 +1,56 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { CollectionsUtil } from '../../utils'; + +import { createTaskIdentifier, extractTaskFromIdentifier } from './task.model'; + +describe('Tasks', () => { + describe('|createTaskIdentifier|', () => { + it('should verify will return expected value', () => { + // Given + const task = 'create_entity'; + const dateNowISO = new Date().toISOString(); + const taskIdentifier = `${ task } __ ${ dateNowISO }`; + const dateISOSpy = spyOn(CollectionsUtil, 'dateISO').and.returnValue(dateNowISO); + const interpolateStringSpy = spyOn(CollectionsUtil, 'interpolateString').and.returnValue(taskIdentifier); + const expected = `${ task } __ ${ dateNowISO }`; + + // When + const response = createTaskIdentifier(task); + + // Then + expect(response).toEqual(expected); + expect(dateISOSpy).toHaveBeenCalled(); + // @ts-ignore + expect(interpolateStringSpy).toHaveBeenCalledWith('%s __ %s', task, dateNowISO); + }); + + it('should verify will return value undefined when no task provided', () => { + // When + const response = createTaskIdentifier(null); + + // Then + expect(response).toBeUndefined(); + }); + }); + + describe('|extractTaskFromIdentifier|', () => { + it('should verify will return Task from provided identifier', () => { + // Given + const dateNowISO = new Date().toISOString(); + const taskIdentifier = `delete_entity __ ${ dateNowISO }`; + const expected = 'delete_entity'; + + // When + const response = extractTaskFromIdentifier(taskIdentifier); + + // Then + expect(response).toEqual(expected); + }); + }); +}); diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/common/tasks/task.model.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/tasks/task.model.ts new file mode 100644 index 0000000000..08f450af50 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/common/tasks/task.model.ts @@ -0,0 +1,37 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { CollectionsUtil } from '../../utils'; + +const TASK_IDENTIFIER_SEPARATOR = ' __ '; +const TASK_IDENTIFIER_TEMPLATE = `%s${ TASK_IDENTIFIER_SEPARATOR }%s`; + +/** + * ** Factory for Tasks identifiers. + */ +export const createTaskIdentifier = (task: string) => { + if (CollectionsUtil.isString(task)) { + return CollectionsUtil.interpolateString( + TASK_IDENTIFIER_TEMPLATE, + task, + CollectionsUtil.dateISO() + ); + } + + return undefined; +}; + +/** + * ** Extract Task from Tasks identifiers. + */ +export const extractTaskFromIdentifier = (taskIdentifier: string) => { + if (CollectionsUtil.isString(taskIdentifier)) { + return taskIdentifier.split(TASK_IDENTIFIER_SEPARATOR)[0] as T; + } + + return null; +}; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/css/colors.scss b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/css/colors.scss new file mode 100644 index 0000000000..707e22e936 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/css/colors.scss @@ -0,0 +1,37 @@ +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +$gray1: #747474; +$gray2: #eee; +$gray3: #ddd; +$gray4: #ccc; +$gray5: #8C8C8C; +$black: #000; +$black1: #313131; +$error-red: #e62700; +$error-red-dark-theme: #f54f47; +$dark-text: #adbbc4; +$red2: #c92100; +$ready-green: #318700; +$darkblue: rgb(0, 106, 144); +$lighterBlue: #89CBDF; +$lightblue: #49AFD9; +$darkButtonHoverBlue: #0095D3; +$blue3: #49afd9; +$blue4: #57c7ea; +$blue5: #0079b8; +$inactiveGray: #95A4B2; +$bgcolor: #25333D; +$bgcolorDark: #0f171c; +$bgcolorTextareaDark: #17242b; +$white: #FFF; +$focused-control-border: #6bc1e3; +$dark-theme-text: #acbac3; +$label-danger-color: #a32100; +$label-success-color: #266900; +$label-info-color: #004c8a; +$clr-primary-color: #0072a3; +$clr-primary-color-dark: #4aaed9; +$clr-input-border-grey: #b3b3b3; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/css/screen-sizes.scss b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/css/screen-sizes.scss new file mode 100644 index 0000000000..ca64f13dcf --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/css/screen-sizes.scss @@ -0,0 +1,49 @@ +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +$xxs-width: 321px; + +$xs-width: 543px; +$xs-max: 543px; + +$small-min: 544px; +$small-width: 767px; +$small-max: 767px; + +$medium-min: 768px; +$medium-width: 991px; +$medium-width2: 990px; + +$large-min: 992px; +$large-width: 1199px; + +$xl-min-width: 1600px; +$xxl-min-width: 1919px; + +$min-el-size: 310px; + +.medium-and-up { + @media screen and (max-width: $medium-width2) { + display: none !important; + } +} + +.small-and-up { + @media screen and (max-width: $xs-max) { + display: none !important; + } +} + +.medium-and-down { + @media screen and (min-width: $medium-width) { + display: none !important; + } +} + +.small-and-down { + @media screen and (min-width: $small-width) { + display: none !important; + } +} diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/css/utils.scss b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/css/utils.scss new file mode 100644 index 0000000000..914f818af9 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/css/utils.scss @@ -0,0 +1,39 @@ + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Theme-aware CSS utility + * Usage: Invoke mixin, including custom theme-specific CSS properties, inside braces `{ ... }`. + * Example: + * + * ``` + * .page-example-image { + * width: 100px; + * height: 100px; + * + * @include light-theme { + * background-image: url('icon-light.png'); + * border-color: black; + * } + * @include dark-theme { + * background-image: url('icon-dark.png'); + * border-color: white; + * } + * } + * ``` + */ +@mixin light-theme { + :host-context(body:not([cds-theme])) &, + :host-context(body[cds-theme='light']) & { + @content; + } +} + +@mixin dark-theme { + :host-context(body[cds-theme='dark']) & { + @content; + } +} diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/index.ts new file mode 100644 index 0000000000..eb4cd3584c --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/index.ts @@ -0,0 +1,9 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './ngx-components'; +export * from './ngx-utils'; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/animation-constants.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/animation-constants.ts new file mode 100644 index 0000000000..8654e82ea5 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/animation-constants.ts @@ -0,0 +1,71 @@ +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +// –——— CLARITY ANIMATIONS ————— +// ATOMIC Animations +// primary +export const atomicPrimaryEnterCurve = 'cubic-bezier(0, 1.5, 0.5, 1)'; +export const atomicPrimaryEnterTiming = 200; +export const atomicPrimaryLeaveCurve = 'cubic-bezier(0,.99,0,.99)'; +export const atomicPrimaryLeaveTiming = 200; + +// secondary +export const atomicSecondaryEnterCurve = 'cubic-bezier(0, 1.5, 0.5, 1)'; +export const atomicSecondaryEnterTiming = 400; +export const atomicSecondaryLeaveCurve = 'cubic-bezier(0, 1.5, 0.5, 1)'; +export const atomicSecondaryLeaveTiming = 100; + + +// COMPONENT Animations +// primary +export const componentPrimaryEnterCurve = 'cubic-bezier(0,.99,0,.99)'; +export const componentPrimaryEnterTiming = 400; +export const componentPrimaryLeaveCurve = 'cubic-bezier(0,.99,0,.99)'; +export const componentPrimaryLeaveTiming = 300; + +// PAGE Animations +// primary +export const pagePrimaryEnterCurve = 'cubic-bezier(0,.99,0,.99)'; +export const pagePrimaryEnterTiming = 250; +export const pagePrimaryLeaveCurve = 'cubic-bezier(0,.99,0,.99)'; +export const pagePrimaryLeaveTiming = 200; + + +// PROGRESS Animations +// primary +export const progressPrimaryCurve = 'cubic-bezier(.17,.4,.8,.79)'; +export const progressPrimaryTiming = 790; + +// secondary +export const progressSecondaryCurve = 'cubic-bezier(.34,.01,.39,1)'; +export const progressSecondaryTiming = 200; + +// ICON Animations +// primary +export const linePrimaryEnterCurve = 'linear'; +export const linePrimaryEnterTiming = 250; +export const linePrimaryEnterDelay = 200; + +// secondary +export const lineSecondaryEnterCurve = 'linear'; +export const lineSecondaryEnterTiming = 400; +export const lineSecondaryEnterDelay = 200; + +// –——— NGX ONLY ANIMATIONS ————— +export const DISMISS_ICON_DURATION = 300; +export const DISMISS_ICON_DELAY = 350; +export const DISMISS_ICON_CURVE = 'cubic-bezier(0, 1.2, 0.7, 1)'; + +export const GRADIENT_DURATION = 500; +export const GRADIENT_DELAY = 100; +export const GRADIENT_LEAVE_CURVE = 'cubic-bezier(0, 1.2, 0.7, 1)'; +export const STAGGER_DURATION = 200; + +// used for animation debugging +const ANIMATION_MULTIPLIER = 1; + +export function multiply(value: number) { + return value * ANIMATION_MULTIPLIER; +} diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/copy-to-clipboard-button/copy-to-clipboard-button.component.html b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/copy-to-clipboard-button/copy-to-clipboard-button.component.html new file mode 100644 index 0000000000..449ab76ebe --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/copy-to-clipboard-button/copy-to-clipboard-button.component.html @@ -0,0 +1,51 @@ + + + + + + + + {{tooltip}} + + + + + + {{copyAlert}} + + + diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/copy-to-clipboard-button/copy-to-clipboard-button.component.scss b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/copy-to-clipboard-button/copy-to-clipboard-button.component.scss new file mode 100644 index 0000000000..9bd2f0a806 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/copy-to-clipboard-button/copy-to-clipboard-button.component.scss @@ -0,0 +1,109 @@ +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +span.copy-button { + button { + margin-right: 0px !important; + border: none; + padding: 0; + } +} + +.flip-horizontal-bottom { + -webkit-animation: flip-horizontal-bottom 0.5s cubic-bezier(0.455, 0.030, 0.515, 0.955) both; + animation: flip-horizontal-bottom 0.5s cubic-bezier(0.455, 0.030, 0.515, 0.955) both; +} + +.flip-horizontal-reverse { + -webkit-animation: flip-horizontal-reverse 0.5s cubic-bezier(0.455, 0.030, 0.515, 0.955) both; + animation: flip-horizontal-reverse 0.5s cubic-bezier(0.455, 0.030, 0.515, 0.955) both; +} + +.anim-object { + transform-style: preserve-3d; + display: block; + outline: none; +} + +.face { + position: absolute; + width: 100%; + height: 100%; + backface-visibility: hidden; + border-radius: 3px; + text-align: center; + overflow: hidden; + text-overflow: ellipsis; + transform: rotateX(0deg); +} + +.front { + z-index: 20; + + &.face-label { + position: relative; + } +} + +.back { + z-index: 10; + transform: rotateX(180deg); + &.face-label { + top: 0px; + } +} + +.hide-tooltip { + display: none; +} + +/** + * ---------------------------------------- + * animation flip-horizontal-bottom + * ---------------------------------------- + */ +@-webkit-keyframes flip-horizontal-bottom { + 0% { + -webkit-transform: rotateX(0); + transform: rotateX(0); + } + 100% { + -webkit-transform: rotateX(-180deg); + transform: rotateX(-180deg); + } +} + +@keyframes flip-horizontal-bottom { + 0% { + -webkit-transform: rotateX(0); + transform: rotateX(0); + } + 100% { + -webkit-transform: rotateX(-180deg); + transform: rotateX(-180deg); + } +} + +@-webkit-keyframes flip-horizontal-reverse { + 0% { + -webkit-transform: rotateX(-180deg); + transform: rotateX(-180deg); + } + 100% { + -webkit-transform: rotateX(0); + transform: rotateX(0); + } +} + +@keyframes flip-horizontal-reverse { + 0% { + -webkit-transform: rotateX(-180deg); + transform: rotateX(-180deg); + } + 100% { + -webkit-transform: rotateX(0); + transform: rotateX(0); + } +} diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/copy-to-clipboard-button/copy-to-clipboard-button.component.spec.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/copy-to-clipboard-button/copy-to-clipboard-button.component.spec.ts new file mode 100644 index 0000000000..a1a034fac8 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/copy-to-clipboard-button/copy-to-clipboard-button.component.spec.ts @@ -0,0 +1,100 @@ + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { ViewChild, Component } from '@angular/core'; +import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing'; + +import { ClarityModule } from "@clr/angular"; + +import { VdkCopyToClipboardButtonComponent } from './copy-to-clipboard-button.component'; + +describe('CopyToClipboardButtonComponent', () => { + let component: VdkCopyToClipboardButtonComponent; + let hostComponent: TestHostComponent; + let fixture: ComponentFixture; + let element: any; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + ClarityModule, + ], + declarations: [ + TestHostComponent, + VdkCopyToClipboardButtonComponent + ] + }); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(TestHostComponent); + element = fixture.debugElement.nativeElement; + hostComponent = fixture.componentInstance; + component = hostComponent.component; + }); + + describe('when no btnLabel specified', () => { + beforeEach(() => { + fixture.detectChanges(); + }); + + it('should initialize size properly', () => { + component.ngOnInit(); + expect(component.bounds).toBe('22px'); + component.size = 24; + component.ngOnInit(); + expect(component.bounds).toBe('30px'); + }); + + it('should copy the value to the clipboard', () => { + spyOn(component, 'copyToClipboard'); + component.doCopy(); + expect(component.copyToClipboard).toHaveBeenCalledWith('Test string'); + }); + + it('should set the proper "copied" status', fakeAsync(() => { + component.doCopy(); + expect(component.copied).toBeTruthy(); + tick(1500); + expect(component.copied).toBeFalsy(); + })); + + // it('should show the copy icon', () => { + // expect(element).toHaveRendered('.copy-button'); + // }); + + // it('should not show the copy button with text', () => { + // expect(element).not.toHaveRendered('button.btn.btn-outline'); + // }); + }); + + // describe('when btnLabel specified', () => { + // beforeEach(() => { + // component.btnLabel = 'COPY TO CLIPBOARD'; + // fixture.detectChanges(); + // }); + // + // it('should show the copy button with text', () => { + // expect(element).toHaveRendered('button.btn.btn-outline'); + // + // let label = element.querySelector('button .face-label span'); + // expect(label.innerText.trim()).toEqual('COPY TO CLIPBOARD'); + // }); + // }); +}); + +@Component({ + template: ` + + + ` +}) +class TestHostComponent { + @ViewChild(VdkCopyToClipboardButtonComponent, { static: true }) component: VdkCopyToClipboardButtonComponent; + + public value = 'Test string'; +} diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/copy-to-clipboard-button/copy-to-clipboard-button.component.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/copy-to-clipboard-button/copy-to-clipboard-button.component.ts new file mode 100644 index 0000000000..7230cfaaff --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/copy-to-clipboard-button/copy-to-clipboard-button.component.ts @@ -0,0 +1,126 @@ +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + ElementRef, + HostBinding, + Output, + EventEmitter, + Component, + OnInit, + Input, + ViewChild, +} from '@angular/core'; +import { VdkSimpleTranslateService } from '../../ngx-utils'; + +import { TRANSLATIONS } from './copy-to-clipboard-button.l10n'; + +const SHOW_CHECKBOX_TIMEOUT = 1500; + +@Component({ + selector: 'vdk-copy-to-clipboard-button', + templateUrl: './copy-to-clipboard-button.component.html', + styleUrls: ['./copy-to-clipboard-button.component.scss'] +}) +export class VdkCopyToClipboardButtonComponent implements OnInit { + @ViewChild('area', { read: ElementRef, static: false }) area: ElementRef; + + @HostBinding('class') @Input('class') classList: string = ''; + + @Input() value: string; + @Input() ariaLabel: string = ""; + @Input() copyAlert: string; + @Input() size = 16; + @Input() tooltip = ''; + @Input() btnLabel = ''; // if no label specified, show the normal copy icon + @Input() btnClasses = ['btn-outline']; // if no label specified, show the normal copy icon + @Input() disabled: boolean = false; + @Input() tooltipDirection = 'top-left'; + + @Output() copyClick = new EventEmitter(); + + private firstLoad = true; // show correct icon first + + btnClassesToApply: string; + bounds: string; + hasProjectedContent: boolean = false; + isSafari: boolean = /^((?!chrome|android).)*safari/i.test(navigator.userAgent); + copied: boolean = false; + + constructor(private el: ElementRef, public translateService: VdkSimpleTranslateService) { + this.translateService.loadTranslationsForComponent('copy-to-clipboard-button', TRANSLATIONS); + } + + ngOnInit() { + this.bounds = (this.size + 6) + 'px'; + + this.hasProjectedContent = this.el.nativeElement.innerText.trim(); + + this.calculateClassesToApply(); + this.copyAlert = this.copyAlert || this.translateService.translate('copy-to-clipboard-button.copied'); + } + + calculateClassesToApply() { + let classes: Array = []; + + if (!this.btnLabel.length) { + classes.push('icon-btn'); + } + + if (this.btnLabel.length) { + classes = classes.concat(this.btnClasses); + } + + if (this.disabled) { + classes.push('disabled'); + } + + this.btnClassesToApply = classes.join(' ') + ' ' + this.classList; + } + + ngOnChanges() { + this.calculateClassesToApply(); + } + + copyToClipboard(val: string) { + let myWindow: any = window; + + let onCopy = (e: ClipboardEvent) => { + e.preventDefault(); + + if (e.clipboardData) { + e.clipboardData.setData('text/plain', val); + } else if (myWindow.clipboardData) { + myWindow.clipboardData.setData('Text', val); + } + + myWindow.removeEventListener('copy', onCopy); + }; + + if (this.isSafari) { + this.area.nativeElement.value = val; + this.area.nativeElement.select(); + navigator.clipboard?.writeText(val); + } + + myWindow.addEventListener('copy', onCopy); + + if (myWindow.clipboardData && myWindow.clipboardData.setData) { + myWindow.clipboardData.setData('Text', val); + } else { + document.execCommand('copy'); + } + } + + doCopy() { + this.copyToClipboard(this.value); + this.copyClick.emit(); + this.firstLoad = false; + this.copied = true; + setTimeout(() => { + this.copied = false; + }, SHOW_CHECKBOX_TIMEOUT); + } +} diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/copy-to-clipboard-button/copy-to-clipboard-button.l10n.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/copy-to-clipboard-button/copy-to-clipboard-button.l10n.ts new file mode 100644 index 0000000000..c8ef3cefc9 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/copy-to-clipboard-button/copy-to-clipboard-button.l10n.ts @@ -0,0 +1,43 @@ +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +export const TRANSLATIONS = { + 'en': { + 'copied': 'Copied' + }, + 'de': { + 'copied': 'Kopiert' + }, + 'es': { + 'copied': 'Copiado' + }, + 'fr': { + 'copied': 'Copié' + }, + 'ja': { + 'copied': 'コピー' + }, + 'ko': { + 'copied': '복사' + }, + 'zh_TW': { + 'copied': '已复制' + }, + 'zh_CN': { + 'copied': '已復制' + }, + 'it': { + 'copied': 'Copiato' + }, + 'nl': { + 'copied': 'Gekopieerd' + }, + 'pt': { + 'copied': 'Copiado' + }, + 'ru': { + 'copied': 'Скопировано' + }, +}; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/copy-to-clipboard-button/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/copy-to-clipboard-button/index.ts new file mode 100644 index 0000000000..35266a99a0 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/copy-to-clipboard-button/index.ts @@ -0,0 +1,16 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Type } from "@angular/core"; + +import { VdkCopyToClipboardButtonComponent } from './copy-to-clipboard-button.component'; + +export * from "./copy-to-clipboard-button.component"; + +export const COPY_TO_CLIPBPOARD_BUTTON_DIRECTIVES: Type[] = [ + VdkCopyToClipboardButtonComponent, +]; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/copy-to-clipboard-button/public-api.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/copy-to-clipboard-button/public-api.ts new file mode 100644 index 0000000000..ba142389bd --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/copy-to-clipboard-button/public-api.ts @@ -0,0 +1,8 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './index'; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/empty-state-placeholder/empty-state-placeholder.component.html b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/empty-state-placeholder/empty-state-placeholder.component.html new file mode 100644 index 0000000000..d1aa65c6d4 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/empty-state-placeholder/empty-state-placeholder.component.html @@ -0,0 +1,22 @@ + + + + + + + +
+ {{title}} +
+ +
+ {{description}} +
+ + diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/empty-state-placeholder/empty-state-placeholder.component.scss b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/empty-state-placeholder/empty-state-placeholder.component.scss new file mode 100644 index 0000000000..7c65c41eae --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/empty-state-placeholder/empty-state-placeholder.component.scss @@ -0,0 +1,49 @@ +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +$text-dark: #919FA8; +$text-light: #67747d; + +:host { + display: flex; + align-items: center; + flex-direction: column; + margin: 1rem 0; + + cds-icon { + fill: #CCC; + } + + .empty-placeholder-heading { + font-size: 18px; + color: $text-light; + margin-top: 18px; + line-height: 48px; + font-weight: 200; + } + + .empty-placeholder-description { + margin-top: 0; + font-size: 13px; + color: $text-light; + margin-bottom: 18px; + font-weight: 200; + } +} + + +:host ::ng-deep button:last-child { + margin-right: 0 !important; +} + +:host ::ng-deep button.btn-link { + margin-top: 0 !important; +} + +:host-context(.dark) { + h2, h3 { + color: $text-dark; + } +} diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/empty-state-placeholder/empty-state-placeholder.component.spec.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/empty-state-placeholder/empty-state-placeholder.component.spec.ts new file mode 100644 index 0000000000..674a0ed32a --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/empty-state-placeholder/empty-state-placeholder.component.spec.ts @@ -0,0 +1,4 @@ +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/empty-state-placeholder/empty-state-placeholder.component.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/empty-state-placeholder/empty-state-placeholder.component.ts new file mode 100644 index 0000000000..e96e5240a5 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/empty-state-placeholder/empty-state-placeholder.component.ts @@ -0,0 +1,20 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Component, Input } from '@angular/core'; + +@Component({ + selector: 'vdk-empty-state-placeholder', + templateUrl: './empty-state-placeholder.component.html', + styleUrls: ["./empty-state-placeholder.component.scss"] +}) +export class VdkEmptyStatePlaceholderComponent { + @Input('title') title: string; + @Input() icon: string; + @Input() description: string; + @Input() headingLevel = 2; +} diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/empty-state-placeholder/empty-state-placeholder.module.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/empty-state-placeholder/empty-state-placeholder.module.ts new file mode 100644 index 0000000000..d0d6729040 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/empty-state-placeholder/empty-state-placeholder.module.ts @@ -0,0 +1,27 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { CommonModule } from "@angular/common"; +import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from "@angular/core"; +import { VdkEmptyStatePlaceholderComponent } from "./empty-state-placeholder.component"; + +@NgModule({ + imports: [ + CommonModule, + ], + declarations: [ + VdkEmptyStatePlaceholderComponent, + ], + exports: [ + VdkEmptyStatePlaceholderComponent, + ], + schemas: [ + CUSTOM_ELEMENTS_SCHEMA, + ], +}) +export class VdkEmptyStatePlaceholderModule { +} diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/empty-state-placeholder/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/empty-state-placeholder/index.ts new file mode 100644 index 0000000000..2877606b57 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/empty-state-placeholder/index.ts @@ -0,0 +1,9 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from "./empty-state-placeholder.component"; +export * from "./empty-state-placeholder.module"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/empty-state-placeholder/public-api.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/empty-state-placeholder/public-api.ts new file mode 100644 index 0000000000..ba142389bd --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/empty-state-placeholder/public-api.ts @@ -0,0 +1,8 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './index'; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/form-section-container/form-section-container.component.html b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/form-section-container/form-section-container.component.html new file mode 100644 index 0000000000..366023234d --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/form-section-container/form-section-container.component.html @@ -0,0 +1,52 @@ + + + +
+ + + + +
+ +
+ + +
+ +
+ + +
+ + +
diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/form-section-container/form-section-container.component.scss b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/form-section-container/form-section-container.component.scss new file mode 100644 index 0000000000..4e07a40b5f --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/form-section-container/form-section-container.component.scss @@ -0,0 +1,17 @@ + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +:host ::ng-deep { + + .section-title { + display: inline-block; + } +} + +.csp-edit-button { + margin: 0 0; + cursor: pointer; +} diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/form-section-container/form-section-container.component.spec.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/form-section-container/form-section-container.component.spec.ts new file mode 100644 index 0000000000..6606b01a94 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/form-section-container/form-section-container.component.spec.ts @@ -0,0 +1,192 @@ +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { VdkFormSectionContainerComponent } from './form-section-container.component'; +import { ComponentFixture, TestBed, waitForAsync } from "@angular/core/testing"; +import { Component, DebugElement, ViewChild } from "@angular/core"; +import { ReactiveFormsModule, FormBuilder, FormGroup, FormControl } from '@angular/forms'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { By } from "@angular/platform-browser"; + +import { ClarityModule } from "@clr/angular"; + +import { FORM_STATE, VDKFormState } from './form-section-container.component'; +import { VdkFormSectionComponent } from '../form-section'; +import { VdkSimpleTranslateModule, VdkSimpleTranslateService } from "../../ngx-utils"; + +let fixture: ComponentFixture; +let hostComp: TestHostComponent; +let page: PageObject; +let el: Element; + +class PageObject { + fixtureElement: DebugElement; + + constructor() { + } + + populateLocators(): void { + this.fixtureElement = fixture.debugElement.query(By.css("vdk-form-section-container")); + } + + getReadOnlySectionElements(): NodeListOf { + return el.querySelectorAll(".form-section-readonly"); + } + + getEditSectionElements(): NodeListOf { + return el.querySelectorAll(".form-section-edit"); + } + + setInputValue(inputName: string, inputValue: any) { + hostComp.editProfileForm.controls[inputName].setValue(inputValue); + hostComp.editProfileForm.controls[inputName].markAsDirty(); + } + + getSaveBtnSpan(): HTMLElement | undefined { + const span = fixture.debugElement.queryAll(By.css('.csp-save-button span')); + if (span && span.length > 0) { + return span[0].nativeElement; + } + return undefined; + } +} + +@Component({ + template: ` +
+ + + +
+ Title +
+ +
+
+
+ +
+
+ firstName +
+
+ +
+ +
+
+
+ +
+
+ +
+
+
+
+
` + +}) +class TestHostComponent { + @ViewChild(VdkFormSectionContainerComponent, { static: false }) child: VdkFormSectionContainerComponent; + formState: VDKFormState; + + editProfileForm = new FormGroup({ + "firstName": new FormControl(), + }); + +} + +describe("Form Section Container Component", () => { + function createComponent() { + fixture = TestBed.createComponent(TestHostComponent); + hostComp = fixture.componentInstance; + el = fixture.nativeElement; + fixture.detectChanges(); + page = new PageObject(); + page.populateLocators(); + } + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [ + ReactiveFormsModule, + VdkSimpleTranslateModule, + BrowserAnimationsModule, + ClarityModule, + ], + providers: [ + VdkSimpleTranslateService, + FormBuilder + ], + declarations: [ + TestHostComponent, + VdkFormSectionComponent, + VdkFormSectionContainerComponent, + ] + }); + + createComponent(); + fixture.detectChanges(); + })); + + it("should project .title content correctly", () => { + expect(el.querySelector('.section-title').textContent).toContain('Title'); + }); + + it("should readOnly and Edit sections to be toggled on Edit", () => { + expect(page.getReadOnlySectionElements().length).toBe(1, "Readonly Section shown initially"); + expect(page.getEditSectionElements().length).toBe(0, "Edit section hidden initially"); + expect(el.querySelectorAll(".csp-edit-button").length).toBe(1, "Edit button shown initially"); + const editButton: HTMLInputElement = el.querySelector(".csp-edit-button"); + editButton.click(); + fixture.detectChanges(); + + expect(page.getReadOnlySectionElements().length).toBe(0, "Readonly section hidden on Edit button"); + expect(page.getEditSectionElements().length).toBe(1, "Edit section shown on Edit button"); + }); + + it("should readOnly and Edit sections to be toggled on Cancel", () => { + const editButton: HTMLInputElement = el.querySelector(".csp-edit-button"); + editButton.click(); + fixture.detectChanges(); + + expect(el.querySelectorAll(".csp-cancel-button").length).toBe(1, "Cancel button to be shown"); + const cancelButton: HTMLInputElement = el.querySelector(".csp-cancel-button"); + cancelButton.click(); + fixture.detectChanges(); + + expect(page.getReadOnlySectionElements().length).toBe(1, "Readonly section shown on Cancel"); + expect(page.getEditSectionElements().length).toBe(0, "Edit section hidden on Cancel"); + }); + + + it("should show Readonly and Edit section after save", () => { + const editButton: HTMLInputElement = el.querySelector(".csp-edit-button"); + editButton.click(); + fixture.detectChanges(); + + page.setInputValue('firstName', 'firstName'); + expect(el.querySelectorAll(".csp-save-button").length).toBe(1, "Save button shown"); + const saveButton: HTMLInputElement = el.querySelector(".csp-save-button"); + expect(page.getSaveBtnSpan().innerText.trim()).toBe('Save', "save btn shown"); + saveButton.click(); + fixture.detectChanges(); + + expect(saveButton.disabled).toEqual(true, "Save to be disabled"); + hostComp.formState = new VDKFormState(FORM_STATE.CAN_EDIT); + fixture.detectChanges(); + + expect(page.getReadOnlySectionElements().length).toBe(1, "Readonly Section shown"); + expect(page.getEditSectionElements().length).toBe(0, "Edit Section hidden"); + }); + +}); diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/form-section-container/form-section-container.component.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/form-section-container/form-section-container.component.ts new file mode 100644 index 0000000000..f736632652 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/form-section-container/form-section-container.component.ts @@ -0,0 +1,170 @@ +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + Component, + Output, + EventEmitter, + Input, + Host, + Optional, + ChangeDetectionStrategy, + ChangeDetectorRef, + ViewChild, + ElementRef +} from "@angular/core"; +import { FormGroupDirective } from "@angular/forms"; +import { ClrLoadingState } from "@clr/angular"; + +export enum FORM_STATE { + VIEW, + CAN_EDIT, + EDIT, + ERROR, + SUBMIT +} +export class VDKFormState { + state: FORM_STATE; + + /** + * Optional. The section with this name identifier will be excluded from the state change. + */ + emittingSection: string; + + /** + * Optional. + * All the sections in the array will change its state. + * All the others will be excluded if this array is not empty. + */ + sectionsToInclude: string[]; + + constructor( _state: FORM_STATE, _sectionsToInclude?: string[], _emittingSection?: string ) { + this.state = _state; + this.sectionsToInclude = _sectionsToInclude ? _sectionsToInclude : []; + this.emittingSection = _emittingSection; + } +} + +@Component({ + selector: "vdk-form-section-container", + templateUrl: "./form-section-container.component.html", + styleUrls: ['form-section-container.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class VdkFormSectionContainerComponent { + FORM_STATE = FORM_STATE; //used in the template + _sectionState: FORM_STATE = FORM_STATE.CAN_EDIT; + ClrLoadingState = ClrLoadingState; + stopInitialFocus = true; + + @Input() canEditSection = true; //set to false if the section is readonly + @Input() isSubmitEnabled: boolean; //controls the Submit(Save) button state + @Input() sectionName: string; //unique section identifier + @Input() editBtn = "Edit"; //Edit button text + @Input() cancelBtn = "Cancel"; //Cancel button text + @Input() saveBtn = "Save"; //Save button text + @Input() editBtnAriaLabel = "Edit"; //Edit button text + @Input() cancelBtnAriaLabel = "Cancel"; //Cancel button text + @Input() saveBtnAriaLabel = "Save"; //Save button text + + @Input('formState') set formState(_formState:VDKFormState) { + if (!_formState) { + return; + } + + if ( (_formState.emittingSection && _formState.emittingSection !== this.sectionName) || + ( !_formState.emittingSection && _formState.sectionsToInclude.length === 0 ) || + (_formState.sectionsToInclude.some(name => name === this.sectionName)) ) { + + //on ERROR set EDIT state only on submitted section + //to the rest of sections restore CAN_EDIT state + if (_formState.state === FORM_STATE.ERROR) { + if (this._sectionState === FORM_STATE.SUBMIT) { + this._sectionState = FORM_STATE.EDIT; + //put it in a microtask(make it asynchronous) to not violate detection run + //and avoid error for Expression changed after check + Promise.resolve(null).then(() => { + if (this.cspForm) { + this.cspForm.form.enable(); + } else { + this.enableForm.emit(); + } + }); + } else { + this._sectionState = FORM_STATE.CAN_EDIT; + } + } else { + this.changeSectionState(_formState.state); + } + } + } + + @Output() formStateChange = new EventEmitter(); + @Output() sectionStateChange = new EventEmitter(); + + //Events that are used when no formGroup is found in parent component + @Output() submitForm = new EventEmitter(); + @Output() disableForm = new EventEmitter(); + @Output() enableForm = new EventEmitter(); + + @ViewChild('editButton') editButtonEl: ElementRef; + + constructor(@Optional() @Host() private cspForm: FormGroupDirective, private cdr: ChangeDetectorRef) {} + + showEditBtn() { + return this._sectionState === FORM_STATE.CAN_EDIT && this.canEditSection; + } + + showSaveBtn() { + return this._sectionState === FORM_STATE.EDIT; + } + + clickEdit() { + this.formStateChange.emit(new VDKFormState(FORM_STATE.CAN_EDIT, [], this.sectionName)); + if (this.cspForm) { + this.cspForm.form.enable(); + } else { + this.enableForm.emit(); + } + this.changeSectionState(FORM_STATE.EDIT); + } + + clickCancel() { + this.changeSectionState(FORM_STATE.CAN_EDIT); + } + + // @ts-ignore + clickSave(): boolean { + this.formStateChange.emit(new VDKFormState(FORM_STATE.VIEW, [], this.sectionName)); + this.changeSectionState(FORM_STATE.SUBMIT); + if (this.cspForm) { + this.cspForm.form.disable(); + this.cspForm.onSubmit(this.cspForm.value); + this.cspForm.form.markAsPristine(); + // cancel submitting the form since cspForm.onSubmit has been called above. + // solves a specific issue in Firefox where onSubmit was called twice. + return false; + } else { + this.disableForm.emit(); + this.submitForm.emit(); + } + } + + changeSectionState(_sectionState: FORM_STATE) { + if (_sectionState !== this._sectionState) { + this.sectionStateChange.emit(new VDKFormState(_sectionState, [], this.sectionName)); + this._sectionState = _sectionState; + } + } + + focusEdit() { + if (this.editButtonEl && this._sectionState === FORM_STATE.CAN_EDIT && !this.stopInitialFocus) { + this.editButtonEl.nativeElement.focus(); + } + if (this.stopInitialFocus) { + this.stopInitialFocus = false; + } + } +} diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/form-section-container/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/form-section-container/index.ts new file mode 100644 index 0000000000..dc87efc967 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/form-section-container/index.ts @@ -0,0 +1,16 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Type } from "@angular/core"; + +import { VdkFormSectionContainerComponent } from './form-section-container.component'; + +export * from "./form-section-container.component"; + +export const FORM_SECTION_CONTAINER_DIRECTIVES: Type[] = [ + VdkFormSectionContainerComponent, +]; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/form-section-container/public-api.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/form-section-container/public-api.ts new file mode 100644 index 0000000000..ba142389bd --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/form-section-container/public-api.ts @@ -0,0 +1,8 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './index'; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/form-section/form-section.component.html b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/form-section/form-section.component.html new file mode 100644 index 0000000000..aea32aea2f --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/form-section/form-section.component.html @@ -0,0 +1,18 @@ + + +
+
+
+
+
+ +
+ +
+ +
+
+
diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/form-section/form-section.component.scss b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/form-section/form-section.component.scss new file mode 100644 index 0000000000..8ff5bd4b2d --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/form-section/form-section.component.scss @@ -0,0 +1,39 @@ +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +.vdk-form-container { + display: flex; + + .vdk-vertical-line { + margin-top: 20px; + margin-bottom: 5px; + border-left: 3px solid #007cbb; + } + + .vdk-form-content { + + .form-header { + padding-bottom: 15px; + } + + display: flex; + flex-direction: column; + width: 100%; + } + + .form-header { + color:#000; + font-size:18px; + font-weight:200; + margin-block-start:24px; + margin-top:24px; + } + + :host-context(.dark) { + .form-header { + color: #eaedf0; + } + } +} diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/form-section/form-section.component.spec.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/form-section/form-section.component.spec.ts new file mode 100644 index 0000000000..267942c3b1 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/form-section/form-section.component.spec.ts @@ -0,0 +1,78 @@ +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { ComponentFixture, TestBed, waitForAsync } from "@angular/core/testing"; +import { Component, ViewChild } from "@angular/core"; + +import { ClarityModule } from "@clr/angular"; + +import { VdkFormSectionComponent } from "./form-section.component"; +import { BrowserAnimationsModule } from "@angular/platform-browser/animations"; + +@Component({ + template: ` + + Test header + Test content + + + ` +}) +class TestHostComponent { + @ViewChild(VdkFormSectionComponent, { static: true }) child: VdkFormSectionComponent; + focused: boolean = false; +} + +describe("VdkFormSectionComponent", () => { + + let fixture: ComponentFixture; + let comp: VdkFormSectionComponent; + let el: any; + + function createComponent() { + fixture = TestBed.createComponent(TestHostComponent); + comp = fixture.componentInstance.child; + el = fixture.debugElement.nativeElement; + fixture.detectChanges(); + } + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [ + BrowserAnimationsModule, + ClarityModule, + ], + providers: [], + declarations: [ + TestHostComponent, + VdkFormSectionComponent + ] + }); + + createComponent(); + fixture.detectChanges(); + })); + + it("should project .form-section-header content correctly", () => { + const headers: NodeListOf = el.querySelectorAll("vdk-form-section span.form-section-header"); + expect(headers.length).toBe(1); + }); + + it("should project .form-section-content content correctly", () => { + const content: NodeListOf = el.querySelectorAll("vdk-form-section span.form-section-content"); + expect(content.length).toBe(1); + }); + + it("should project .form-section-footer content only when focused and then change edit state", () => { + expect(comp.getFormState()).toBe("normal"); + const footer: NodeListOf = el.querySelectorAll("vdk-form-section span.form-section-footer"); + expect(footer.length).toBe(0); + fixture.componentInstance.focused = true; + fixture.detectChanges(); + expect(comp.getFormState()).toBe("edit"); + const footers: NodeListOf = el.querySelectorAll("vdk-form-section span.form-section-footer"); + expect(footers.length).toBe(1); + }); +}); diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/form-section/form-section.component.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/form-section/form-section.component.ts new file mode 100644 index 0000000000..e05e30ed44 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/form-section/form-section.component.ts @@ -0,0 +1,60 @@ +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Component, EventEmitter, Input, Output } from "@angular/core"; +import { animate, state, style, transition, trigger } from "@angular/animations"; + + +@Component({ + selector: 'vdk-form-section', + styleUrls: ['form-section.component.scss'], + templateUrl: 'form-section.component.html', + animations: [ + trigger('customFormState', [ + state('edit', style({ + opacity: 1, + width: '3px', + 'margin-right': '24px' + })), + state('normal', style({ + opacity: 0, + width: '0px', + 'margin-right': '0px' + + })), + transition('normal => edit', [ + animate('300ms ease-in-out') + ]), + transition('edit => normal', [ + animate('100ms ease-in-out') + ]) + ]), + trigger('footerState', [ + transition(':enter', [ + style({ opacity: 0, height: '0' }), + animate('0.1s 0.2s ease-in-out', style({ opacity: 1, height: '*'})), + ]), + transition(':leave', [ + animate('0.1s', style({ opacity: 0, height: '0' })) + ]) + ]) + ] +}) + +export class VdkFormSectionComponent { + @Input() + focused: boolean = false; + + @Output() + animationDone = new EventEmitter(); + + getFormState() { + return this.focused ? 'edit' : 'normal'; + } + + emitAnimationDone() { + this.animationDone.emit(); + } +} diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/form-section/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/form-section/index.ts new file mode 100644 index 0000000000..7b17da1cbf --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/form-section/index.ts @@ -0,0 +1,16 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Type } from "@angular/core"; + +import { VdkFormSectionComponent } from './form-section.component'; + +export * from "./form-section.component"; + +export const FORM_SECTION_DIRECTIVES: Type[] = [ + VdkFormSectionComponent, +]; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/form-section/public-api.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/form-section/public-api.ts new file mode 100644 index 0000000000..4996b4ff68 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/form-section/public-api.ts @@ -0,0 +1,9 @@ + + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './index'; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/index.ts new file mode 100644 index 0000000000..4bc9385849 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/index.ts @@ -0,0 +1,16 @@ + + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from "./copy-to-clipboard-button/index"; +export * from "./form-section/index"; +export * from "./form-section-container/index"; +export * from "./empty-state-placeholder/index"; +export * from "./toast/index"; +export * from "./search/index"; + +export * from "./vdk-ngx-components.module"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/public-api.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/public-api.ts new file mode 100644 index 0000000000..ba142389bd --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/public-api.ts @@ -0,0 +1,8 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './index'; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/search/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/search/index.ts new file mode 100644 index 0000000000..cfb0e28f62 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/search/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './search.component' +export { VdkSearchModule } from './search.module'; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/search/public-api.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/search/public-api.ts new file mode 100644 index 0000000000..ba142389bd --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/search/public-api.ts @@ -0,0 +1,8 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './index'; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/search/search.component.html b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/search/search.component.html new file mode 100644 index 0000000000..c33a6cad4c --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/search/search.component.html @@ -0,0 +1,53 @@ + + +
+ + + + + + + + + + + + + + {{helperText}} +
diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/search/search.component.scss b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/search/search.component.scss new file mode 100644 index 0000000000..1daa216d82 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/search/search.component.scss @@ -0,0 +1,95 @@ +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +:host { + width: 200px; + display: block; +} + +.search-container { + display: flex; + flex-direction: column; + position: relative; + width: 100%; + margin-bottom: 0; + margin: 0.5rem 0; + + &.disabled { + cursor: not-allowed; + + * { + pointer-events: none; + } + + input { + color: #888888; + } + + cds-icon[shape="search"] { + fill: #888888; + } + + .clear-search-btn { + cds-icon[shape="times-circle"] { + fill: #888888; + } + } + } + + cds-icon[shape="search"] { + height: 18px; + position: absolute; + top: 3px; + left: 0; + } + + &.focused { + cds-icon[shape="search"] { + fill: var(--clr-forms-focused-color); + stroke: var(--clr-forms-focused-color); + stroke-width: 0.75px; + } + } + + .clear-search-btn { + position: absolute; + top: -1px; + right: 0; + padding: 0; + border: transparent; + background-color: transparent; + + cds-icon[shape="times-circle"] { + fill: #565656; + opacity: 0.8; + &:hover { + cursor: pointer; + opacity: 1; + } + } + } + + .btn-link { + position: absolute; + right: 0; + top: -6px; + margin: 0; + } + + input { + padding: 0 24px; + flex: 1 1 24px; + } +} + +:host-context(.dark) { + .clear-search-btn { + color: #d3d3d3; + + cds-icon[shape="times-circle"] { + fill: #acbac3; + } + } +} diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/search/search.component.spec.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/search/search.component.spec.ts new file mode 100644 index 0000000000..af4d373825 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/search/search.component.spec.ts @@ -0,0 +1,229 @@ +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { ComponentFixture, TestBed, ComponentFixtureAutoDetect, waitForAsync } from '@angular/core/testing'; +import { ReactiveFormsModule } from "@angular/forms"; + +import { ClarityModule } from "@clr/angular"; +import { VdkSearchComponent } from "./search.component"; + +let comp: VdkSearchComponent; +let fixture: ComponentFixture; +let page: PageObject; +export class PageObject { + + getSearchInputElement(): HTMLInputElement { + return fixture.nativeElement.querySelector(`[data-test-id="search-input"]`); + } + + getSearchButtonElement(): HTMLInputElement { + return fixture.nativeElement.querySelector(`[data-test-id="search-button"]`); + } + + getClearSearchBtn(): HTMLButtonElement { + return fixture.nativeElement.querySelector(`[data-test-id="clear-search-btn"]`); + } + + getSearchIcon(): HTMLElement { + return fixture.nativeElement.querySelector(`[data-test-id="search-icon"]`); + } + + getHelperText(): HTMLElement { + return fixture.nativeElement.querySelector('[data-test-id="search-results-text"]') + } +} + +describe('VdkSearchComponent', () => { + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + ClarityModule, + ReactiveFormsModule + ], + declarations: [ + VdkSearchComponent + ], + providers: [ + { provide: ComponentFixtureAutoDetect, useValue: true }, + ] + }); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(VdkSearchComponent); + comp = fixture.componentInstance; + fixture.detectChanges(); + page = new PageObject(); + }); + + it('should create', () => { + expect(comp).toBeTruthy(); + expect(page.getSearchInputElement()).toBeTruthy(); + expect(page.getSearchIcon()).toBeTruthy(); + expect(page.getClearSearchBtn()).toBeFalsy(); + }); + + it('should show and hide "clear search button"', waitForAsync(() => { + const searchQuery = 'test'; + expect(page.getClearSearchBtn()).toBeFalsy(); + page.getSearchInputElement().value = searchQuery; + page.getSearchInputElement().dispatchEvent(new Event('input')); + fixture.detectChanges(); + fixture.whenStable().then(() => { + expect(comp.searchQuery.value).toBe(searchQuery, 'searchQuery input value is not correct'); + expect(comp.searchQueryValue).toBe(searchQuery, 'searchQueryValue property is not correct'); + expect(page.getClearSearchBtn()).toBeTruthy(); + page.getClearSearchBtn().click(); + fixture.detectChanges(); + return fixture.whenStable(); + }) + .then(() => { + expect(page.getClearSearchBtn()).toBeFalsy(); + }); + })); + + it('should input show correct data', waitForAsync(() => { + const searchQuery = 'test'; + page.getSearchInputElement().value = searchQuery; + page.getSearchInputElement().dispatchEvent(new Event('input')); + fixture.detectChanges(); + fixture.whenStable().then(() => { + expect(page.getSearchInputElement().value).toBe(searchQuery); + page.getClearSearchBtn().click(); + fixture.detectChanges(); + return fixture.whenStable(); + }) + .then(() => { + expect(comp.searchQueryValue).toBe(''); + }); + })); + + it('should emit correct value', waitForAsync(() => { + const searchQuery = 'test'; + const searchSpy = spyOn(comp.search, 'emit').and.callThrough(); + page.getSearchInputElement().value = searchQuery; + page.getSearchInputElement().dispatchEvent(new Event('input')); + fixture.detectChanges(); + fixture.whenStable().then(() => { + expect(searchSpy).toHaveBeenCalledWith(searchQuery); + }); + })); + + + it('should show helper text with results when resultCount is a number', () => { + fixture.componentInstance.helperText = `Over 9999 results`; + fixture.detectChanges(); + expect(page.getHelperText()).not.toBeNull(); + }); + + it('should not show helper text with results when resultCount is not used', () => { + expect(page.getHelperText()).toBeNull(); + }); + + describe('when in "Manual Search" mode', () => { + beforeEach(() => { + comp.showSearchButton = true; + }); + + it( + 'should show "SEARCH" button disabled when more than one char is entered but it is not above the min limit', + waitForAsync(async () => { + comp.searchTermMinimalLength = 3; + + fixture.detectChanges(); + + expect(page.getSearchButtonElement()).toBeNull(); + + page.getSearchInputElement().value = 't'; + page.getSearchInputElement().dispatchEvent(new Event('input')); + fixture.detectChanges(); + await fixture.whenStable(); + + let searchButton = page.getSearchButtonElement(); + expect(searchButton).toBeTruthy(); + expect(searchButton.disabled).toBeTruthy(); + + page.getSearchInputElement().value = 'two'; + page.getSearchInputElement().dispatchEvent(new Event('input')); + fixture.detectChanges(); + await fixture.whenStable(); + + searchButton = page.getSearchButtonElement(); + expect(searchButton).toBeTruthy(); + expect(searchButton.disabled).toBeFalsy(); + }) + ); + + it( + 'should emit value ONLY upon clicking "Enter" or "SEARCH" and should hide the latter', + waitForAsync(async () => { + const searchQuery = 'test'; + const searchSpy = spyOn(comp.search, 'emit').and.callThrough(); + page.getSearchInputElement().value = searchQuery; + page.getSearchInputElement().dispatchEvent(new Event('input')); + fixture.detectChanges(); + await fixture.whenStable(); + + expect(page.getClearSearchBtn()).toBeFalsy(); + expect(searchSpy).not.toHaveBeenCalledWith(searchQuery); + + const searchButton = page.getSearchButtonElement(); + searchButton.click(); + + await fixture.whenStable(); + + expect(searchSpy).toHaveBeenCalledWith(searchQuery); + + expect(page.getClearSearchBtn()).toBeTruthy(); + + page.getSearchInputElement().dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter' })); + + const newSearchQuery = searchQuery + '123'; + page.getSearchInputElement().value = newSearchQuery; + page.getSearchInputElement().dispatchEvent(new Event('input')); + fixture.detectChanges(); + await fixture.whenStable(); + + expect(searchSpy).toHaveBeenCalledWith(newSearchQuery); + }) + ); + + it( + 'should clear input upon clicking "X" button', + waitForAsync(async () => { + const searchQuery = 'test'; + const searchSpy = spyOn(comp.search, 'emit').and.callThrough(); + + page.getSearchInputElement().value = searchQuery; + page.getSearchInputElement().dispatchEvent(new Event('input')); + fixture.detectChanges(); + await fixture.whenStable(); + + expect(searchSpy).not.toHaveBeenCalledWith(searchQuery); + + let searchButton = page.getSearchButtonElement(); + searchButton.click(); + await fixture.whenStable(); + + expect(searchSpy).toHaveBeenCalledWith(searchQuery); + + searchButton = page.getSearchButtonElement(); + expect(searchButton).toBeNull(); + + const clearSearchButton = page.getClearSearchBtn(); + expect(clearSearchButton).toBeTruthy(); + + clearSearchButton.click(); + fixture.detectChanges(); + await fixture.whenStable(); + + expect(comp.searchQueryValue).toEqual(''); + expect(searchSpy).toHaveBeenCalledWith(''); + }) + ); + }); + +}); diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/search/search.component.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/search/search.component.ts new file mode 100644 index 0000000000..64cbe04d00 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/search/search.component.ts @@ -0,0 +1,136 @@ +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Component, ElementRef, EventEmitter, Input, Output, ViewChild } from "@angular/core"; +import { FormControl } from "@angular/forms"; + +import { BehaviorSubject, combineLatest, Subject, Subscription } from "rxjs"; +import { debounceTime } from "rxjs/operators"; + +const defaultSearchInputPadding = 24; + +@Component({ + selector: "vdk-search", + templateUrl: "./search.component.html", + styleUrls: ["./search.component.scss"], +}) +export class VdkSearchComponent { + searchInputPaddingRight = `${defaultSearchInputPadding}px`; + + private _disabled: boolean = false; + public get disabled(): boolean { + return this._disabled; + } + @Input('disabled') + public set disabled(value: boolean) { + this._disabled = value; + + if (value) { + this.searchQuery?.disable({ emitEvent: false }); + } else { + this.searchQuery?.enable({ emitEvent: false }); + } + } + + @Input() searchQueryValue: string = ''; + @Input() clearSearchTitle: string = 'Clear Search'; + @Input('placeholder') + set placeholder(pass: string) { + this.finalPlaceholder = pass ? pass : "Search"; + } + + @Input() helperText: string = ''; + @Input() debounceTime: number = 100; + @Input() searchTermMinimalLength: number = 2; + @Input() showSearchButton = false; + @Input() searchButtonText: string = "Search"; + @Input() searchButtonAriaLabelText: string = "Search"; + @Input() searchAriaLabelText: string; + + @Output() search: EventEmitter = new EventEmitter(); + + @ViewChild('searchButton', { read: ElementRef }) + searchButton?: ElementRef; + isSeachButtonVisible: boolean; + + private triggerSearch$: Subject = new BehaviorSubject(undefined); + private hasSearchBeenTriggeredManually: boolean; + + public searchQuery: FormControl; + public searchQuerySub: Subscription; + public focused: boolean = false; + public finalPlaceholder: string = "Search"; + + ngOnInit() { + this.searchQuery = new FormControl(this.searchQueryValue); + + this.searchQuerySub = combineLatest([this.searchQuery.valueChanges, this.triggerSearch$]) + .pipe(debounceTime(this.debounceTime)) + .subscribe(([query]) => { + const queryLength = query.length; + query = query.trim(); + this.searchQueryValue = query; + + // not emit search event if it hasn't been inputted something different from whitespace + if (this.searchQueryValue.length === 0 && + queryLength !== 0) { + return; + } + + // Make sure that the 'Search' button will be visible in 'Manual Search' mode upon every change. + this.isSeachButtonVisible = this.showSearchButton; + + const shouldNotifyForQueryChange = !this.showSearchButton || this.hasSearchBeenTriggeredManually; + const inputHasMinLengthOrIsCleared = + this.searchQueryValue.length >= this.searchTermMinimalLength || this.searchQueryValue.length === 0; + if (shouldNotifyForQueryChange && inputHasMinLengthOrIsCleared) { + // If we are about to notify that the search term has changed replace 'Search' button with the `X` one. + this.isSeachButtonVisible = false; + this.search.emit(query); + } + + this.hasSearchBeenTriggeredManually = false; + this.computeSearchInputPadding(); + }); + } + + ngOnDestroy(): void { + if (this.searchQuerySub) { + this.searchQuerySub.unsubscribe(); + } + } + + clearSearch(): void { + this.searchQuery.setValue(''); + if (this.showSearchButton) { + this.triggerSearch(); + } + } + + handleKeyDown(event: KeyboardEvent): void { + if (event.key === 'Enter') { + this.triggerSearch(); + } + } + + triggerSearch(): void { + this.hasSearchBeenTriggeredManually = true; + this.triggerSearch$.next(); + } + + private computeSearchInputPadding(): void { + if (this.showSearchButton && this.isSeachButtonVisible) { + + // Wait for the search button to be rendered as changes 'shouldShowSearchButton' might not be applied in the template. + // Useful especially after the first rendering. + setTimeout(() => { + this.searchInputPaddingRight = + Math.round(this.searchButton?.nativeElement.clientWidth || defaultSearchInputPadding) + 'px'; + }); + } else { + this.searchInputPaddingRight = `${defaultSearchInputPadding}px`; + } + } +} diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/search/search.module.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/search/search.module.ts new file mode 100644 index 0000000000..e705868a39 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/search/search.module.ts @@ -0,0 +1,27 @@ +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from "@angular/core"; + +import { CommonModule } from "@angular/common"; +import {VdkSearchComponent} from "./search.component"; +import {ReactiveFormsModule} from "@angular/forms"; + +@NgModule({ + declarations: [ + VdkSearchComponent + ], + imports: [ + CommonModule, + ReactiveFormsModule + ], + exports: [ + VdkSearchComponent + ], + schemas: [ + CUSTOM_ELEMENTS_SCHEMA, + ], +}) +export class VdkSearchModule {} diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/toast/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/toast/index.ts new file mode 100644 index 0000000000..6a02961b9a --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/toast/index.ts @@ -0,0 +1,18 @@ + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Type } from "@angular/core"; +import { VdkToastComponent } from "./toast.component"; +import { VdkToastContainerComponent } from "./toast-container.component"; + +export * from "./toast.component"; +export * from "./toast-container.component"; +export * from "./toast.model"; + +export const TOAST_DIRECTIVES: Type[] = [ + VdkToastContainerComponent, + VdkToastComponent, +]; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/toast/public-api.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/toast/public-api.ts new file mode 100644 index 0000000000..ba142389bd --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/toast/public-api.ts @@ -0,0 +1,8 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './index'; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/toast/toast-container.component.html b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/toast/toast-container.component.html new file mode 100644 index 0000000000..de5c2d9785 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/toast/toast-container.component.html @@ -0,0 +1,8 @@ + + +
+ +
diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/toast/toast-container.component.scss b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/toast/toast-container.component.scss new file mode 100644 index 0000000000..f3f25c1356 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/toast/toast-container.component.scss @@ -0,0 +1,32 @@ +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +@import "../../css/screen-sizes.scss"; + +:host { + display: block; + z-index: 1040; + position: absolute; + right: 0; + width: 414px; + opacity: 1.0; + margin-top: 18px; + + @media screen and(max-width: $xs-width) { + max-width: $min-el-size; + right: -12px; + } +} + +.toast-container { + margin-right: 0; + > div { + margin-left: 0px; + margin-right: -12px; + } + display: flex; + justify-content: flex-end; + flex-direction: column; +} diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/toast/toast-container.component.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/toast/toast-container.component.ts new file mode 100644 index 0000000000..6036f5fea3 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/toast/toast-container.component.ts @@ -0,0 +1,48 @@ +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { HostBinding, Input, Component } from '@angular/core'; +import { + trigger, + transition, + query, + animateChild, + stagger, +} from '@angular/animations'; + +import { + multiply, + STAGGER_DURATION, +} from "../animation-constants"; + +import { VdkToastComponent } from './toast.component'; + +@Component({ + selector: "vdk-toast-container", + templateUrl: "./toast-container.component.html", + styleUrls: ["./toast-container.component.scss"], + animations:[ + trigger('toastContainer', [ + transition(':enter', [ + query('@launchToast', [ + stagger(`${multiply(STAGGER_DURATION)}ms`, animateChild()) + ], {optional: true}), + ]), + transition(':leave', [ + query('@launchToast', [ + animateChild() + ], {optional:true}) + ]), + ]) + ] +}) +export class VdkToastContainerComponent { + @Input() topOffset: number = 0; + + @HostBinding('style.top') + get top(): string { + return (60 + this.topOffset) + 'px'; + } +} diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/toast/toast.component.html b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/toast/toast.component.html new file mode 100644 index 0000000000..2acec5f5e3 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/toast/toast.component.html @@ -0,0 +1,89 @@ + + +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+ + + + +
+
+ + + + +
+ + +
+
+
diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/toast/toast.component.scss b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/toast/toast.component.scss new file mode 100644 index 0000000000..0f24bc0dd7 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/toast/toast.component.scss @@ -0,0 +1,318 @@ +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +@import "../../css/colors"; +@import "../../css/screen-sizes.scss"; + +:host { + @media screen and(max-width: $xs-width) { + max-width: 75vw; + } + + ::ng-deep .toast-description { + font-size: 1.0em; + color: #ADBBC4; + margin-top: 3px; + line-height: 18px; + + -webkit-line-clamp: 4; + -moz-line-clamp: 4; + display: -webkit-box; + overflow: hidden; + text-overflow: ellipsis; + // https://github.com/postcss/autoprefixer/issues/776 + /* autoprefixer: ignore next */ + -webkit-box-orient: vertical; + max-height: 70px; + } + + ::ng-deep .toast-link { + font-size: 0.85em; + color: #89CBDF; + display: block; + text-transform: uppercase; + font-family: 'Metropolis-Semibold'; + margin-top: 12px; + float: right; + letter-spacing: 1px; + } + + ::ng-deep .toast-date { + color: #ADBBC4; + font-size: 11px; + margin-left: 6px; + position: relative; + top: 1px; + } + + ::ng-deep .toast-title { + font-weight: 500; + color: #E9ECEF; + font-size: 14px; + margin-top: 0; + line-height: 24px; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + max-width: 240px; + } + +} + +.toast { + display: flex; + flex-direction: row; + max-width: 100%; + min-width: $min-el-size; + background-color: $bgcolor; + border-radius: 3px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.4); + position: relative; + right: 18px; + padding: 12px; + margin: 0 0 12px 0; + + // animation - steady state of component + transform: scale(1, 1); + transform-origin: 100% 0; +} + +.dismiss-bg { + .dismiss { + cds-icon[shape="times"] { + fill: #ADBBC4; + margin-top: -8px; + margin-left: 1px; + } + } + background: transparent; + width: 30px; + height: 30px; + display: inline-block; + position: absolute; + right: 8px; + top: 8px; + cursor: pointer; + border-radius: 100%; + border: none; + + &:hover { + background-color: rgba(0, 0, 0, 0.15); + } +} + + +.dismiss { + position: absolute; + left: 6px; + top: 6px; +} + + +.gradient { + width: 35%; + height: 100%; + background: linear-gradient(-90deg, rgba($bgcolor, 0) -14.71%, rgba($bgcolor, 1) 46.71%); + display: inline-block; + position: absolute; + top: 0px; + transform: scale(0, 1); + transform-origin: 0 100%; +} + +.toast-button { + font-weight: 500; + color: #89CBDF; + text-transform: uppercase; + font-size: 11px; + letter-spacing: 1px; + margin-top: 12px; +} + +.toast-button.secondary { + margin-right: 6px; +} + +button.toast-button { + background: none; + border: none; +} + +.toast-title-container { + display: flex; + flex-direction: row; +} + +div.clr-row { + padding-bottom: 10px; +} + +div.content { + padding-left: 12px; + width: 100%; +} + +div.content-wrapper { + flex: 1; + display: flex; +} + +div.button-container { + text-align: right; + + button { + cursor: pointer; + + &:hover { + color: $darkButtonHoverBlue; + } + } +} + +// icons +.icon { + transform: scale(1); + transform-origin: center; +} + +.icon-container { + margin: -6px; +} + +// info +#info-icon-outline, #info-icon-line { + fill: none; + stroke: #0095D3; + stroke-miterlimit: 10; + stroke-width: 2; +} + +#info-icon-line { + stroke-linecap: round; + stroke-dasharray: 16; + stroke-dashoffset: 0; + animation-fill-mode: forwards; +} + +#info-icon-dot { + transform-origin: 50% 42%; + fill: #0095D3; +} + +//warning +#warn-icon-line, #warn-icon-triangle, #warn-icon-dot { + fill: #ffef5ff1; +} + +#warn-icon-dot { + transform-origin: 50% 54%; +} + +// error +#error-icon-outline, #error-icon-line { + fill: none; + stroke: #F54F47; + stroke-miterlimit: 10; + stroke-width: 2; +} + +#error-icon-line { + stroke-linecap: round; + stroke-width: 2.65; + stroke-dasharray: 7; + animation-fill-mode: forwards; + stroke-dashoffset: 0; +} + +#error-icon-dot { + transform-origin: 50% 54%; + fill: #F54F47; +} + +// success icon +.checkmark { + fill: none; + stroke: #FFFFFF; + stroke-linecap: round; + stroke-miterlimit: 10; + stroke-width: 1.9px; + stroke-dasharray: 31.386688232421875; + animation-fill-mode: forwards; +} + +:host-context(.dark) { + .toast { + background-color: $bgcolorDark; + } + + .gradient { + background: linear-gradient(-90deg, rgba($bgcolorDark, 0) -14.71%, rgba($bgcolorDark, 1) 46.71%); + } + + ::ng-deep .toast-description { + color: #A9B6BE; + } + + ::ng-deep .toast-button { + color: #49AFD9; + + &:hover { + color: #0095D3 ; + } + } + + ::ng-deep .toast-date { + color: #A9B6BE; + } + + .dismiss-bg:hover { + background-color: rgba(255,255,255,.1); + } +} + +// MUTED styles +:host-context(.muted) { + ::ng-deep .toast-title, + ::ng-deep .toast-date, + ::ng-deep .toast-description, + ::ng-deep .button-container button { + color: $inactiveGray; + } + + .info-bg, + .error-bg { + fill: $inactiveGray; + } + + .info-dot { + fill: $bgcolor; + } + + .info-bg, + .info-dot, + .info-line-vertical, + .info-line-horizontal, + .error-bg, + .error-line, + .error-dot { + stroke: $bgcolor; + } + + :host-context(.dark) { + .info-bg, + .info-dot, + .info-line-vertical, + .info-line-horizontal, + .error-bg, + .error-line, + .error-dot { + stroke: $bgcolorDark; + } + + .info-dot { + fill: $bgcolorDark; + } + } +} diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/toast/toast.component.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/toast/toast.component.ts new file mode 100644 index 0000000000..8b6358ff34 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/toast/toast.component.ts @@ -0,0 +1,302 @@ +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + ElementRef, + NgZone, + Component, + Input, + Output, + EventEmitter, + HostListener, + Optional +} from "@angular/core"; +import { + trigger, + group, + style, + animate, + transition, + query, + animateChild, + keyframes, +} from '@angular/animations'; + +import { timer } from "rxjs"; +import { take } from "rxjs/operators"; + +import { VmwToastType } from "./toast.model"; +import { TRANSLATIONS } from './toast.l10n'; +import { VdkSimpleTranslateService } from "../../ngx-utils"; + +import { + multiply, + componentPrimaryEnterCurve, + componentPrimaryEnterTiming, + componentPrimaryLeaveCurve, + componentPrimaryLeaveTiming, + linePrimaryEnterCurve, + linePrimaryEnterTiming, + linePrimaryEnterDelay, + lineSecondaryEnterCurve, + lineSecondaryEnterTiming, + lineSecondaryEnterDelay, + DISMISS_ICON_DELAY, + DISMISS_ICON_DURATION, + DISMISS_ICON_CURVE, + GRADIENT_DURATION, + GRADIENT_DELAY, + GRADIENT_LEAVE_CURVE, +} from "../animation-constants"; + +const AUTODISMISS_TIMEOUT_SECONDS = 6; +const TRACKED_TAG = { + "A": true, + "BUTTON": true +} + +@Component({ + selector: "vdk-toast", + templateUrl: "./toast.component.html", + styleUrls: ["./toast.component.scss"], + animations: [ + trigger('launchToast', [ + transition(':enter', [ + // toast parent element animation + group([ + style({ + transform: 'translateX(48px) scale(0, 1)' + }), + animate(`${multiply(componentPrimaryEnterTiming)}ms ${componentPrimaryEnterCurve}`, style({ + transform: 'translateX(0) scale(1, 1)' + })), + + // use optional: true for if/else elements + query('.checkmark', animateChild(), {optional: true}), + query('#info-icon-dot', animateChild(), {optional: true}), + query('#info-icon-line', animateChild(), {optional: true}), + query('#warn-icon-dot', animateChild(), {optional: true}), + query('#warn-icon-line', animateChild(), {optional: true}), + query('#error-icon-dot', animateChild(), {optional: true}), + query('#error-icon-line', animateChild(), {optional: true}), + query('.gradient', animateChild()), + query('.dismiss', animateChild(), { optional: true }), + ]), + ]), + + // START LEAVE ANIMATION + // ':leave' is a default state for ngIf and ngFor, doesn't need to be predefined + transition(':leave', [ + group([ + style({ + transform: 'translateX(0px) scale(1, 1)', + marginTop: '*', + }), + + // use query self to be able to group the animation on the current element + query(':self', [ + animate(`${multiply(componentPrimaryLeaveTiming)}ms ${componentPrimaryLeaveCurve}`, style({ + transform: 'translateX(18px) scale(0, 1)' + })), + + animate(`${multiply(componentPrimaryLeaveTiming)}ms ${componentPrimaryLeaveCurve}`, style({ + marginTop: '-{{height}}px' + })), + ]), + + query('.toast-description, .toast-title, .icon, .button-container, .dismiss-bg, .dismiss, .toast-date', [ + animate(`${multiply(10)}ms`, style({ + opacity: '0' + })), + ]), + + ]), + ], { + params: { + height: 0 + } + }), + // end launchToast + ]), + + + // info icon animation + trigger('infoLine', [ + transition('* => *', [ + animate(`${multiply(linePrimaryEnterTiming)}ms ${multiply(linePrimaryEnterDelay)}ms ${linePrimaryEnterCurve}`, keyframes([ + style({ strokeDashoffset: '16', offset: 0 }), + style({ strokeDashoffset: '0', offset: 1.0}), + ])) + ]), + ]), + trigger('infoDot', [ + transition('* => *', [ + style({ + transform: 'scale(0)' + }), + animate(`${multiply(lineSecondaryEnterTiming)}ms ${multiply(lineSecondaryEnterDelay)}ms ${lineSecondaryEnterCurve}`, style({ + transform: 'scale(1)' + })) + ]), + ]), + + // error icon animation + trigger('errorLine', [ + transition('* => *', [ + animate(`${multiply(linePrimaryEnterTiming)}ms ${multiply(linePrimaryEnterDelay)}ms ${linePrimaryEnterCurve}`, keyframes([ + style({ strokeDashoffset: '7.919999599456787', offset: 0 }), + style({ strokeDashoffset: '0', offset: 1.0}), + ])) + ]), + ]), + trigger('errorDot', [ + transition('* => *', [ + style({ + transform: 'scale(0)' + }), + animate(`${multiply(lineSecondaryEnterTiming)}ms ${multiply(lineSecondaryEnterDelay)}ms ${lineSecondaryEnterCurve}`, style({ + transform: 'scale(1)' + })) + ]), + ]), + + //warning icon animation + trigger('warnLine', [ + transition('* => *', [ + animate(`${multiply(linePrimaryEnterTiming)}ms ${multiply(linePrimaryEnterDelay)}ms ${linePrimaryEnterCurve}`, keyframes([ + style({ strokeDashoffset: '7.919999599456787', offset: 0 }), + style({ strokeDashoffset: '0', offset: 1.0}), + ])) + ]), + ]), + trigger('warnDot', [ + transition('* => *', [ + style({ + transform: 'scale(0)' + }), + animate(`${multiply(lineSecondaryEnterTiming)}ms ${multiply(lineSecondaryEnterDelay)}ms ${lineSecondaryEnterCurve}`, style({ + transform: 'scale(1)' + })) + ]), + ]), + + // success icon animation + trigger('checkmarkLine', [ + transition('* => *', [ + // css keyframe animation + animate(`${multiply(linePrimaryEnterTiming)}ms ${multiply(linePrimaryEnterDelay)}ms ${linePrimaryEnterCurve}`, keyframes([ + style({ strokeDashoffset: '31.386688232421875', offset: 0 }), + style({ strokeDashoffset: '0', offset: 1.0}), + ])) + ]), + ]), + + // moving the gradient offview + trigger('gradientMove', [ + transition('* => *', [ + style({ + transform: 'scale(1, 1)' + }), + animate(`${multiply(GRADIENT_DURATION)}ms ${multiply(GRADIENT_DELAY)}ms ${GRADIENT_LEAVE_CURVE}`, style({ + transform: 'scale(0, 1)' + })) + ]), + ]), + + // fade in the dismiss icon + trigger('dismissIconVisible', [ + transition('* => *', [ + style({ + opacity: '0' + }), + animate(`${multiply(DISMISS_ICON_DURATION)}ms ${multiply(DISMISS_ICON_DELAY)}ms ${DISMISS_ICON_CURVE}`, style({ + opacity: '1' + })) + ]), + ]), + ] + +}) +export class VdkToastComponent { + public mouseover = false; + public focused = false; + + @Input() type: VmwToastType = VmwToastType.INFO; + @Input() primaryButtonText: string; + @Input() secondaryButtonText: string; + @Input() dismissible: boolean = true; + @Input() timeoutSeconds: number = AUTODISMISS_TIMEOUT_SECONDS; + + @Output() dismissed = new EventEmitter(); + @Output() primaryButtonClick = new EventEmitter(); + @Output() secondaryButtonClick = new EventEmitter(); + + readonly VmwToastType = VmwToastType; + disableAutoDismiss: boolean = false; + height: number; + animate = true; + + constructor( + private element: ElementRef, + private ngZone: NgZone, + public translateService: VdkSimpleTranslateService, + // @Optional() private segmentService, + ) { + this.translateService.loadTranslationsForComponent('toast', TRANSLATIONS); + } + + ngOnInit() { + this.setUpTimer(); + } + + @HostListener('click', ['$event']) + trackClicks(event: any) { + return; + } + + mouseOver(over: boolean) { + // If the user moves their mouse over the snack, disable auto-dismiss + this.disableAutoDismiss = over; + } + + focus(focused: boolean) { + this.disableAutoDismiss = focused; + } + + get loaded() { + return { + value: this.animate, + params: { + height: this.element.nativeElement.clientHeight + } + }; + } + + dismiss(userDismissed: boolean = false) { + this.animate = false; + + // before we tell the app to remove the toast, give the leave animation + // some time to run... + timer(multiply(componentPrimaryLeaveTiming + 200)).pipe(take(1)).subscribe(() => { + this.dismissed.emit(); + }); + } + + private setUpTimer() { + if (this.timeoutSeconds > 0) { + this.ngZone.runOutsideAngular(() => { + timer(this.timeoutSeconds * multiply(1000)).pipe(take(1)).subscribe(() => { + this.ngZone.run(() => { + if (this.disableAutoDismiss) { + this.setUpTimer(); + return; + } + this.dismiss(); + }); + }); + }); + } + } +} diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/toast/toast.l10n.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/toast/toast.l10n.ts new file mode 100644 index 0000000000..d54ac54fbe --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/toast/toast.l10n.ts @@ -0,0 +1,79 @@ +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +export const TRANSLATIONS = { + 'en': { + 'dismiss-notification': 'Dismiss notification', + 'success-icon': 'Success Icon', + 'info-icon': 'Information Icon', + 'failure-icon': 'Failure Icon', + }, + 'es': { + 'dismiss-notification': 'Descartar notificación', + 'success-icon': 'Icono de éxito', + 'failure-icon': 'Icono de error', + 'info-icon': 'Icono de información', + }, + 'de': { + 'dismiss-notification': 'Benachrichtigung verwerfen', + 'success-icon': 'Symbol „Erfolgreich“', + 'failure-icon': 'Fehlersymbol', + 'info-icon': 'Informationssymbol', + }, + 'fr': { + 'dismiss-notification': 'Ignorer la notification', + 'success-icon': 'Icône de réussite', + 'failure-icon': "Icône d'échec", + 'info-icon': "Icône d'informations", + }, + 'ja': { + 'dismiss-notification': '通知を破棄', + 'success-icon': '成功アイコン', + 'failure-icon': '障害アイコン', + 'info-icon': '情報アイコン', + }, + 'ko': { + 'dismiss-notification': '알림 해제', + 'success-icon': '성공 아이콘', + 'failure-icon': '실패 아이콘', + 'info-icon': '정보 아이콘', + }, + 'zh_TW': { + 'dismiss-notification': '關閉通知', + 'success-icon': '成功图标', + 'failure-icon': '失敗圖示', + 'info-icon': '資訊圖示', + }, + 'zh_CN': { + 'dismiss-notification': '关闭通知', + 'success-icon': '成功图标', + 'info-icon': '信息图标', + 'failure-icon': '故障图标', + }, + 'it': { + 'dismiss-notification': 'Ignora notifica', + 'success-icon': 'Icona Operazione riuscita', + 'info-icon': 'Icona Informazioni', + 'failure-icon': 'Icona Errore', + }, + 'nl': { + 'dismiss-notification': 'Melding negeren', + 'success-icon': 'Pictogram Geslaagd', + 'info-icon': 'Pictogram Informatie', + 'failure-icon': 'Pictogram Fout', + }, + 'pt': { + 'dismiss-notification': 'Descartar notificação', + 'success-icon': 'Ícone de êxito', + 'info-icon': 'Ícone de informação', + 'failure-icon': 'Ícone de falha', + }, + 'ru': { + 'dismiss-notification': 'Закрыть уведомление', + 'success-icon': 'Значок успешного выполнения', + 'info-icon': 'Значок информации', + 'failure-icon': 'Значок ошибки', + }, +}; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/toast/toast.model.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/toast/toast.model.ts new file mode 100644 index 0000000000..ca492ec7d2 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/toast/toast.model.ts @@ -0,0 +1,11 @@ +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +export enum VmwToastType { + SUCCESS = "success", + FAILURE = "failure", + INFO = "info", + WARN = "warning", +} diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/vdk-ngx-components.module.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/vdk-ngx-components.module.ts new file mode 100644 index 0000000000..3d200de559 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-components/vdk-ngx-components.module.ts @@ -0,0 +1,101 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { CUSTOM_ELEMENTS_SCHEMA, ModuleWithProviders, NgModule } from "@angular/core"; +import { CommonModule } from "@angular/common"; +import { FormsModule, ReactiveFormsModule } from "@angular/forms"; + +import { + ClarityModule, + ClrDropdownModule, + ClrLoadingButtonModule, + ClrLoadingModule, + ClrTooltipModule, +} from "@clr/angular"; + +import { VdkSimpleTranslateModule } from '../ngx-utils'; + +import { + angleIcon, + arrowIcon, + checkCircleIcon, + checkIcon, + ClarityIcons, + copyToClipboardIcon, + exclamationCircleIcon, + searchIcon, + timesCircleIcon, + timesIcon +} from '@cds/core/icon'; + +import { VdkEmptyStatePlaceholderModule } from "./empty-state-placeholder/empty-state-placeholder.module"; + +import { COPY_TO_CLIPBPOARD_BUTTON_DIRECTIVES } from "./copy-to-clipboard-button/index"; +import { FORM_SECTION_DIRECTIVES } from "./form-section/index"; +import { FORM_SECTION_CONTAINER_DIRECTIVES } from "./form-section-container/index"; +import { TOAST_DIRECTIVES } from "./toast/index"; +import { VdkSearchModule } from "./search"; + +@NgModule({ + imports: [ + CommonModule, + FormsModule, + ReactiveFormsModule, + ClarityModule, + ClrTooltipModule, + ClrDropdownModule, + ClrLoadingModule, + ClrLoadingButtonModule, + VdkSimpleTranslateModule, + VdkEmptyStatePlaceholderModule, + VdkSearchModule + ], + declarations: [ + COPY_TO_CLIPBPOARD_BUTTON_DIRECTIVES, + FORM_SECTION_DIRECTIVES, + FORM_SECTION_CONTAINER_DIRECTIVES, + TOAST_DIRECTIVES + ], + exports: [ + COPY_TO_CLIPBPOARD_BUTTON_DIRECTIVES, + FORM_SECTION_DIRECTIVES, + FORM_SECTION_CONTAINER_DIRECTIVES, + TOAST_DIRECTIVES, + VdkEmptyStatePlaceholderModule, + VdkSearchModule + ], + schemas: [ + CUSTOM_ELEMENTS_SCHEMA, + ], +}) +export class VdkComponentsModule { + static forRoot(): ModuleWithProviders { + return { + ngModule: VdkComponentsModule, + }; + } + + static forChild(): ModuleWithProviders { + return { + ngModule: VdkComponentsModule, + }; + } + + constructor() { + ClarityIcons.addIcons( + angleIcon, + arrowIcon, + checkCircleIcon, + checkIcon, + copyToClipboardIcon, + exclamationCircleIcon, + searchIcon, + timesCircleIcon, + timesIcon + ); + } +} diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-utils/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-utils/index.ts new file mode 100644 index 0000000000..79cbf723dd --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-utils/index.ts @@ -0,0 +1,6 @@ +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from "./simple-translate-service/index"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-utils/public-api.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-utils/public-api.ts new file mode 100644 index 0000000000..ba142389bd --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-utils/public-api.ts @@ -0,0 +1,8 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './index'; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-utils/simple-translate-service/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-utils/simple-translate-service/index.ts new file mode 100644 index 0000000000..b135809eb5 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-utils/simple-translate-service/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from "./simple-translate.module"; +export * from "./simple-translate.service"; +export * from "./simple-translate.pipe"; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-utils/simple-translate-service/public-api.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-utils/simple-translate-service/public-api.ts new file mode 100644 index 0000000000..ba142389bd --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-utils/simple-translate-service/public-api.ts @@ -0,0 +1,8 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './index'; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-utils/simple-translate-service/simple-translate.module.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-utils/simple-translate-service/simple-translate.module.ts new file mode 100644 index 0000000000..4aef87e8f8 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-utils/simple-translate-service/simple-translate.module.ts @@ -0,0 +1,30 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { NgModule, ModuleWithProviders } from "@angular/core"; + +import { VdkSimpleTranslateService } from "./simple-translate.service"; +import { VdkSimpleTranslatePipe } from "./simple-translate.pipe"; + +@NgModule({ + declarations: [ + VdkSimpleTranslatePipe, + ], + exports: [ + VdkSimpleTranslatePipe, + ], +}) +export class VdkSimpleTranslateModule { + static forRoot(): ModuleWithProviders { + return { + ngModule: VdkSimpleTranslateModule, + providers: [ + VdkSimpleTranslateService, + ] + }; + } +} diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-utils/simple-translate-service/simple-translate.pipe.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-utils/simple-translate-service/simple-translate.pipe.ts new file mode 100644 index 0000000000..39f00b5294 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-utils/simple-translate-service/simple-translate.pipe.ts @@ -0,0 +1,20 @@ +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Pipe, PipeTransform } from '@angular/core'; + +import { VdkSimpleTranslateService } from './simple-translate.service'; + +@Pipe({ + name: 'simpleTranslate', + pure: false, +}) +export class VdkSimpleTranslatePipe implements PipeTransform { + constructor(private simpleTranslate: VdkSimpleTranslateService) {} + + transform(text: string, ...args: any[]) { + return this.simpleTranslate.translate(text, ...args); + } +} diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-utils/simple-translate-service/simple-translate.service.spec.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-utils/simple-translate-service/simple-translate.service.spec.ts new file mode 100644 index 0000000000..47c9ac9eef --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-utils/simple-translate-service/simple-translate.service.spec.ts @@ -0,0 +1,70 @@ +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { TestBed, inject } from "@angular/core/testing"; + +import { VdkSimpleTranslateService } from "./simple-translate.service"; + +const mockTranslations = { + 'en': { + 'chat-with-vmware-support': 'Chat with VMware Support', + 'return-to-chat': "Return to chat" + }, + 'es': { + 'chat-with-vmware-support': 'Conversa con VMware Support', + 'return-to-chat': "Retornar a conversacion" + } +}; + +describe("VdkSimpleTranslateService", () => { + let service: VdkSimpleTranslateService; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + VdkSimpleTranslateService + ] + }); + + service = TestBed.inject(VdkSimpleTranslateService); + }); + + it("should accept a LCID and set language", () => { + expect(service).toBeTruthy(); + expect(service.getLanguage()).toEqual('en'); + service.setLanguage('es-CO'); + expect(service.getLanguage()).toEqual('es'); + service.setLanguage('es_CO'); + expect(service.getLanguage()).toEqual('es'); + service.setLanguage('zh_TW'); + expect(service.getLanguage()).toEqual('zh_TW'); + service.setLanguage('zh'); + expect(service.getLanguage()).toEqual('zh'); + }); + + it("should translate string as per specified language", () => { + service.setLanguage('es-CO'); + + service.loadTranslationsForComponent('test', mockTranslations); + + expect(service.translate('test.return-to-chat')).toEqual('Retornar a conversacion'); + }); + + it("should return translation in the default langauge if the string does not exist in the specified language", () => { + service.setLanguage('pl-PL'); + + service.loadTranslationsForComponent('test', mockTranslations); + + expect(service.translate('test.return-to-chat')).toEqual('Return to chat'); + }); + + it("error message should show if key is not found", () => { + service.setLanguage('pl-PL'); + + service.loadTranslationsForComponent('test', mockTranslations); + + expect(service.translate('test.some-non-existing-key')).toEqual('!! Key test.some-non-existing-key not found !!'); + }); +}); diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-utils/simple-translate-service/simple-translate.service.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-utils/simple-translate-service/simple-translate.service.ts new file mode 100644 index 0000000000..03b2047599 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/ngx-utils/simple-translate-service/simple-translate.service.ts @@ -0,0 +1,95 @@ +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Injectable } from "@angular/core"; + +const DEFAULT_LANGUAGE = 'en'; + +@Injectable({ + providedIn: 'root' +}) +export class VdkSimpleTranslateService { + private language: string = DEFAULT_LANGUAGE; + private translations: any = {}; + + constructor () { + const w: any = window as any; + const language = w.navigator.language || w.navigator.userLanguage; + this.setLanguage(language); + } + + loadTranslationsForComponent(componentKey: string, translationsToAdd: any) { + for (let language in translationsToAdd) { + if (!this.translations[language]) { + this.translations[language] = {}; + } + + for (let y in translationsToAdd[language]) { + let newKey = componentKey + '.' + y; + this.translations[language][newKey] = translationsToAdd[language][y]; + } + } + } + + setLanguage(language: string) { + language = language.replace('-', '_'); + + // check if different locale names are used for Simplified or Traditional Chinese + const chineseMapping = { + 'zh_Hans': 'zh_CN', + 'zh_Hant': 'zh_TW' + } + language = chineseMapping[language] || language; + + // Special-case Chinese because we support both TW and CN locales + // Also if the locale is pt_PT(Portuguese Portugal) we should default to EN + // otherwise the 2 character language ID is what we want + const mainLocale = language.substring(0, 2); + const isChinese = language.length > 2 && mainLocale === 'zh'; + const isNotSupportedPortuguese = language.length > 2 && mainLocale === 'pt' && language !== 'pt_BR'; + + if (isChinese || isNotSupportedPortuguese) { + this.language = language; + } else { + this.language = mainLocale; + } + } + + getLanguage() { + return this.language; + } + + translate(key: any, ...args: any[]) { + let translations = this.translations[this.language]; + + //if there are no translations of the user's language set translations to equal the default language + if (!translations) { + translations = this.translations[DEFAULT_LANGUAGE] ? this.translations[DEFAULT_LANGUAGE] : []; + } + + //if there is a translation get that translation otherwise use the default language + let translation = undefined; + if (translations[key]) { + translation = translations[key]; + } else if(this.translations[DEFAULT_LANGUAGE]) { + translation = this.translations[DEFAULT_LANGUAGE][key]; + } + + //if no translation is found in the user's language or in the default language then show an error string + if (!translation) { + translation = `!! Key ${key} not found !!`; + } + + return this.format(translation, ...args); + } + + private format(format: string, ...args: string[]) { + return format.replace(/{(\d+)}/g, function(match, number) { + return typeof args[number] != 'undefined' + ? args[number] + : match; + }); + } +} diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/public-api.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/public-api.ts new file mode 100644 index 0000000000..dac97364a2 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/commons/public-api.ts @@ -0,0 +1,10 @@ +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +/* + * Public API Surface of commons + */ + +export * from './index'; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/base/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/base/index.ts new file mode 100644 index 0000000000..da2b664823 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/base/index.ts @@ -0,0 +1,9 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './interfaces'; +export * from './taurus-base.component'; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/base/interfaces/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/base/interfaces/index.ts new file mode 100644 index 0000000000..9d35b0dc49 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/base/interfaces/index.ts @@ -0,0 +1,8 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './taurus-component-lifecycle-hooks.interface'; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/base/interfaces/taurus-component-lifecycle-hooks.interface.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/base/interfaces/taurus-component-lifecycle-hooks.interface.ts new file mode 100644 index 0000000000..d7ad5c60d4 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/base/interfaces/taurus-component-lifecycle-hooks.interface.ts @@ -0,0 +1,139 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { ComponentModel } from '../../model'; + +/** + * ** Taurus Component Lifecycle hook for Model initialized. + * + * + */ +export interface OnTaurusModelInit { + /** + * ** Fires once per Route, after Component is initialized and + * immediately after {@link ComponentModel} is initialized and it is bound to the Component model field. + * + * - model - ComponentModel optional parameter. + * - task - Is string optional parameter that inject context to callback for the specific operation. + */ + onModelInit(model?: ComponentModel, task?: string): void; +} + +/** + * @deprecated Deprecated since version 1.5.0 in favor of {@link OnTaurusModelInitialLoad} + * + * ** Taurus Component Lifecycle hook for Model First Load. + * + * + */ +export interface OnTaurusModelFirstLoad { + /** + * ** Fires when something in State change and its status is LOADED or FAILED, and it fires only once. + * + * - model - ComponentModel optional parameter. + * - task - Is string optional parameter that inject context to callback for the specific operation. + * + *

+ * - General hook ideal for Ui state restore, or something that need Read->Action->Done behaviour. + *

+ */ + onModelFirstLoad(model?: ComponentModel, task?: string): void; +} + + +/** + * ** Taurus Component Lifecycle hook for Model Initial Load. + * + * + */ +export interface OnTaurusModelInitialLoad { + /** + * ** Fires when something in State change and its status is LOADED or FAILED, and it fires only once. + * + * - model - ComponentModel optional parameter. + * - task - Is string optional parameter that inject context to callback for the specific operation. + * + *

+ * - General hook ideal for Ui state restore, or something that need Read->Action->Done behaviour. + *

+ */ + onModelInitialLoad(model?: ComponentModel, task?: string): void; +} + +/** + * ** Taurus Component Lifecycle hook for Model Loaded. + * + * + */ +export interface OnTaurusModelLoad { + /** + * ** Fires when something in State change and its status is LOADED or FAILED. + * + * - model - ComponentModel optional parameter. + * - task - Is string optional parameter that inject context to callback for the specific operation. + * + *

+ * - General hook ideal for loading spinner HIDE. + *

+ */ + onModelLoad(model?: ComponentModel, task?: string): void; +} + +/** + * ** Taurus Component Lifecycle hook for Model Changed. + * + * + */ +export interface OnTaurusModelChange { + /** + * ** Fires when something in State change and its status is LOADED. + * + * - model - ComponentModel optional parameter. + * - task - Is string optional parameter that inject context to callback for the specific operation. + */ + onModelChange(model?: ComponentModel, task?: string): void; +} + +/** + * @deprecated Deprecated since version 1.5.0 in favor of {@link OnTaurusModelError} + * + * ** Taurus Component Lifecycle hook for Model Failed. + * + * + */ +export interface OnTaurusModelFail { + /** + * ** Fires when something in State change and its status is FAILED. + * + * - model - ComponentModel optional parameter. + * - task - Is string optional parameter that inject context to callback for the specific operation. + */ + onModelFail(model?: ComponentModel, task?: string): void; +} + +/** + * ** Taurus Component Lifecycle hook for Model Failed. + * + * + */ +export interface OnTaurusModelError { + /** + * ** Fires when something in State change and its status is FAILED. + * + * - model - ComponentModel optional parameter. + * - task - Is string optional parameter that inject context to callback for the specific operation. + */ + onModelError(model?: ComponentModel, task?: string): void; +} + +export type TaurusComponentHooks = OnTaurusModelInit + & OnTaurusModelInitialLoad + & OnTaurusModelFirstLoad + & OnTaurusModelLoad + & OnTaurusModelChange + & OnTaurusModelError + & OnTaurusModelFail; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/base/taurus-base.component.spec.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/base/taurus-base.component.spec.ts new file mode 100644 index 0000000000..735c3ea7ef --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/base/taurus-base.component.spec.ts @@ -0,0 +1,554 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +/* eslint-disable @typescript-eslint/dot-notation */ + +import { Component, OnDestroy, OnInit } from '@angular/core'; +import { ActivatedRoute, ActivatedRouteSnapshot } from '@angular/router'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { of } from 'rxjs'; +import { take } from 'rxjs/operators'; + +import { CallFake } from '../../../unit-testing'; + +import { TaurusObject } from '../../../common'; + +import { RouteState } from '../../router'; +import { RouteStateFactory } from '../../router/factory'; + +import { NavigationService } from '../../navigation'; + +import { ComponentModel, ComponentState, FAILED, LOADED } from '../model'; +import { ComponentService } from '../services'; + +import { + OnTaurusModelChange, + OnTaurusModelFail, + OnTaurusModelFirstLoad, + OnTaurusModelInit, + OnTaurusModelLoad +} from './interfaces'; + +import { TaurusBaseComponent } from './taurus-base.component'; + +@Component({ + selector: 'shared-taurus-base-subclass-component', + template: '' +}) + // eslint-disable-next-line @angular-eslint/component-class-suffix +class TaurusBaseComponentStub extends TaurusBaseComponent + implements OnInit, OnDestroy, + OnTaurusModelInit, OnTaurusModelLoad, + OnTaurusModelFirstLoad, OnTaurusModelChange, + OnTaurusModelFail { + + uuid = 'TaurusBaseComponentStub'; + + constructor(componentService: ComponentService, + navigationService: NavigationService, + activatedRoute: ActivatedRoute) { + super(componentService, navigationService, activatedRoute); + } + + onModelInit(_model?: ComponentModel, _task?: string) { + // No-op. + } + + onModelFirstLoad(_model?: ComponentModel, _task?: string) { + // No-op. + } + + onModelLoad(_model?: ComponentModel, _task?: string) { + // No-op. + } + + onModelChange(_model?: ComponentModel, _task?: string) { + // No-op. + } + + onModelFail(_model?: ComponentModel, _task?: string) { + // No-op. + } + + override ngOnInit() { + super.ngOnInit(); + } + + override ngOnDestroy() { + super.ngOnDestroy(); + } +} + +class ComponentModelStub { + task: string; + + constructor(task?: string) { + this.task = task; + } + + get status() { + return null; + } + + isModified(): boolean { + return null; + } + + withStatusIdle() { + return this; + } + + getComponentState() { + return {}; + } + + getTask() { + return this.task; + } + + clearTask() { + return this; + } +} + +describe('TaurusBaseComponent -> TaurusSubclassComponent', () => { + let componentServiceStub: jasmine.SpyObj; + let navigationServiceStub: jasmine.SpyObj; + let activatedRouteStub: Partial; + let activatedRouteSnapshotStub: ActivatedRouteSnapshot; + + let routeStateStub: RouteState; + + let fixture: ComponentFixture; + let component: TaurusBaseComponentStub; + let modelStub: ComponentModel; + + beforeEach(() => { + componentServiceStub = jasmine.createSpyObj('componentService', ['init', 'idle', 'getModel', 'update']); + navigationServiceStub = jasmine.createSpyObj('navigationService', ['navigateTo', 'navigateBack']); + activatedRouteSnapshotStub = { + url: '', + params: {}, + data: {} + } as any; + activatedRouteStub = { snapshot: activatedRouteSnapshotStub }; + routeStateStub = { + routePathSegments: ['path1/path2', 'path3/path4'] + } as RouteState; + spyOn(RouteStateFactory.prototype, 'create').and.returnValue(routeStateStub); + + modelStub = new ComponentModelStub() as any; + + TestBed.configureTestingModule({ + declarations: [ + TaurusBaseComponentStub + ], + providers: [ + { provide: ComponentService, useValue: componentServiceStub }, + { provide: NavigationService, useValue: navigationServiceStub }, + { provide: ActivatedRoute, useValue: activatedRouteStub } + ] + }); + + fixture = TestBed.createComponent(TaurusBaseComponentStub); + component = fixture.componentInstance; + }); + + it('should verify component is created', () => { + // Given + component.model = modelStub; + + // Then + expect(component).toBeDefined(); + expect(component).toBeInstanceOf(TaurusBaseComponentStub); + expect(component).toBeInstanceOf(TaurusBaseComponent); + }); + + describe('Properties::', () => { + describe('|uuid|', () => { + it('should verify default value is undefined', () => { + // Given + component.model = modelStub; + + // When + expect(component.uuid).toEqual('TaurusBaseComponentStub'); + }); + }); + + describe('|componentService|', () => { + it('should verify ComponentService will be injected', () => { + // Given + component.model = modelStub; + + // When + expect(component['componentService']).toBeDefined(); + }); + }); + + describe('|navigationService|', () => { + it('should verify NavigationService will be injected', () => { + // Given + component.model = modelStub; + + // When + expect(component['navigationService']).toBeDefined(); + }); + }); + + describe('|activatedRoute|', () => { + it('should verify ActivatedRoute will be injected', () => { + // Given + component.model = modelStub; + + // When + expect(component['activatedRoute']).toBeDefined(); + }); + }); + }); + + describe('Angular lifecycle hooks::', () => { + beforeEach(() => { + component.model = modelStub; + }); + + describe('|ngOnInit|', () => { + it('should verify will invoke expected method', () => { + // Given + // @ts-ignore + const bindModelSpy = spyOn(component, 'bindModel').and.callFake(CallFake); + + // When + fixture.detectChanges(); + + // Then + expect(bindModelSpy).toHaveBeenCalled(); + }); + }); + + describe('|ngOnDestroy|', () => { + it('should verify will invoke expected methods', () => { + // Given + spyOn(TaurusBaseComponent.prototype, 'ngOnInit').and.callFake(CallFake); + // @ts-ignore + const setIdleSpy: jasmine.Spy<() => void> = spyOn(TaurusBaseComponent.prototype, 'setStateIdle').and.callFake(CallFake); + const ngOnDestroySpy: jasmine.Spy<() => void> = spyOn(TaurusObject.prototype, 'ngOnDestroy').and.callFake(CallFake); + + // When + fixture = TestBed.createComponent(TaurusBaseComponentStub); + fixture.componentInstance.model = modelStub; + fixture.detectChanges(); + fixture.destroy(); + + // Then + expect(setIdleSpy).toHaveBeenCalled(); + expect(ngOnDestroySpy).toHaveBeenCalled(); + }); + }); + }); + + describe('Methods::', () => { + describe('navigateTo', () => { + it('should verify will invoke correct methods', () => { + // Given + const replaceValues = { '$.team': 'team1', '$.entity': 'Prime' }; + navigationServiceStub.navigateTo.and.returnValue(Promise.resolve(true)); + component.model = modelStub; + + // When + const promise = component.navigateTo(replaceValues); + + // Then + expect(promise).toBeInstanceOf(Promise); + expect(navigationServiceStub.navigateTo).toHaveBeenCalledWith(replaceValues); + }); + }); + + describe('navigateBack', () => { + it('should verify will invoke correct methods', () => { + // Given + const replaceValues = { '$.team': 'team10', '$.entity': 'Second' }; + navigationServiceStub.navigateBack.and.returnValue(Promise.resolve(false)); + component.model = modelStub; + + // When + const promise = component.navigateBack(replaceValues); + + // Then + expect(promise).toBeInstanceOf(Promise); + expect(navigationServiceStub.navigateBack).toHaveBeenCalledWith(replaceValues); + }); + }); + + describe('|bindModel|', () => { + it('should verify will create correct subscriptions', () => { + // Given + componentServiceStub.init.and.returnValue(of(modelStub).pipe(take(1))); + componentServiceStub.getModel.and.returnValue(of(modelStub)); + + // When + // @ts-ignore + component.bindModel(); + + // Then 2 + expect(componentServiceStub.init).toHaveBeenCalledWith(component.uuid, routeStateStub); + expect(componentServiceStub.getModel).toHaveBeenCalledWith(component.uuid, routeStateStub.routePathSegments); + expect(component['subscriptions'].length).toEqual(1); + }); + + it('should verify will invoke correct Taurus lifecycle hooks for LOADED and modified', () => { + // Given + const onModelInitSpy = spyOn(TaurusBaseComponentStub.prototype, 'onModelInit').and.callFake(CallFake); + const onModelFirstLoadSpy = spyOn(TaurusBaseComponentStub.prototype, 'onModelFirstLoad').and.callFake(CallFake); + const onModelLoadSpy = spyOn(TaurusBaseComponentStub.prototype, 'onModelLoad').and.callFake(CallFake); + const onModelChangeSpy = spyOn(TaurusBaseComponentStub.prototype, 'onModelChange').and.callFake(CallFake); + const onModelFailSpy = spyOn(TaurusBaseComponentStub.prototype, 'onModelFail').and.callFake(CallFake); + const isModelModifiedSpy = spyOn(modelStub, 'isModified').and.returnValue(true); + const modelStatusSpy = spyOnProperty(modelStub, 'status', 'get').and.returnValue(LOADED); + + componentServiceStub.init.and.returnValue(of(modelStub)); + componentServiceStub.getModel.and.returnValue(of(modelStub)); + + // Then 1 + expect(component.model).toBeUndefined(); + + // When + // @ts-ignore + component.bindModel(); + + // Then 2 + expect(component.model).toBe(modelStub); + expect(onModelInitSpy).toHaveBeenCalledWith(modelStub); + expect(onModelFirstLoadSpy).toHaveBeenCalledWith(modelStub); + expect(onModelLoadSpy).toHaveBeenCalledWith(modelStub); + expect(isModelModifiedSpy).toHaveBeenCalledWith(modelStub); + expect(component.model).toBe(modelStub); + expect(modelStatusSpy).toHaveBeenCalled(); + expect(onModelChangeSpy).toHaveBeenCalledWith(modelStub); + expect(onModelFailSpy).not.toHaveBeenCalled(); + }); + + it('should verify will invoke correct Taurus lifecycle hooks for LOADED and modified with Task', () => { + // Given + const task = 'delete_entity'; + modelStub = new ComponentModelStub(task) as any; + const onModelInitSpy = spyOn(TaurusBaseComponentStub.prototype, 'onModelInit').and.callFake(CallFake); + const onModelFirstLoadSpy = spyOn(TaurusBaseComponentStub.prototype, 'onModelFirstLoad').and.callFake(CallFake); + const onModelLoadSpy = spyOn(TaurusBaseComponentStub.prototype, 'onModelLoad').and.callFake(CallFake); + const onModelChangeSpy = spyOn(TaurusBaseComponentStub.prototype, 'onModelChange').and.callFake(CallFake); + const onModelFailSpy = spyOn(TaurusBaseComponentStub.prototype, 'onModelFail').and.callFake(CallFake); + const isModelModifiedSpy = spyOn(modelStub, 'isModified').and.returnValue(true); + const modelStatusSpy = spyOnProperty(modelStub, 'status', 'get').and.returnValue(LOADED); + + componentServiceStub.init.and.returnValue(of(modelStub)); + componentServiceStub.getModel.and.returnValue(of(modelStub)); + + // Then 1 + expect(component.model).toBeUndefined(); + + // When + // @ts-ignore + component.bindModel(); + + // Then 2 + expect(component.model).toBe(modelStub); + expect(onModelInitSpy).toHaveBeenCalledWith(modelStub, task); + expect(onModelFirstLoadSpy).toHaveBeenCalledWith(modelStub, task); + expect(onModelLoadSpy).toHaveBeenCalledWith(modelStub, task); + expect(isModelModifiedSpy).toHaveBeenCalledWith(modelStub); + expect(component.model).toBe(modelStub); + expect(modelStatusSpy).toHaveBeenCalled(); + expect(onModelChangeSpy).toHaveBeenCalledWith(modelStub, task); + expect(onModelFailSpy).not.toHaveBeenCalled(); + }); + + it('should verify will invoke correct Taurus lifecycle hooks for FAILED and modified', () => { + // Given + const onModelInitSpy = spyOn(TaurusBaseComponentStub.prototype, 'onModelInit').and.callFake(CallFake); + const onModelFirstLoadSpy = spyOn(TaurusBaseComponentStub.prototype, 'onModelFirstLoad').and.callFake(CallFake); + const onModelLoadSpy = spyOn(TaurusBaseComponentStub.prototype, 'onModelLoad').and.callFake(CallFake); + const onModelChangeSpy = spyOn(TaurusBaseComponentStub.prototype, 'onModelChange').and.callFake(CallFake); + const onModelFailSpy = spyOn(TaurusBaseComponentStub.prototype, 'onModelFail').and.callFake(CallFake); + const isModelModifiedSpy = spyOn(modelStub, 'isModified').and.returnValue(true); + const modelStatusSpy = spyOnProperty(modelStub, 'status', 'get').and.returnValue(FAILED); + + componentServiceStub.init.and.returnValue(of(modelStub)); + componentServiceStub.getModel.and.returnValue(of(modelStub)); + + // Then 1 + expect(component.model).toBeUndefined(); + + // When + // @ts-ignore + component.bindModel(); + + // Then 2 + expect(component.model).toBe(modelStub); + expect(onModelInitSpy).toHaveBeenCalledWith(modelStub); + expect(onModelFirstLoadSpy).toHaveBeenCalledWith(modelStub); + expect(onModelLoadSpy).toHaveBeenCalledWith(modelStub); + expect(isModelModifiedSpy).toHaveBeenCalledWith(modelStub); + expect(component.model).toBe(modelStub); + expect(modelStatusSpy).toHaveBeenCalled(); + expect(onModelFailSpy).toHaveBeenCalledWith(modelStub); + expect(onModelChangeSpy).not.toHaveBeenCalled(); + }); + + it('should verify will invoke correct Taurus lifecycle hooks for FAILED and modified with Task', () => { + // Given + const task = 'patch_entity'; + modelStub = new ComponentModelStub(task) as any; + const onModelInitSpy = spyOn(TaurusBaseComponentStub.prototype, 'onModelInit').and.callFake(CallFake); + const onModelFirstLoadSpy = spyOn(TaurusBaseComponentStub.prototype, 'onModelFirstLoad').and.callFake(CallFake); + const onModelLoadSpy = spyOn(TaurusBaseComponentStub.prototype, 'onModelLoad').and.callFake(CallFake); + const onModelChangeSpy = spyOn(TaurusBaseComponentStub.prototype, 'onModelChange').and.callFake(CallFake); + const onModelFailSpy = spyOn(TaurusBaseComponentStub.prototype, 'onModelFail').and.callFake(CallFake); + const isModelModifiedSpy = spyOn(modelStub, 'isModified').and.returnValue(true); + const modelStatusSpy = spyOnProperty(modelStub, 'status', 'get').and.returnValue(FAILED); + + componentServiceStub.init.and.returnValue(of(modelStub)); + componentServiceStub.getModel.and.returnValue(of(modelStub)); + + // Then 1 + expect(component.model).toBeUndefined(); + + // When + // @ts-ignore + component.bindModel(); + + // Then 2 + expect(component.model).toBe(modelStub); + expect(onModelInitSpy).toHaveBeenCalledWith(modelStub, task); + expect(onModelFirstLoadSpy).toHaveBeenCalledWith(modelStub, task); + expect(onModelLoadSpy).toHaveBeenCalledWith(modelStub, task); + expect(isModelModifiedSpy).toHaveBeenCalledWith(modelStub); + expect(component.model).toBe(modelStub); + expect(modelStatusSpy).toHaveBeenCalled(); + expect(onModelFailSpy).toHaveBeenCalledWith(modelStub, task); + expect(onModelChangeSpy).not.toHaveBeenCalled(); + }); + + it('should verify will invoke correct Taurus lifecycle hooks for LOADED and not modified', () => { + // Given + const localModelStub: ComponentModel = new ComponentModelStub() as any; + const onModelInitSpy = spyOn(TaurusBaseComponentStub.prototype, 'onModelInit').and.callFake(CallFake); + const onModelFirstLoadSpy = spyOn(TaurusBaseComponentStub.prototype, 'onModelFirstLoad').and.callFake(CallFake); + const onModelLoadSpy = spyOn(TaurusBaseComponentStub.prototype, 'onModelLoad').and.callFake(CallFake); + const onModelChangeSpy = spyOn(TaurusBaseComponentStub.prototype, 'onModelChange').and.callFake(CallFake); + const onModelFailSpy = spyOn(TaurusBaseComponentStub.prototype, 'onModelFail').and.callFake(CallFake); + const isModelModifiedSpy = spyOn(localModelStub, 'isModified').and.returnValue(false); + const modelStatusSpy = spyOnProperty(localModelStub, 'status', 'get').and.returnValue(LOADED); + + componentServiceStub.init.and.returnValue(of(modelStub)); + componentServiceStub.getModel.and.returnValue(of(localModelStub)); + + // Then 1 + expect(component.model).toBeUndefined(); + + // When + // @ts-ignore + component.bindModel(); + + // Then 2 + expect(component.model).toBe(modelStub); + expect(onModelInitSpy).toHaveBeenCalledWith(modelStub); + expect(onModelFirstLoadSpy).toHaveBeenCalledWith(localModelStub); + expect(onModelLoadSpy).toHaveBeenCalledWith(localModelStub); + expect(isModelModifiedSpy).toHaveBeenCalledWith(modelStub); + expect(component.model).not.toBe(localModelStub); + expect(modelStatusSpy).not.toHaveBeenCalled(); + expect(onModelChangeSpy).not.toHaveBeenCalled(); + expect(onModelFailSpy).not.toHaveBeenCalled(); + }); + + it('should verify will invoke correct Taurus lifecycle hooks for LOADED and not modified with Task', () => { + // Given + const task = 'move_entity'; + modelStub = new ComponentModelStub() as any; + const localModelStub: ComponentModel = new ComponentModelStub(task) as any; + const onModelInitSpy = spyOn(TaurusBaseComponentStub.prototype, 'onModelInit').and.callFake(CallFake); + const onModelFirstLoadSpy = spyOn(TaurusBaseComponentStub.prototype, 'onModelFirstLoad').and.callFake(CallFake); + const onModelLoadSpy = spyOn(TaurusBaseComponentStub.prototype, 'onModelLoad').and.callFake(CallFake); + const onModelChangeSpy = spyOn(TaurusBaseComponentStub.prototype, 'onModelChange').and.callFake(CallFake); + const onModelFailSpy = spyOn(TaurusBaseComponentStub.prototype, 'onModelFail').and.callFake(CallFake); + const isModelModifiedSpy = spyOn(localModelStub, 'isModified').and.returnValue(false); + const modelStatusSpy = spyOnProperty(localModelStub, 'status', 'get').and.returnValue(LOADED); + + componentServiceStub.init.and.returnValue(of(modelStub)); + componentServiceStub.getModel.and.returnValue(of(localModelStub)); + + // Then 1 + expect(component.model).toBeUndefined(); + + // When + // @ts-ignore + component.bindModel(); + + // Then 2 + expect(component.model).toBe(modelStub); + expect(onModelInitSpy).toHaveBeenCalledWith(modelStub); + expect(onModelFirstLoadSpy).toHaveBeenCalledWith(localModelStub, task); + expect(onModelLoadSpy).toHaveBeenCalledWith(localModelStub, task); + expect(isModelModifiedSpy).toHaveBeenCalledWith(modelStub); + expect(component.model).not.toBe(localModelStub); + expect(modelStatusSpy).not.toHaveBeenCalled(); + expect(onModelChangeSpy).not.toHaveBeenCalled(); + expect(onModelFailSpy).not.toHaveBeenCalled(); + }); + }); + + describe('|setStateIdle|', () => { + it('should verify will invoke correct methods', () => { + // Given + const componentState: ComponentState = { + id: 'TaurusBaseComponentStub', + status: LOADED, + toLiteral: CallFake, + toLiteralDeepClone: CallFake, + copy: CallFake + }; + const setStatusIdleSpy = spyOn(modelStub, 'withStatusIdle').and.callFake(() => modelStub); + const getComponentStateSpy = spyOn(modelStub, 'getComponentState').and.returnValue(componentState); + component.model = modelStub; + + // When + // @ts-ignore + component.setStateIdle(); + + // Then + expect(setStatusIdleSpy).toHaveBeenCalled(); + expect(getComponentStateSpy).toHaveBeenCalled(); + expect(componentServiceStub.idle).toHaveBeenCalledWith(componentState); + }); + }); + + describe('|normalizeModelState|', () => { + it('should verify will invoke correct methods', () => { + // Given + const componentState: ComponentState = { + id: 'TaurusBaseComponentStub', + status: LOADED, + toLiteral: CallFake, + toLiteralDeepClone: CallFake, + copy: CallFake + }; + const clearTaskSpy = spyOn(modelStub, 'clearTask').and.callFake(() => modelStub); + const getComponentStateSpy = spyOn(modelStub, 'getComponentState').and.returnValue(componentState); + component.model = modelStub; + + // When + // @ts-ignore + component.normalizeModelState(modelStub); + + // Then + expect(clearTaskSpy).toHaveBeenCalled(); + expect(getComponentStateSpy).toHaveBeenCalled(); + expect(componentServiceStub.update).toHaveBeenCalledWith(componentState); + }); + }); + }); +}); diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/base/taurus-base.component.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/base/taurus-base.component.ts new file mode 100644 index 0000000000..f92668aa98 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/base/taurus-base.component.ts @@ -0,0 +1,335 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +/* eslint-disable @angular-eslint/directive-class-suffix */ + +import { Directive, OnDestroy, OnInit } from '@angular/core'; +import { ActivatedRoute, Params } from '@angular/router'; + +import { Subscription } from 'rxjs'; + +import { ArrayElement, CollectionsUtil } from '../../../utils'; + +import { TaurusNavigateAction, TaurusObject } from '../../../common'; + +import { NavigationService } from '../../navigation'; +import { RouteStateFactory } from '../../router/factory'; + +import { ComponentModel, FAILED } from '../model'; +import { ComponentService } from '../services'; + +import { TaurusComponentHooks } from './interfaces'; + +/** + * ** Superclass Component for all other Components that want to use NgRx Store and all lifecycle hooks from Taurus. + * + * + */ +@Directive() +export abstract class TaurusBaseComponent extends TaurusObject implements OnInit, OnDestroy { + private static _routeStateFactory: RouteStateFactory; + + /** + * ** Field that hold Component Model. + */ + model: ComponentModel; + + /** + * ** UUID is identifier for every Subclass in Components state Store. + */ + abstract readonly uuid: string; + + /** + * ** Feature flag to enforce Route reuse in native way provided from Taurus NgRx. + * + * - Introduced for backward compatibility. + * - Default value is false, and continues to operate like previous major version. + * - If set to true will enforce Route reuse strategy and will re-initialize Component with new Model for the new params. + */ + enforceRouteReuse = false; + + /** + * ** Model subscription ref. + * @private + */ + private _modelSubscription: Subscription; + + /** + * ** Constructor. + */ + protected constructor(protected readonly componentService: ComponentService, + protected readonly navigationService: NavigationService, + protected readonly activatedRoute: ActivatedRoute) { + super(); + } + + /** + * ** Navigate to page using {@link NavigationService.navigateTo}. + */ + navigateTo(replaceValues?: { [key: string]: ArrayElement['replaceValue'] }): Promise { + return this + .navigationService + .navigateTo(replaceValues); + } + + /** + * ** Navigate back to previous page using {@link NavigationService.navigateBack}. + */ + navigateBack(replaceValues?: { [key: string]: ArrayElement['replaceValue'] }): Promise { + return this + .navigationService + .navigateBack(replaceValues); + } + + /** + * @inheritDoc + */ + ngOnInit() { + this.bindModel(); + + this.initializeRouteReuse(); + } + + /** + * @inheritDoc + */ + override ngOnDestroy() { + this.setStateIdle(); + + super.ngOnDestroy(); + } + + /** + * ** Invoking method register subscriber for Taurus NgRx Redux Store mutation in context of {@link ComponentState.routePathSegments}, + * which binds {@link ComponentModel} to {@link TaurusBaseComponent.model} + * and start invocation of Taurus NgRx Redux Component lifecycle hooks. + * + * Invocation order: + * + * 1. {@link OnTaurusModelInit} + * 2. {@link OnTaurusModelInitialLoad} or {@link OnTaurusModelFirstLoad} - only one could be invoke, + * where deprecated shouldn't be implemented anymore. + * 3. {@link OnTaurusModelLoad} + * 4. {@link OnTaurusModelChange} when status is {@link LOADED} + * 5. {@link OnTaurusModelError} or {@link OnTaurusModelFail} when status is {@link FAILED} - only one could be invoke, + * where deprecated shouldn't be implemented anymore. + * + *

+ *
+ * Override it if you want to change default behavior. + *

+ * + * @protected + */ + protected bindModel(): void { + let isOnModelInitialLoadExecuted = false; + + if (!TaurusBaseComponent._routeStateFactory) { + TaurusBaseComponent._routeStateFactory = new RouteStateFactory(); + } + + const routeState = TaurusBaseComponent + ._routeStateFactory + .create(this.activatedRoute.snapshot, null); + + this.componentService + .init(this.uuid, routeState) + .subscribe((model) => { + this.model = model; + + TaurusBaseComponent._executeTaurusComponentHook(this, 'onModelInit', model); + }); + + this._modelSubscription = this.componentService + .getModel(this.uuid, routeState.routePathSegments) + .subscribe((model) => { + if (!isOnModelInitialLoadExecuted) { + isOnModelInitialLoadExecuted = true; + if (!TaurusBaseComponent._executeTaurusComponentHook(this, 'onModelInitialLoad', model)) { + TaurusBaseComponent._executeTaurusComponentHook(this, 'onModelFirstLoad', model); + } + } + + this.evaluateTaurusComponentLifecycleHooks(model); + }); + + this.subscriptions.push(this._modelSubscription); + } + + /** + * ** Evaluates Taurus NgRx Redux Component lifecycle hooks + * ({@link OnTaurusModelLoad} and {@link OnTaurusModelChange} and ({@link OnTaurusModelFail} or {@link OnTaurusModelError})). + * + * - Override it if you want to change default behavior. + * + * @protected + */ + protected evaluateTaurusComponentLifecycleHooks(model: ComponentModel): void { + TaurusBaseComponent._executeTaurusComponentHook(this, 'onModelLoad', model); + + if (!this.isModelModified(model)) { + return; + } + + this.refreshModel(model); + + if (model.status === FAILED) { + if (!TaurusBaseComponent._executeTaurusComponentHook(this, 'onModelError', model)) { + TaurusBaseComponent._executeTaurusComponentHook(this, 'onModelFail', model); + } + } else { + TaurusBaseComponent._executeTaurusComponentHook(this, 'onModelChange', model); + } + + try { + this.normalizeModelState(model); + } catch (e) { + console.error(`Taurus NgRx Redux failed to normalize ComponentModel!`, e); + } + } + + /** + * ** Refresh model field {@link TaurusBaseComponent.model} with new one, assignment by reference. + * + * - Override it if you want to change default behavior. + * + * @protected + */ + protected refreshModel(model: ComponentModel): void { + this.model = model; + } + + /** + * ** Normalize Model state, by default it clear Task field in {@link ComponentState.task} and update model in Taurus NgRx Redux Store. + * + * - It is invoked only if {@link ComponentModel} is modified and after invocation of all Taurus components lifecycle hooks. + * - Override it if you want to change default behavior. + * + * @protected + */ + protected normalizeModelState(model: ComponentModel): void { + this.componentService.update( + model + .clearTask() + .getComponentState() + ); + } + + /** + * ** Set Model state in IDLE to stop listening on Events from Store. + * + * - It is invoked by default before Component is destroyed, in Angular lifecycle hook {@link OnDestroy}. + * - Override it if you want to change default behavior. + * + * @protected + */ + protected setStateIdle(): void { + this.componentService.idle( + this.model + .withStatusIdle() + .getComponentState() + ); + } + + /** + * ** Evaluation of this method acknowledge that new {@link ComponentModel} is modified or not. + * + * - Comparison is evaluated between provided Model and assigned Component's Model {@link TaurusBaseComponent.model}. + * - Default implementation use {@link ComponentModel.isModified} for deep Comparison of specific fields. + * - Override it if you want to change default behavior. + * + *

+ * Be cautious about your changes and intents! + * Examples what can wrong comparison do? + *

+ * + * 1. Infinite lifecycle hooks invocation, where consequences are: performance deterioration or application freeze. + * 2. Prevent lifecycle hooks invocation, where consequences are: your Data never arrive to your Component fields. + * + * @protected + */ + protected isModelModified(model: ComponentModel): boolean { + return model.isModified(this.model); + } + + /** + * ** Initialize Route reuse strategy for Component in context of Taurus NgRx. + * - Turns on listener for Activated params change and if detects mutation + * sets current model in current RoutePathSegment to idle, + * and bind for new model stream to the new RoutePathSegment. + * - New feature this is completely backward compatible, + * and it can be turned on with feature flag per Component Class. + * + * @protected + */ + protected initializeRouteReuse(): void { + if (!this.enforceRouteReuse) { + return; + } + + let previousParams: Params; + + this.subscriptions.push( + this.activatedRoute + .params + .subscribe((params) => { + if (CollectionsUtil.isNil(previousParams)) { + previousParams = params; + + return; + } + + if (!CollectionsUtil.isEqual(previousParams, params)) { + previousParams = params; + + const isRemoveSuccessful = this.removeSubscriptionRef(this._modelSubscription); + if (isRemoveSuccessful) { + // set current RoutePathSegment model state to idle + this.setStateIdle(); + // bind new model stream to new RoutePathSegment + this.bindModel(); + } + } + }) + ); + } + + /** + * ** Invoke Taurus NgRx Redux Component lifecycle hook. + * + * - Lifecycle hooks are invoked only if implementation they are found as implemented in subclasses. + * - Returns true if method is found and executed, otherwise false. + * + * @private + */ + // eslint-disable-next-line @typescript-eslint/member-ordering + private static _executeTaurusComponentHook( + instance: TaurusBaseComponent, + method: keyof TaurusComponentHooks, + model: ComponentModel): boolean { + + if (CollectionsUtil.isFunction(instance[method])) { + const currentTask = model.getTask(); + + try { + if (CollectionsUtil.isString(currentTask)) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + instance[method](model, currentTask); + } else { + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + instance[method](model); + } + } catch (e) { + console.error(`Taurus NgRx Redux failed to execute lifecycle hook "${ method }"!`, e); + } + + return true; + } + + return false; + } +} diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/index.ts new file mode 100644 index 0000000000..ec2d13fdc8 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/index.ts @@ -0,0 +1,11 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './base'; +export * from './model'; +export * from './services'; +export * from './state'; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/model/component-model.comparable.spec.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/model/component-model.comparable.spec.ts new file mode 100644 index 0000000000..22092047f4 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/model/component-model.comparable.spec.ts @@ -0,0 +1,235 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { CollectionsUtil } from '../../../utils'; + +import { ComparableImpl } from '../../../common'; + +import { RouterState, RouteSegments, RouteState } from '../../router'; + +import { ComponentState, ComponentStateImpl, IDLE, LOADED } from './state'; + +import { ComponentModel } from './component.model'; + +import { ComponentModelComparable } from './component-model.comparable'; + +describe('ComponentModelComparable', () => { + it('should verify instance is created', () => { + // When + const instance = new ComponentModelComparable(null); + + // Then + expect(instance).toBeDefined(); + }); + + describe('Statics::', () => { + describe('Methods::', () => { + describe('|of|', () => { + it('should verify factory method will create instance', () => { + // When + const instance = ComponentModelComparable.of(null); + + // Then + expect(instance).toBeInstanceOf(ComponentModelComparable); + expect(instance).toBeInstanceOf(ComparableImpl); + }); + }); + }); + }); + + describe('Methods::', () => { + let componentState: ComponentState; + let routerState: RouterState; + let modelComparable: ComponentModelComparable; + + beforeEach(() => { + componentState = ComponentStateImpl.of({ + id: 'testId', + status: IDLE, + data: new Map([['countries', ['aCountry', 'bCountry', 'cCountry']]]) + }); + routerState = RouterState.of( + RouteState.of( + RouteSegments.of( + 'entity/117', + {}, + { entityId: '107' }, + { search: 'test-team-107' }, + RouteSegments.of( + 'domain/context', + {}, + {}, + { search: 'test-team-107' } + ) + ), + 'domain/context/entity/117' + ), + 43 + ); + modelComparable = ComponentModelComparable.of(ComponentModel.of(componentState, routerState)); + }); + + describe('|compare|', () => { + it('should verify will return -1 when stored comparable value is not instance of ComponentModel', () => { + // Given + modelComparable = ComponentModelComparable.of(null); + + // When + const compareV = modelComparable.compare(ComponentModelComparable.of(undefined)); + + // Then + expect(compareV).toEqual(-1); + }); + + it('should verify will return -1 when provided comparable is not instance of expected', () => { + // When + const compareV = modelComparable.compare(ComponentModelComparable.of(null)); + + // Then + expect(compareV).toEqual(-1); + }); + + it('should verify will return -1 when no comparable model provided', () => { + // When + const compareV = modelComparable.compare(null); + + // Then + expect(compareV).toEqual(-1); + }); + + it('should verify will return 0 when ComponentModel ref is same', () => { + // Given + const isEqualSpy = spyOn(CollectionsUtil, 'isEqual').and.callThrough(); + + // When + modelComparable.value.withPage(10, 100); + modelComparable.value.getComponentState().uiState.set('submitBtn', { state: 'submitted' }); + const compareV = modelComparable.compare(modelComparable); + + // Then + expect(compareV).toEqual(0); + expect(isEqualSpy).not.toHaveBeenCalled(); + }); + + it('should verify will return -1 when status is different', () => { + // Given + const componentStateComparable = ComponentStateImpl.of({ + ...componentState, + status: LOADED + }); + const modelComparableDI = ComponentModelComparable.of(ComponentModel.of(componentStateComparable, routerState)); + + // When + const compareV = modelComparable.compare(modelComparableDI); + + // Then + expect(compareV).toEqual(-1); + }); + + it('should verify will return -1 when task is different', () => { + // Given + const componentStateComparable = ComponentStateImpl.of({ + ...componentState, + task: 'patch_entity' + }); + const modelComparableDI = ComponentModelComparable.of(ComponentModel.of(componentStateComparable, routerState)); + + // When + const compareV = modelComparable.compare(modelComparableDI); + + // Then + expect(compareV).toEqual(-1); + }); + + it('should verify will return 0 when navigationId is different', () => { + // Given + const routerStateComparable = RouterState.of( + routerState.state, + 90 + ); + const modelComparableDI = ComponentModelComparable.of(ComponentModel.of(componentState, routerStateComparable)); + + // When + const compareV = modelComparable.compare(modelComparableDI); + + // Then + expect(compareV).toEqual(0); + }); + + it('should verify will return -1 when error is different', () => { + // Given + const isEqualSpy = spyOn(CollectionsUtil, 'isEqual').and.callThrough(); + const componentStateComparable = ComponentStateImpl.of({ + ...componentState, + error: new Error('new Error()') + }); + const modelComparableDI = ComponentModelComparable.of(ComponentModel.of(componentStateComparable, routerState)); + + // When + const compareV = modelComparable.compare(modelComparableDI); + + // Then + expect(compareV).toEqual(-1); + expect(isEqualSpy).not.toHaveBeenCalled(); + }); + + it('should verify will return 0 when data ref is same', () => { + // Given + const isEqualSpy = spyOn(CollectionsUtil, 'isEqual').and.callThrough(); + const componentStateComparable = ComponentStateImpl.of({ + ...componentState, + data: componentState.data + }); + const modelComparableDI = ComponentModelComparable.of(ComponentModel.of(componentStateComparable, routerState)); + + // When + const compareV = modelComparable.compare(modelComparableDI); + + // Then + expect(compareV).toEqual(0); + expect(isEqualSpy).not.toHaveBeenCalled(); + }); + + it('should verify will return 0 when ref is different but content is same', () => { + // Given + const areMapsEqualSpy = spyOn(CollectionsUtil, 'areMapsEqual').and.callThrough(); + const componentStateComparable = ComponentStateImpl.of({ + ...componentState, + data: new Map([['countries', ['aCountry', 'bCountry', 'cCountry']]]) + }); + const modelComparableDI = ComponentModelComparable.of(ComponentModel.of(componentStateComparable, routerState)); + + // When + const compareV = modelComparable.compare(modelComparableDI); + + // Then + expect(compareV).toEqual(0); + expect(areMapsEqualSpy).toHaveBeenCalledWith(componentState.data, componentStateComparable.data); + }); + + it('should verify will return -1 when ref is different and content is different', () => { + // Given + const areMapsEqualSpy = spyOn(CollectionsUtil, 'areMapsEqual').and.callThrough(); + const componentStateComparable = ComponentStateImpl.of({ + ...componentState, + data: new Map([ + ['countries', ['aCountry', 'bCountry', 'cCountry']], + ['users', ['aUser', 'bUser', 'cUser']] + ]) + }); + const modelComparableDI = ComponentModelComparable.of(ComponentModel.of(componentStateComparable, routerState)); + + // When + const compareV = modelComparable.compare(modelComparableDI); + + // Then + expect(compareV).toEqual(-1); + expect(areMapsEqualSpy).toHaveBeenCalledWith(componentState.data, componentStateComparable.data); + }); + }); + }); +}); diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/model/component-model.comparable.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/model/component-model.comparable.ts new file mode 100644 index 0000000000..9a6c427de7 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/model/component-model.comparable.ts @@ -0,0 +1,75 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { CollectionsUtil } from '../../../utils'; + +import { Comparable, ComparableImpl } from '../../../common'; + +import { AbstractComponentModel } from './component.model.interface'; + +export class ComponentModelComparable extends ComparableImpl { + /** + * ** Constructor. + */ + constructor(model: AbstractComponentModel) { + super(model); + } + + /** + * ** Factory method. + */ + static override of(model: AbstractComponentModel): ComponentModelComparable { + return new ComponentModelComparable(model); + } + + /** + * @inheritDoc + */ + override compare(comparable: Comparable): number { + if (comparable instanceof ComponentModelComparable) { + return this._compareEquality(comparable); + } else { + return -1; + } + } + + private _compareEquality(comparable: ComponentModelComparable): number { + if (!(this.value instanceof AbstractComponentModel)) { + return -1; + } + + if (!(comparable.value instanceof AbstractComponentModel)) { + return -1; + } + + if (this.value === comparable.value) { + return 0; + } + + if (this.value.status !== comparable.value.status) { + return -1; + } + + if (this.value.getTask() !== comparable.value.getTask()) { + return -1; + } + + if (this.value.getComponentState().error !== comparable.value.getComponentState().error) { + return -1; + } + + if (this.value.getComponentState().data === comparable.value.getComponentState().data) { + return 0; + } + + if (CollectionsUtil.areMapsEqual(this.value.getComponentState().data, comparable.value.getComponentState().data)) { + return 0; + } + + return -1; + } +} diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/model/component.model.interface.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/model/component.model.interface.ts new file mode 100644 index 0000000000..1c46014743 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/model/component.model.interface.ts @@ -0,0 +1,186 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import { ApiPredicate } from '../../../common'; + +import { RouterState } from '../../router'; + +import { ComponentState, StatusType } from './state'; + +/** + * ** Interface for Model for all Components. + * + * + */ +export abstract class AbstractComponentModel { + + /** + * ** Return RouterState. + */ + abstract get routerState(): RouterState; + + /** + * ** Return Status from ComponentState. + */ + abstract get status(): StatusType; + + /** + * ** Return routePath from RouterState. + */ + abstract get routePath(): string; + + /** + * ** Return the ComponentState. + */ + abstract getComponentState(): ComponentState; + + /** + * ** Set Search query to ComponentState and get Model reference again. + *

+ * - Ready for method chaining. + *

+ */ + abstract withSearch(search: string): this; + + /** + * ** Set Page to ComponentState and get Model reference again. + *

+ * - Ready for method chaining. + *

+ */ + abstract withPage(page: number, size: number): this; + + /** + * ** Set Filter to ComponentState and get Model reference again. + *

+ * - Ready for method chaining. + *

+ */ + abstract withFilter(filterPredicates: ApiPredicate[]): this; + + /** + * ** Set Request params to ComponentState and get Model reference again. + *

+ * - Ready for method chaining. + *

+ */ + abstract withRequestParam(key: string, value: any): this; + + /** + * ** Set Data (bound to key identifier) to ComponentState and get Model reference again. + *

+ * - Ready for method chaining. + *

+ */ + abstract withData(key: string, data: any): this; + + /** + * ** Set Task identifier to ComponentState and get Model reference again. + * + * - Should be set only after data comes from the API, action is asynchronous. + * - Don't set this property during action dispatch + * + *

+ * - Ready for method chaining. + *

+ */ + abstract withTask(taskIdentifier: string): this; + + /** + * ** Clear Task identifier from ComponentState and get Model reference again. + *

+ * - Ready for method chaining. + *

+ */ + abstract clearTask(): this; + + /** + * ** Returns latest Task from ComponentState. + */ + abstract getTask(): string; + + /** + * ** Returns latest Task unique identifier from ComponentState. + */ + abstract getTaskUniqueIdentifier(): string; + + /** + * ** Set Error to ComponentState and get Model reference again. + *

+ * - Ready for method chaining. + *

+ */ + abstract withError(error: Error): this; + + /** + * ** Clear Error from ComponentState and get Model reference again. + *

+ * - Ready for method chaining. + *

+ */ + abstract clearError(): this; + + /** + * ** Set UiState for given identifier to ComponentState and get Model reference again. + *

+ * - Ready for method chaining. + *

+ */ + abstract withUiState(key: string, value: any): this; + + /** + * ** Returns UiState for given identifier. + */ + abstract getUiState(key: string): T; + + /** + * ** Set Component State status to IDLE. + *

+ * - Ready for method chaining. + *

+ */ + abstract withStatusIdle(): this; + + /** + * ** Set Component State status to LOADING. + *

+ * - Ready for method chaining. + *

+ */ + abstract withStatusLoading(): this; + + /** + * ** Set Component State status to LOADED. + *

+ * - Ready for method chaining. + *

+ */ + abstract withStatusLoaded(): this; + + /** + * ** Set Component State status to FAILED. + *

+ * - Ready for method chaining. + *

+ */ + abstract withStatusFailed(): this; + + /** + * ** Update Component State with Partial Component State patch. + *

+ * - Ready for method chaining. + *

+ */ + abstract updateComponentState(patchState: Partial): this; + + /** + * ** Filter method that analyze if current Model is different from given one. + */ + abstract isModified(model: AbstractComponentModel): boolean; +} diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/model/component.model.spec.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/model/component.model.spec.ts new file mode 100644 index 0000000000..557d4cab14 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/model/component.model.spec.ts @@ -0,0 +1,449 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { ASC, RequestFilterImpl, RequestPageImpl } from '../../../common'; + +import { RouterState, RouteSegments, RouteState } from '../../router'; + +import { ComponentState, ComponentStateImpl, FAILED, IDLE, INITIALIZED, LOADED, LOADING } from './state'; + +import { ComponentModel } from './component.model'; + +import { ComponentModelComparable } from './component-model.comparable'; + +describe('ComponentModel', () => { + it('should verify instance is created', () => { + // When + const instance = new ComponentModel(null, null); + + // Then + expect(instance).toBeDefined(); + }); + + describe('Statics::', () => { + describe('Methods::()', () => { + describe('|of|', () => { + it('should verify factory method will create instance', () => { + // Given + const componentState = { id: 'testId', status: IDLE } as ComponentState; + const routerState = RouterState.of( + { + routeSegments: { + routePath: 'domain/context' + } + } as RouteState, + 9 + ); + + // When + const instance = ComponentModel.of(componentState, routerState); + + // Then + expect(instance).toBeInstanceOf(ComponentModel); + }); + }); + }); + }); + + describe('Getters/Setters::', () => { + let componentState: ComponentState; + let routerState: RouterState; + + beforeEach(() => { + componentState = { id: 'testId', status: IDLE } as ComponentState; + routerState = RouterState.of( + { + routeSegments: { + routePath: 'domain/context' + } + } as RouteState, + 9 + ); + }); + + describe('|GET -> routerState|', () => { + it('should verify will return correct value', () => { + // Given + const model = ComponentModel.of(componentState, routerState); + + // When + const _routerState = model.routerState; + + // Then + expect(_routerState).toBe(routerState); + }); + }); + + describe('|GET -> status|', () => { + it('should verify will return correct value', () => { + // Given + const model = ComponentModel.of(componentState, routerState); + + // When + const status = model.status; + + // Then + expect(status).toEqual(IDLE); + }); + }); + + describe('|GET -> routePath|', () => { + it('should verify will return correct value from componentState', () => { + // Given + componentState = { ...componentState, routePath: 'entity/15' }; + const model = ComponentModel.of(componentState, routerState); + + // When + const routePath = model.routePath; + + // Then + expect(routePath).toEqual('entity/15'); + }); + + it('should verify will return correct value from routerState', () => { + // Given + const model = ComponentModel.of(componentState, routerState); + + // When + const routePath = model.routePath; + + // Then + expect(routePath).toEqual('domain/context'); + }); + }); + }); + + describe('Methods::', () => { + let componentState: ComponentState; + let routerState: RouterState; + let model: ComponentModel; + + beforeEach(() => { + componentState = ComponentStateImpl.of({ + id: 'testId', + status: IDLE, + data: new Map([['countries', ['aCountry', 'bCountry', 'cCountry']]]) + }); + routerState = RouterState.of( + RouteState.of( + RouteSegments.of( + 'entity/21', + { entityId: '21' }, + { search: 'test-team' }, + RouteSegments.of('domain/context', + {}, + { search: 'test-team' } + ) + ), + 'domain/context/entity/21' + ), + 9 + ); + model = ComponentModel.of(componentState, routerState); + }); + + describe('|getComponentState|', () => { + it('should verify will return ComponentState', () => { + // When + const _componentState = model.getComponentState(); + + // Then + expect(_componentState).toBe(componentState); + }); + }); + + describe('|withSearch|', () => { + it('should verify will invoke correct method with correct payload', () => { + // Given + const updateSpy = spyOn(model, 'updateComponentState').and.callThrough(); + + // When + const ref = model.withSearch('metadataSearchParam'); + + // Then + expect(ref).toBe(model); + expect(updateSpy).toHaveBeenCalledWith({ search: 'metadataSearchParam' }); + }); + }); + + describe('|withPage|', () => { + it('should verify will invoke correct method with correct payload', () => { + // Given + const updateSpy = spyOn(model, 'updateComponentState').and.callThrough(); + + // When + const ref = model.withPage(6, 18); + + // Then + expect(ref).toBe(model); + expect(updateSpy).toHaveBeenCalledWith({ page: RequestPageImpl.of(6, 18) }); + }); + }); + + describe('|withFilter|', () => { + it('should verify will invoke correct method with correct payload', () => { + // Given + const updateSpy = spyOn(model, 'updateComponentState').and.callThrough(); + + // When + const ref = model.withFilter([{ pattern: 'test*', property: 'config.path', sort: ASC }]); + + // Then + expect(ref).toBe(model); + expect(updateSpy).toHaveBeenCalledWith({ + filter: RequestFilterImpl.of({ + pattern: 'test*', + property: 'config.path', + sort: ASC + }) + }); + }); + }); + + describe('|withRequestParam|', () => { + it('should verify will set data to Request param map', () => { + // Given + const attributes = ['topBrand', 'extraSize', 'largeNumber']; + const order = { sort: ASC, path: 'config.user' }; + + // When + const ref = model + .withRequestParam('attributes', attributes) + .withRequestParam('order', order); + + // Then + expect(ref).toBe(model); + expect(model.getComponentState().requestParams).toEqual( + new Map([['attributes', [...attributes]], ['order', { ...order }]]) + ); + }); + }); + + describe('|withData|', () => { + it('should verify will set data to Data map', () => { + // Given + const users = ['aUser', 'bUser', 'cUser']; + + // When + const ref = model.withData('users', users); + + // Then + expect(ref).toBe(model); + expect(model.getComponentState().data).toEqual( + new Map([['countries', ['aCountry', 'bCountry', 'cCountry']], ['users', users]]) + ); + }); + }); + + describe('|withTask|', () => { + it('should verify will set Task to ComponentState', () => { + // Given + const task = 'delete_entity'; + + // Then 1 + expect(model.getComponentState().task).not.toEqual(task); + + // When + const ref = model.withTask(task); + + // Then 2 + expect(ref).toBe(model); + expect(model.getComponentState().task).toEqual(task); + }); + }); + + describe('|getTask|', () => { + it('should verify will return Task from ComponentState', () => { + // Given + const task = 'search_users'; + const taskIdentifier = `${ task } __ ${ new Date().toISOString() }`; + componentState = ComponentStateImpl.of({ + ...componentState, + task: taskIdentifier + }); + model = ComponentModel.of(componentState, routerState); + + // When + const res = model.getTask(); + + // Then + expect(res).toBe(task); + }); + }); + + describe('|withError|', () => { + it('should verify will invoke correct method with correct payload', () => { + // Given + const error = new Error('random error'); + const updateSpy = spyOn(model, 'updateComponentState').and.callThrough(); + + // When + const ref = model.withError(error); + + // Then + expect(ref).toBe(model); + expect(updateSpy).toHaveBeenCalledWith({ error }); + }); + }); + + describe('|clearError|', () => { + it('should verify will invoke correct method with correct payload', () => { + // Given + const updateSpy = spyOn(model, 'updateComponentState').and.callThrough(); + + // When + const ref = model.clearError(); + + // Then + expect(ref).toBe(model); + expect(updateSpy).toHaveBeenCalledWith({ error: null }); + }); + }); + + describe('|withUiState|', () => { + it('should verify will set data to Data map', () => { + // Given + const btnState = { active: true }; + + // When + const ref = model.withUiState('btnOk', btnState); + + // Then + expect(ref).toBe(model); + expect(model.getComponentState().uiState).toEqual(new Map([['btnOk', btnState]])); + }); + }); + + describe('|getUiState|', () => { + it('should verify will return uiState from ComponentState for given key', () => { + // Given + const btnState = { active: true }; + componentState = ComponentStateImpl.of({ + ...componentState, + uiState: new Map([['btnOk', btnState]]) + }); + model = ComponentModel.of(componentState, routerState); + + // When + const uiState = model.getUiState('btnOk'); + + // Then + expect(uiState).toBe(btnState); + }); + }); + + describe('|withStatusIdle|', () => { + it('should verify will invoke correct method with correct payload', () => { + // Given + const updateSpy = spyOn(model, 'updateComponentState').and.callThrough(); + + // When + const ref = model.withStatusIdle(); + + // Then + expect(ref).toBe(model); + expect(updateSpy).toHaveBeenCalledWith({ status: IDLE }); + }); + }); + + describe('|withStatusLoading|', () => { + it('should verify will invoke correct method with correct payload', () => { + // Given + const updateSpy = spyOn(model, 'updateComponentState').and.callThrough(); + + // When + const ref = model.withStatusLoading(); + + // Then + expect(ref).toBe(model); + expect(updateSpy).toHaveBeenCalledWith({ status: LOADING }); + }); + }); + + describe('|withStatusLoaded|', () => { + it('should verify will invoke correct method with correct payload', () => { + // Given + const updateSpy = spyOn(model, 'updateComponentState').and.callThrough(); + + // When + const ref = model.withStatusLoaded(); + + // Then + expect(ref).toBe(model); + expect(updateSpy).toHaveBeenCalledWith({ status: LOADED }); + }); + }); + + describe('|withStatusFailed|', () => { + it('should verify will invoke correct method with correct payload', () => { + // Given + const updateSpy = spyOn(model, 'updateComponentState').and.callThrough(); + + // When + const ref = model.withStatusFailed(); + + // Then + expect(ref).toBe(model); + expect(updateSpy).toHaveBeenCalledWith({ status: FAILED }); + }); + }); + + describe('|updateComponentState|', () => { + it('should verify will update local ComponentState', () => { + // Given + const assertionComponentState = ComponentStateImpl.of({ + ...componentState, + status: INITIALIZED, + id: 'test-component-1234', + search: 'teamUSA*', + page: RequestPageImpl.of(9, 45) + }); + + // When + const ref = model.updateComponentState({ + status: INITIALIZED, + id: 'test-component-1234', + search: 'teamUSA*', + page: RequestPageImpl.of(9, 45) + }); + + // Then + expect(ref).toBe(model); + expect(model.getComponentState()).toEqual(assertionComponentState); + }); + }); + + describe('|isModified|', () => { + it('should verify will invoke correct methods', () => { + // Given + const componentModelComparableStub = jasmine.createSpyObj('comparable', ['notEqual']); + componentModelComparableStub.notEqual.and.returnValue(false); + const factoryOfSpy = spyOn(ComponentModelComparable, 'of').and.returnValue(componentModelComparableStub); + + const comparableState = ComponentStateImpl.of({ + ...componentState, + data: new Map([ + ['countries', ['aCountry', 'bCountry', 'cCountry']], + ['users', ['aUser', 'bUser', 'cUser']] + ]) + }); + const comparableModel = ComponentModel.of(comparableState, routerState); + + // When + const isModified = model.isModified(comparableModel); + + // Then + expect(isModified).toBeFalse(); + expect(factoryOfSpy).toHaveBeenCalledTimes(2); + expect(factoryOfSpy.calls.argsFor(0)).toEqual([model]); + expect(factoryOfSpy.calls.argsFor(1)).toEqual([comparableModel]); + expect(componentModelComparableStub.notEqual).toHaveBeenCalledTimes(1); + expect(componentModelComparableStub.notEqual).toHaveBeenCalledWith(componentModelComparableStub); + }); + }); + }); +}); diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/model/component.model.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/model/component.model.ts new file mode 100644 index 0000000000..45c3163191 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/model/component.model.ts @@ -0,0 +1,250 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import { ApiPredicate, extractTaskFromIdentifier, RequestFilterImpl, RequestPageImpl } from '../../../common'; + +import { RouterState } from '../../router'; + +import { ComponentState, ComponentStateImpl, FAILED, IDLE, LOADED, LOADING, StatusType } from './state'; + +import { ComponentModelComparable } from './component-model.comparable'; + +import { AbstractComponentModel } from './component.model.interface'; + +/** + * ** Generic Model for all Components. + * + * + */ +export class ComponentModel extends AbstractComponentModel { + /** + * ** Constructor. + */ + constructor(protected _componentState: ComponentState, + protected _routerState: RouterState) { + super(); + } + + /** + * ** Factory method. + */ + static of(componentState: ComponentState, routerState: RouterState) { + return new ComponentModel(componentState, routerState); + } + + /** + * @inheritDoc + */ + get routerState(): RouterState { + return this._routerState; + } + + /** + * @inheritDoc + */ + get status(): StatusType { + return this.getComponentState().status; + } + + /** + * @inheritDoc + */ + get routePath(): string { + return this.getComponentState().routePath || + this.routerState.state.routeSegments.routePath; + } + + /** + * @inheritDoc + */ + getComponentState(): ComponentState { + return this._componentState; + } + + /** + * @inheritDoc + */ + withSearch(search: string) { + this.updateComponentState({ + search + }); + + return this; + } + + /** + * @inheritDoc + */ + withPage(page: number, size: number) { + this.updateComponentState({ + page: RequestPageImpl.of(page, size) + }); + + return this; + } + + /** + * @inheritDoc + */ + withFilter(filterPredicates: ApiPredicate[]) { + this.updateComponentState({ + filter: RequestFilterImpl.of(...filterPredicates) + }); + + return this; + } + + /** + * @inheritDoc + */ + withRequestParam(key: string, value: any) { + this.getComponentState() + .requestParams + .set(key, value); + + return this; + } + + /** + * @inheritDoc + */ + withData(key: string, data: any) { + this.getComponentState() + .data + .set(key, data); + + return this; + } + + /** + * @inheritDoc + */ + withTask(taskIdentifier: string) { + this.updateComponentState({ task: taskIdentifier }); + + return this; + } + + /** + * @inheritDoc + */ + clearTask() { + this.updateComponentState({ task: null }); + + return this; + } + + /** + * @inheritDoc + */ + getTask(): string { + return extractTaskFromIdentifier(this.getComponentState().task); + } + + /** + * @inheritDoc + */ + getTaskUniqueIdentifier(): string { + return this.getComponentState().task; + } + + /** + * @inheritDoc + */ + withError(error: Error) { + this.updateComponentState({ error }); + + return this; + } + + /** + * @inheritDoc + */ + clearError() { + this.updateComponentState({ error: null }); + + return this; + } + + /** + * @inheritDoc + */ + withUiState(key: string, value: any) { + this.getComponentState() + .uiState + .set(key, value); + + return this; + } + + /** + * @inheritDoc + */ + getUiState(key: string): T { + return this.getComponentState() + .uiState + .get(key) as T; + } + + /** + * @inheritDoc + */ + withStatusIdle() { + this.updateComponentState({ status: IDLE }); + + return this; + } + + /** + * @inheritDoc + */ + withStatusLoading() { + this.updateComponentState({ status: LOADING }); + + return this; + } + + /** + * @inheritDoc + */ + withStatusLoaded() { + this.updateComponentState({ status: LOADED }); + + return this; + } + + /** + * @inheritDoc + */ + withStatusFailed() { + this.updateComponentState({ status: FAILED }); + + return this; + } + + /** + * @inheritDoc + */ + updateComponentState(patchState: Partial) { + this._componentState = ComponentStateImpl.of({ + ...this.getComponentState(), + ...patchState + }); + + return this; + } + + /** + * @inheritDoc + */ + isModified(model: ComponentModel): boolean { + return ComponentModelComparable.of(this) + .notEqual(ComponentModelComparable.of(model)); + } +} diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/model/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/model/index.ts new file mode 100644 index 0000000000..9409d7dea5 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/model/index.ts @@ -0,0 +1,10 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './state'; +export * from './component.model.interface'; +export * from './component.model'; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/model/state/component-state.model.spec.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/model/state/component-state.model.spec.ts new file mode 100644 index 0000000000..2143716c84 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/model/state/component-state.model.spec.ts @@ -0,0 +1,275 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { ApiPredicate, ASC, RequestFilterImpl, RequestOrderImpl, RequestPageImpl } from '../../../../common'; + +import { IDLE, LOADED } from './component-status.model'; + +import { ComponentState, ComponentStateImpl, LiteralComponentState } from './component-state.model'; + +describe('ComponentStateImpl', () => { + let partialStateMock: Partial; + let stateMock: ComponentStateImpl; + let literalStateMock: LiteralComponentState; + + beforeEach(() => { + const apiPredicate1: ApiPredicate = { sort: ASC, property: 'test.property.10', pattern: 'test.pattern.20' }; + const apiPredicate2: ApiPredicate = { sort: ASC, property: 'test.property.30', pattern: 'test.pattern.40' }; + + const uiState1 = { order: 'ASC' }; + const uiState2 = [1, 2, 3]; + + const data1 = { data: { name: 'aName' } }; + const data2 = { content: { page: { size: 10, page: 2 } } }; + + const dateNowISO = new Date().toISOString(); + + partialStateMock = { + id: 'testId', + status: LOADED, + navigationId: 11, + error: null, + search: 'testSearch', + routePath: 'domain/context/entity/10', + routePathSegments: ['domain/context', 'entity/10'], + page: new RequestPageImpl(5, 10), + order: new RequestOrderImpl(apiPredicate1), + filter: new RequestFilterImpl(apiPredicate2), + requestParams: new Map([ + ['test_param.1', apiPredicate1], + ['test_param.2', apiPredicate2] + ]), + task: `delete_user __ ${ dateNowISO }`, + uiState: new Map([ + ['test_uiState.1', uiState1], + ['test_uiState.2', uiState2] + ]), + data: new Map([ + ['test_data.1', data1], + ['test_data.2', data2] + ]) + }; + + stateMock = new ComponentStateImpl(partialStateMock); + + literalStateMock = { + id: 'testId', + status: LOADED, + navigationId: 11, + error: null, + search: 'testSearch', + routePath: 'domain/context/entity/10', + routePathSegments: ['domain/context', 'entity/10'], + page: { pageNumber: 5, pageSize: 10 }, + order: [apiPredicate1], + filter: [apiPredicate2], + requestParams: { + 'test_param.1': apiPredicate1, + 'test_param.2': apiPredicate2 + }, + task: `delete_user __ ${ dateNowISO }`, + uiState: { + 'test_uiState.1': uiState1, + 'test_uiState.2': uiState2 + }, + data: { + 'test_data.1': data1, + 'test_data.2': data2 + } + }; + }); + + it('should verify instance is created', () => { + // When + const instance = new ComponentStateImpl({}); + + // Then + expect(instance).toBeDefined(); + }); + + it('should verify provided value will be correctly assigned', () => { + // When + const instance = new ComponentStateImpl(partialStateMock); + + // Then + expect(instance.id).toEqual(partialStateMock.id); + expect(instance.status).toEqual(partialStateMock.status); + expect(instance.navigationId).toEqual(partialStateMock.navigationId); + expect(instance.error).toEqual(partialStateMock.error); + expect(instance.search).toEqual(partialStateMock.search); + expect(instance.routePath).toEqual(partialStateMock.routePath); + expect(instance.routePathSegments).toBe(partialStateMock.routePathSegments); + expect(instance.page).toBe(partialStateMock.page); + expect(instance.order).toBe(partialStateMock.order); + expect(instance.filter).toBe(partialStateMock.filter); + expect(instance.requestParams).toBe(partialStateMock.requestParams); + expect(instance.task).toEqual(partialStateMock.task); + expect(instance.uiState).toBe(partialStateMock.uiState); + expect(instance.data).toBe(partialStateMock.data); + }); + + it('should verify will correctly assign default values', () => { + // When + const instance = new ComponentStateImpl({}); + + // Then + expect(instance).toBeDefined(); + expect(instance.id).toBeUndefined(); + expect(instance.status).toEqual(IDLE); + expect(instance.navigationId).toEqual(null); + expect(instance.routePath).toBeUndefined(); + expect(instance.routePathSegments).toEqual([]); + expect(instance.search).toEqual(''); + expect(instance.page).toEqual(RequestPageImpl.empty()); + expect(instance.order).toEqual(RequestOrderImpl.empty()); + expect(instance.filter).toEqual(RequestFilterImpl.empty()); + expect(instance.requestParams).toBeInstanceOf(Map); + expect(instance.error).toEqual(null); + expect(instance.task).toEqual(null); + expect(instance.data).toBeInstanceOf(Map); + expect(instance.uiState).toBeInstanceOf(Map); + }); + + describe('Statics::', () => { + describe('Methods::', () => { + describe('|of|', () => { + it('should verify factory method will create instance', () => { + // When + const instance = ComponentStateImpl.of(partialStateMock); + + // Then + expect(instance.id).toEqual(partialStateMock.id); + expect(instance.status).toEqual(partialStateMock.status); + expect(instance.navigationId).toEqual(partialStateMock.navigationId); + expect(instance.error).toEqual(partialStateMock.error); + expect(instance.search).toEqual(partialStateMock.search); + expect(instance.routePath).toEqual(partialStateMock.routePath); + expect(instance.routePathSegments).toBe(partialStateMock.routePathSegments); + expect(instance.page).toBe(partialStateMock.page); + expect(instance.order).toBe(partialStateMock.order); + expect(instance.filter).toBe(partialStateMock.filter); + expect(instance.requestParams).toBe(partialStateMock.requestParams); + expect(instance.task).toEqual(partialStateMock.task); + expect(instance.uiState).toBe(partialStateMock.uiState); + expect(instance.data).toBe(partialStateMock.data); + }); + }); + + describe('|fromLiteralComponentState|', () => { + it('should verify will create instance of ComponentStateImpl from LiteralComponentState', () => { + // When + const instance = ComponentStateImpl.fromLiteralComponentState(literalStateMock); + + // Then + expect(instance).toEqual(stateMock); + expect(instance.order.criteria[0]).toBe(stateMock.order.criteria[0]); + expect(instance.filter.criteria[0]).toBe(stateMock.filter.criteria[0]); + expect(instance.requestParams.get('test_param.1')).toBe(stateMock.requestParams.get('test_param.1')); + expect(instance.requestParams.get('test_param.2')).toBe(stateMock.requestParams.get('test_param.2')); + expect(instance.uiState.get('test_uiState.1')).toBe(stateMock.uiState.get('test_uiState.1')); + expect(instance.uiState.get('test_uiState.2')).toBe(stateMock.uiState.get('test_uiState.2')); + expect(instance.data.get('test_data.1')).toBe(stateMock.data.get('test_data.1')); + expect(instance.data.get('test_data.2')).toBe(stateMock.data.get('test_data.2')); + }); + }); + + describe('|cloneDeepLiteral|', () => { + it('should verify will create deep clone from LiteralComponentState', () => { + // When + const instance = ComponentStateImpl.cloneDeepLiteral(literalStateMock); + + // Then + expect(instance).toEqual(literalStateMock); + expect(instance.order[0]).not.toBe(literalStateMock.order[0]); + expect(instance.filter[0]).not.toBe(literalStateMock.filter[0]); + expect(instance.requestParams['test_param.1']).not.toBe(literalStateMock.requestParams['test_param.1']); + expect(instance.requestParams['test_param.2']).not.toBe(literalStateMock.requestParams['test_param.2']); + expect(instance.uiState['test_uiState.1']).not.toBe(literalStateMock.uiState['test_uiState.1']); + expect(instance.uiState['test_uiState.2']).not.toBe(literalStateMock.uiState['test_uiState.2']); + expect(instance.data['test_data.1']).not.toBe(literalStateMock.data['test_data.1']); + expect(instance.data['test_data.2']).not.toBe(literalStateMock.data['test_data.2']); + }); + }); + }); + }); + + describe('Methods::', () => { + describe('|toLiteral|', () => { + it('should verify will create LiteralComponentState', () => { + // When + const instance = stateMock.toLiteral(); + + // Then + expect(instance).toEqual(literalStateMock); + expect(instance.order[0]).toBe(literalStateMock.order[0]); + expect(instance.filter[0]).toBe(literalStateMock.filter[0]); + expect(instance.requestParams['test_param.1']).toBe(literalStateMock.requestParams['test_param.1']); + expect(instance.requestParams['test_param.2']).toBe(literalStateMock.requestParams['test_param.2']); + expect(instance.uiState['test_uiState.1']).toBe(literalStateMock.uiState['test_uiState.1']); + expect(instance.uiState['test_uiState.2']).toBe(literalStateMock.uiState['test_uiState.2']); + expect(instance.data['test_data.1']).toBe(literalStateMock.data['test_data.1']); + expect(instance.data['test_data.2']).toBe(literalStateMock.data['test_data.2']); + }); + }); + + describe('|toLiteralDeepClone|', () => { + it('should verify will create LiteralComponentState deep cloned', () => { + // When + const instance = stateMock.toLiteralDeepClone(); + + // Then + expect(instance).toEqual(literalStateMock); + expect(instance.order[0]).not.toBe(literalStateMock.order[0]); + expect(instance.filter[0]).not.toBe(literalStateMock.filter[0]); + expect(instance.requestParams['test_param.1']).not.toBe(literalStateMock.requestParams['test_param.1']); + expect(instance.requestParams['test_param.2']).not.toBe(literalStateMock.requestParams['test_param.2']); + expect(instance.uiState['test_uiState.1']).not.toBe(literalStateMock.uiState['test_uiState.1']); + expect(instance.uiState['test_uiState.2']).not.toBe(literalStateMock.uiState['test_uiState.2']); + expect(instance.data['test_data.1']).not.toBe(literalStateMock.data['test_data.1']); + expect(instance.data['test_data.2']).not.toBe(literalStateMock.data['test_data.2']); + }); + }); + + describe('|copy|', () => { + it('should verify will create copy from ComponentStateImpl', () => { + // When + const instance = stateMock.copy(); + + // Then + expect(instance).not.toBe(stateMock); + expect(instance).toEqual(stateMock); + }); + + it('should verify will merge provided State on top of original and return instance', () => { + // Given + const partialState: Partial = { + id: 'newId', + status: LOADED, + search: 'randomSearch', + routePath: 'domain/context/entity/20', + routePathSegments: ['domain/context', 'entity/20'], + data: new Map(), + uiState: new Map() + }; + + // When + const instance = stateMock.copy(partialState); + + // Then + expect(instance).not.toBe(stateMock); + expect(instance).not.toEqual(stateMock); + expect(instance.id).toEqual(partialState.id); + expect(instance.status).toEqual(partialState.status); + expect(instance.search).toEqual(partialState.search); + expect(instance.routePath).toEqual(partialState.routePath); + expect(instance.routePathSegments).toBe(partialState.routePathSegments); + expect(instance.data).toBe(partialState.data); + expect(instance.uiState).toBe(partialState.uiState); + }); + }); + }); +}); diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/model/state/component-state.model.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/model/state/component-state.model.ts new file mode 100644 index 0000000000..44c9652104 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/model/state/component-state.model.ts @@ -0,0 +1,438 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import { CollectionsUtil } from '../../../../utils'; + +import { + Copy, + Literal, + LiteralApiPredicates, + LiteralRequestPage, + RequestFilter, + RequestFilterImpl, + RequestOrder, + RequestOrderImpl, + RequestPage, + RequestPageImpl +} from '../../../../common'; + +import { IDLE, StatusType } from './component-status.model'; + +/** + * ** Literal Component State in purest format ready for Store persisting. + * + * + */ +export interface LiteralComponentState { + /** + * ** Identifier for Component State. + */ + readonly id: string; + + /** + * ** Status for Component State. + */ + readonly status: StatusType; + + /** + * ** Component State Data. + *

+ * - Free format Literal Object. + *

+ */ + readonly data?: { [key: string]: any }; + + /** + * ** Route path for current State. + */ + readonly routePath?: string; + + /** + * ** Route path segments for current State. + */ + readonly routePathSegments?: string[]; + + /** + * ** Search query for Http requests. + */ + readonly search?: string; + + /** + * ** Page for Http requests. + */ + readonly page?: LiteralRequestPage; + + /** + * ** Order for Http requests. + */ + readonly order?: LiteralApiPredicates; + + /** + * ** Filter for Http requests. + */ + readonly filter?: LiteralApiPredicates; + + /** + * ** Order for Http requests. + */ + readonly requestParams?: { [key: string]: any }; + + /** + * ** Task is property that give bi-directional refinement context. + * + * - Gives context to Effect through Action. + * - Gives context to Component through ComponentState (ComponentModel). + */ + readonly task?: string; + + /** + * ** Router NavigationId bound to this Component State. + */ + readonly navigationId?: number; + + /** + * ** Error that could happen in Stream manipulation down to the Components. + *

+ * - Ideal for storing Http errors, so Component could easily leverage that knowledge and show info for User. + *

+ */ + readonly error?: Error; + + /** + * ** Component State UiState, that holds all information for UiElements. + *

+ * - Free format Literal Object where key identifier could be (Component/Html Element) name/id/class etc... + *

+ */ + readonly uiState?: { [key: string]: any }; +} + +export interface ComponentState extends Literal, Copy { + /** + * ** Identifier for Component State. + */ + readonly id: string; + + /** + * ** Status for Component State. + */ + readonly status: StatusType; + + /** + * ** Component State Data. + *

+ * - Free format Map. + *

+ */ + readonly data?: Map; + + /** + * ** Route path for current State. + */ + readonly routePath?: string; + + /** + * ** Route Path Segments for current State. + */ + readonly routePathSegments?: string[]; + + /** + * ** Search query for Http requests. + */ + readonly search?: string; + + /** + * ** Page for Http requests. + */ + readonly page?: RequestPage; + + /** + * ** Order for Http requests. + */ + readonly order?: RequestOrder; + + /** + * ** Filter for Http requests. + */ + readonly filter?: RequestFilter; + + /** + * ** Map with different parameters for Http requests. + */ + readonly requestParams?: Map; + + /** + * ** Task is property that give bi-directional refinement context. + * + * - Gives context to Effect through Action. + * - Gives context to Component through ComponentState (ComponentModel). + */ + readonly task?: string; + + /** + * ** Router NavigationId bound to this Component State. + */ + readonly navigationId?: number; + + /** + * ** Error that could happen in Stream manipulation down to the Components. + *

+ * - Ideal for storing Http errors, so Component could easily leverage that knowledge and show info for User. + *

+ */ + readonly error?: Error; + + /** + * ** Component State UiState, that holds all information for UiElements. + *

+ * - Free format Map where key identifier could be (Component/Html Element) name/id/class etc... + *

+ */ + readonly uiState?: Map; + + /** + * @inheritDoc + */ + toLiteral(): LiteralComponentState; + + /** + * @inheritDoc + */ + toLiteralDeepClone(): LiteralComponentState; + + /** + * @inheritDoc + */ + copy(state?: Partial): ComponentState; +} + +/** + * ** ComponentState implementation will all methods and other utilities. + */ +export class ComponentStateImpl implements ComponentState { + /** + * @inheritDoc + */ + readonly id: string; + + /** + * @inheritDoc + */ + readonly status: StatusType; + + /** + * @inheritDoc + */ + readonly data: Map; + + /** + * @inheritDoc + */ + readonly routePath: string; + + /** + * @inheritDoc + */ + readonly routePathSegments: string[]; + + /** + * @inheritDoc + */ + readonly search: string; + + /** + * @inheritDoc + */ + readonly page: RequestPageImpl; + + /** + * @inheritDoc + */ + readonly order: RequestOrderImpl; + + /** + * @inheritDoc + */ + readonly filter: RequestFilterImpl; + + /** + * @inheritDoc + */ + readonly requestParams: Map; + + /** + * @inheritDoc + */ + readonly task: string; + + /** + * @inheritDoc + */ + readonly navigationId: number; + + /** + * @inheritDoc + */ + readonly error: Error; + + /** + * @inheritDoc + */ + readonly uiState: Map; + + /** + * ** Constructor. + * + *

+ * Important: + *

+ *

+ * If you add new Property in {@link LiteralComponentState}/{@link ComponentState} + *

    + *
  • + * Implement field in {@link ComponentStateImpl} and handle null/undefined, assign defaults (required for Collections). + *
  • + *
  • + * Copy/Clone process have to be handled manually (for performance gain) in methods: + * + * {@link ComponentStateImpl.fromLiteralComponentState} + * {@link ComponentStateImpl.cloneDeepLiteral} + * {@link ComponentStateImpl.toLiteral} + *
  • + *
+ *

+ */ + constructor(stateModelProp: Partial) { + const stateModel: Partial = CollectionsUtil.isDefined(stateModelProp) + ? stateModelProp + : {}; + + this.id = stateModel.id; + this.status = stateModel.status ?? IDLE; + this.navigationId = stateModel.navigationId ?? null; + this.routePath = stateModel.routePath; + this.routePathSegments = stateModel.routePathSegments ?? []; + this.search = stateModel.search ?? ''; + this.page = stateModel.page ?? RequestPageImpl.empty(); + this.order = stateModel.order ?? RequestOrderImpl.empty(); + this.filter = stateModel.filter ?? RequestFilterImpl.empty(); + this.requestParams = stateModel.requestParams ?? new Map(); + this.task = stateModel.task ?? null; + this.error = stateModel.error ?? null; + this.data = stateModel.data ?? new Map(); + this.uiState = stateModel.uiState ?? new Map(); + } + + /** + * ** Factory method. + */ + static of(stateModel: Partial): ComponentStateImpl { + return new ComponentStateImpl(stateModel); + } + + /** + * ** Convert provided {@link LiteralComponentState} into instance of {@link ComponentStateImpl}. + *

+ * Every literals could be transformed to their original Collection format. + *

    + *
  • + * Object literals could be transformed to Map/WeakMap/Set depends of the needs. + *
  • + *
  • + * Array is keep as it is. + *
  • + *
+ *

+ * + * @see CollectionsUtil.transformObjectToMap + * @see CollectionsUtil.transformMapToObject + */ + static fromLiteralComponentState(literalStateModel: LiteralComponentState): ComponentStateImpl { + return ComponentStateImpl.of({ + ...literalStateModel, + page: RequestPageImpl.fromLiteral(literalStateModel.page), + order: RequestOrderImpl.fromLiteral(literalStateModel.order), + filter: RequestFilterImpl.fromLiteral(literalStateModel.filter), + requestParams: CollectionsUtil.transformObjectToMap(literalStateModel.requestParams), + data: CollectionsUtil.transformObjectToMap(literalStateModel.data), + uiState: CollectionsUtil.transformObjectToMap(literalStateModel.uiState) + }); + } + + /** + * ** Make deep clone from Literal Component State. + */ + static cloneDeepLiteral(literalStateModel: LiteralComponentState): LiteralComponentState { + return { + id: literalStateModel.id, + status: literalStateModel.status, + data: CollectionsUtil.cloneDeep(literalStateModel.data), + routePath: literalStateModel.routePath, + routePathSegments: [...literalStateModel.routePathSegments], + search: literalStateModel.search, + page: CollectionsUtil.cloneDeep(literalStateModel.page), + order: CollectionsUtil.cloneDeep(literalStateModel.order), + filter: CollectionsUtil.cloneDeep(literalStateModel.filter), + requestParams: CollectionsUtil.cloneDeep(literalStateModel.requestParams), + task: literalStateModel.task, + navigationId: literalStateModel.navigationId, + error: literalStateModel.error, + uiState: CollectionsUtil.cloneDeep(literalStateModel.uiState) + }; + } + + /** + *

+ * Every Collection should be transformed to format of JSON supported literals, ready for LocalStorage/SessionStorage persist. + *

    + *
  • + * Map/WeakMap/Set have to be transform to Object literal. + *
  • + *
  • + * Array is keep as it is. + *
  • + *
+ *

+ * + * @see CollectionsUtil.transformObjectToMap + * @see CollectionsUtil.transformMapToObject + * + * @inheritDoc + */ + toLiteral(): LiteralComponentState { + return { + ...this, + page: this.page.toLiteral(), + order: this.order.toLiteral(), + filter: this.filter.toLiteral(), + requestParams: CollectionsUtil.transformMapToObject(this.requestParams), + data: CollectionsUtil.transformMapToObject(this.data), + uiState: CollectionsUtil.transformMapToObject(this.uiState) + }; + } + + /** + * @inheritDoc + */ + toLiteralDeepClone(): LiteralComponentState { + return ComponentStateImpl.cloneDeepLiteral( + this.toLiteral() + ); + } + + /** + * @inheritDoc + */ + copy(state: Partial = {}): ComponentStateImpl { + return ComponentStateImpl.of({ + ...this, + ...state + }); + } +} diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/model/state/component-status.model.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/model/state/component-status.model.ts new file mode 100644 index 0000000000..ccd24bb50b --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/model/state/component-status.model.ts @@ -0,0 +1,38 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +/* eslint-disable @typescript-eslint/no-inferrable-types */ + +/** + * ** Status constant for Initialized. + */ +export const INITIALIZED = 'Initialized'; + +/** + * ** Status constant for Idle. + */ +export const IDLE = 'Idle'; + +/** + * ** Status constant for Loading. + */ +export const LOADING = 'Loading'; + +/** + * ** Status constant for Loaded. + */ +export const LOADED = 'Loaded'; + +/** + * ** Status constant for Failed. + */ +export const FAILED = 'Failed'; + +/** + * ** Status types. + */ +export type StatusType = typeof INITIALIZED | typeof IDLE | typeof LOADING | typeof LOADED | typeof FAILED; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/model/state/components-state.model.spec.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/model/state/components-state.model.spec.ts new file mode 100644 index 0000000000..226e6822c2 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/model/state/components-state.model.spec.ts @@ -0,0 +1,311 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +/* eslint-disable @typescript-eslint/dot-notation,arrow-body-style,prefer-arrow/prefer-arrow-functions */ + +import { CollectionsUtil } from '../../../../utils'; + +import { FAILED, IDLE, LOADED, LOADING } from './component-status.model'; + +import { ComponentState, ComponentStateImpl, LiteralComponentState } from './component-state.model'; + +import { ComponentsStateHelper, LiteralComponentsState } from './components-state.model'; + +describe('ComponentsStateHelper', () => { + it('should verify instance is created', () => { + // When + const instance = new ComponentsStateHelper(); + + // Then + expect(instance).toBeDefined(); + }); + + describe('Methods::', () => { + let literalComponentsState: LiteralComponentsState; + let helper: ComponentsStateHelper; + let componentState1: ComponentState; + let componentState2: ComponentState; + let componentState3: ComponentState; + let componentState4: ComponentState; + + beforeEach(() => { + componentState1 = ComponentStateImpl.of({ id: 'component1', status: LOADED }); + componentState2 = ComponentStateImpl.of({ id: 'component2', status: IDLE }); + componentState3 = ComponentStateImpl.of({ id: 'component3', status: FAILED }); + componentState4 = ComponentStateImpl.of({ id: 'component4', status: IDLE }); + + literalComponentsState = createComponentsLiteralState( + componentState1, + componentState2, + componentState3, + componentState4 + ); + + helper = new ComponentsStateHelper(); + helper.setState(literalComponentsState); + }); + + describe('|getState|', () => { + it('should verify will return literalComponentState', () => { + // When + const state = helper.getState(); + + // Then + expect(state).toBeDefined(); + expect(state).toEqual(literalComponentsState); + expect(state.components).not.toBe(literalComponentsState.components); + expect(state.routePathSegments).not.toBe(literalComponentsState.routePathSegments); + }); + }); + + describe('|getLiteralComponentState|', () => { + it('should verify will return LiteralComponentState', () => { + // When + const state = helper.getLiteralComponentState('component3', ['test_domain/context', 'test_entity/10']); + + // Then + expect(state).toBeDefined(); + expect(state).toEqual(literalComponentsState + .routePathSegments['test_domain/context'] + .routePathSegments['test_entity/10'] + .components['component3']); + }); + + it('should verify will return null if there is no such state', () => { + // When + const state = helper.getLiteralComponentState('component10', ['test_domain/context', 'test_entity/10']); + + // Then + expect(state).toEqual(null); + }); + + it('should verify will return null if there is no such routePathSegments', () => { + // When + const state = helper.getLiteralComponentState('component3', ['test_domain/context', 'entity/15']); + + // Then + expect(state).toEqual(null); + }); + }); + + describe('|getComponentState|', () => { + it('should verify will return ComponentState', () => { + // When + const state = helper.getComponentState('component2', ['test_domain/context', 'test_entity/18']); + + // Then + expect(state).toBeDefined(); + expect(state).toEqual(componentState2.copy({ + routePath: 'domain/context/entity/18', + routePathSegments: ['test_domain/context', 'test_entity/18'] + })); + expect(state.routePathSegments).not.toBe(componentState2.routePathSegments); + expect(state.data).not.toBe(componentState2.data); + expect(state.uiState).not.toBe(componentState2.uiState); + }); + + it('should verify will return null if there is no such state', () => { + // When + const state = helper.getComponentState('component4', ['test_domain/context']); + + // Then + expect(state).toEqual(null); + }); + + it('should verify will return null if there is no such routePathSegments', () => { + // When + const state = helper.getLiteralComponentState('component2', ['domain/explore', 'test_entity/10']); + + // Then + expect(state).toEqual(null); + }); + }); + + describe('|getAllComponentState|', () => { + it('should verify will return Array of all ComponentState in given routePathSegments', () => { + // When + const states = helper.getAllComponentState(['test_domain/context', 'test_entity/10']); + + // Then + expect(states.length).toEqual(8); + expect(states).toEqual([ + componentState1, + componentState3, + componentState2.copy({ + routePath: 'test_domain/context', + routePathSegments: ['test_domain/context'] + }), + componentState3.copy({ + routePath: 'test_domain/context', + routePathSegments: ['test_domain/context'] + }), + componentState2.copy({ + routePath: 'domain/context/entity/10', + routePathSegments: ['test_domain/context', 'test_entity/10'] + }), + componentState4.copy({ + routePath: 'domain/context/entity/10', + routePathSegments: ['test_domain/context', 'test_entity/10'] + }), + componentState1.copy({ + routePath: 'domain/context/entity/10', + routePathSegments: ['test_domain/context', 'test_entity/10'] + }), + componentState3.copy({ + routePath: 'domain/context/entity/10', + routePathSegments: ['test_domain/context', 'test_entity/10'] + }) + ]); + }); + }); + + describe('|updateLiteralComponentState|', () => { + it('should verify will update state in right routePathSegments', () => { + // Given + const literalState: LiteralComponentState = { + ...componentState1.toLiteral(), + status: LOADING, + routePath: 'domain/context/entity/10', + routePathSegments: ['test_domain/context', 'test_entity/10'], + data: { payload: { content: { data: [1, 2, 3] } } }, + uiState: { element1: { click: true } } + }; + + // Then 1 + expect(helper + .getState() + .routePathSegments['test_domain/context'] + .routePathSegments['test_entity/10'] + .components['component1']).not.toEqual(literalState); + + // When + helper.updateLiteralComponentState(literalState); + + // Then 2 + expect(helper + .getState() + .routePathSegments['test_domain/context'] + .routePathSegments['test_entity/10'] + .components['component1']).toEqual(literalState); + }); + }); + + describe('|resetComponentStates|', () => { + it('should verify will reset all ComponentStata status in given routePathSegments', () => { + // When + helper.resetComponentStates(['test_domain/context', 'test_entity/10']); + const state = helper.getState(); + + // Then + CollectionsUtil.iterateObject(state.components, (value) => { + expect(value.status).toEqual(IDLE); + }); + + CollectionsUtil.iterateObject(state.routePathSegments['test_domain/context'].components, (value) => { + expect(value.status).toEqual(IDLE); + }); + + CollectionsUtil.iterateObject( + state.routePathSegments['test_domain/context'].routePathSegments['test_entity/10'].components, + (value) => { + expect(value.status).toEqual(IDLE); + } + ); + }); + }); + + describe('|deleteRoutePathSegments|', () => { + it('should verify will delete routePathSegment', () => { + // Then 1 + expect(helper.getState().routePathSegments['test_domain/context'].routePathSegments['test_entity/18']).toBeDefined(); + + // When + helper.deleteRoutePathSegments(['test_domain/context', 'test_entity/18']); + + // Then 2 + expect(helper.getState().routePathSegments['test_domain/context'].routePathSegments['test_entity/18']).toBeUndefined(); + }); + }); + }); +}); + +const createComponentsLiteralState = (c1: ComponentState, c2: ComponentState, c3: ComponentState, c4: ComponentState) => { + return { + components: { + component1: { ...c1.toLiteral() }, + component3: { ...c3.toLiteral() } + }, + routePathSegments: { + 'test_domain/context': { + components: { + component2: { + ...c2.toLiteral(), + routePath: 'test_domain/context', + routePathSegments: ['test_domain/context'] + }, + component3: { + ...c3.toLiteral(), + routePath: 'test_domain/context', + routePathSegments: ['test_domain/context'] + } + }, + routePathSegments: { + 'test_entity/10': { + components: { + component2: { + ...c2.toLiteral(), + routePath: 'domain/context/entity/10', + routePathSegments: ['test_domain/context', 'test_entity/10'] + }, + component4: { + ...c4.toLiteral(), + routePath: 'domain/context/entity/10', + routePathSegments: ['test_domain/context', 'test_entity/10'] + }, + component1: { + ...c1.toLiteral(), + routePath: 'domain/context/entity/10', + routePathSegments: ['test_domain/context', 'test_entity/10'] + }, + component3: { + ...c3.toLiteral(), + routePath: 'domain/context/entity/10', + routePathSegments: ['test_domain/context', 'test_entity/10'] + } + }, + routePathSegments: {} + }, + 'test_entity/18': { + components: { + component3: { + ...c3.toLiteral(), + routePath: 'domain/context/entity/18', + routePathSegments: ['test_domain/context', 'test_entity/18'] + }, + component2: { + ...c2.toLiteral(), + routePath: 'domain/context/entity/18', + routePathSegments: ['test_domain/context', 'test_entity/18'] + }, + component1: { + ...c1.toLiteral(), + routePath: 'domain/context/entity/18', + routePathSegments: ['test_domain/context', 'test_entity/18'] + }, + component4: { + ...c4.toLiteral(), + routePath: 'domain/context/entity/18', + routePathSegments: ['test_domain/context', 'test_entity/18'] + } + }, + routePathSegments: {} + } + } + } + } + } as LiteralComponentsState; +}; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/model/state/components-state.model.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/model/state/components-state.model.ts new file mode 100644 index 0000000000..2620df6f48 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/model/state/components-state.model.ts @@ -0,0 +1,283 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { CollectionsUtil } from '../../../../utils'; + +import { IDLE } from './component-status.model'; +import { ComponentState, ComponentStateImpl, LiteralComponentState } from './component-state.model'; + +export interface LiteralComponentsState { + readonly components: { [name: string]: LiteralComponentState }; + readonly routePathSegments: { [segmentId: string]: LiteralComponentsState }; +} + +/** + * ** ComponentsState Helper. + * + * + */ +export class ComponentsStateHelper { + private _literalComponentsState: LiteralComponentsState; + + constructor() { + this._literalComponentsState = { + components: {}, + routePathSegments: {} + }; + } + + /** + * ** Returns LiteralComponentsState from Helper. + */ + getState(): LiteralComponentsState { + return { + ...this._literalComponentsState + }; + } + + /** + * ** Will set state to the local Helper state. + */ + setState(literalComponentsState: LiteralComponentsState) { + this._literalComponentsState = this._shallowCloneComponentsState(literalComponentsState); + + return this; + } + + /** + * ** Will return LiteralComponentState for given id and routePathSegments. + */ + getLiteralComponentState(id: string, routePathSegments?: string[]): LiteralComponentState { + return this._getLiteralComponentState( + id, + CollectionsUtil.isArray(routePathSegments) + ? [...routePathSegments] + : [], + this._literalComponentsState + ); + } + + /** + * ** Get ComponentState for given id and routePathSegments. + */ + getComponentState(id: string, routePathSegments?: string[]): ComponentState { + const literalComponentState = this._getLiteralComponentState( + id, + CollectionsUtil.isArray(routePathSegments) + ? [...routePathSegments] + : [], + this._literalComponentsState + ); + + return CollectionsUtil.isDefined(literalComponentState) + ? ComponentStateImpl.fromLiteralComponentState(literalComponentState) + : null; + } + + /** + * ** Get all ComponentState for given routePathSegments. + */ + getAllComponentState(routePathSegments: string[]): ComponentState[] { + return this._getAllComponentState( + CollectionsUtil.isArray(routePathSegments) + ? [...routePathSegments] + : [], + this._literalComponentsState + ); + } + + /** + * ** Update LiteralComponentState. + */ + updateLiteralComponentState(literalComponentState: LiteralComponentState): void { + return this._updateLiteralComponentState( + literalComponentState, + [...literalComponentState.routePathSegments], + this._literalComponentsState + ); + } + + /** + * ** Reset component status to NOT_LOADED for all ComponentState in a given routePathSegment. + */ + resetComponentStates(routePathSegments: string[]): void { + this._resetComponentStates( + CollectionsUtil.isArray(routePathSegments) + ? [...routePathSegments] + : [], + this._literalComponentsState + ); + } + + /** + * ** Delete all ComponentState for given routePathSegment. + */ + deleteRoutePathSegments(routePathSegments: string[]): void { + this._deleteRoutePathSegments( + CollectionsUtil.isArray(routePathSegments) + ? [...routePathSegments] + : [], + this._literalComponentsState + ); + } + + /** + * ** Update ComponentState. + */ + private _updateLiteralComponentState( + literalComponentState: LiteralComponentState, + routePathSegments: string[], + state: LiteralComponentsState): void { + + if (CollectionsUtil.isArrayEmpty(routePathSegments)) { + state.components[literalComponentState.id] = literalComponentState; + + return; + } + + const routePathSegment = routePathSegments.shift(); + + this._updateLiteralComponentState( + literalComponentState, + routePathSegments, + this._normalizeRoutePathSegments(state.routePathSegments, routePathSegment) + ); + } + + /** + * ** Get ComponentState. + */ + private _getLiteralComponentState( + id: string, + routePathSegments: string[], + state: LiteralComponentsState): LiteralComponentState | null { + + if (!state) { + return null; + } + + if (CollectionsUtil.isArrayEmpty(routePathSegments)) { + if (state.components[id]) { + return ComponentStateImpl.cloneDeepLiteral(state.components[id]); + } + + return null; + } + + const routePathSegment = routePathSegments.shift(); + + return this._getLiteralComponentState( + id, + routePathSegments, + state.routePathSegments[routePathSegment] + ); + } + + /** + * ** Get all components for given routePathSegments. + */ + private _getAllComponentState(routePathSegments: string[], state: LiteralComponentsState): ComponentState[] { + if (!state) { + return []; + } + + const components: ComponentState[] = CollectionsUtil.objectValues(state.components) + .map((c) => ComponentStateImpl.fromLiteralComponentState( + ComponentStateImpl.cloneDeepLiteral(c) + )); + + if (CollectionsUtil.isArrayEmpty(routePathSegments)) { + return components; + } + + const routePathSegment = routePathSegments.shift(); + + return [ + ...components, + ...this._getAllComponentState(routePathSegments, state.routePathSegments[routePathSegment]) + ]; + } + + /** + * ** Reset component status to NOT_LOADED for all component in a given context. + */ + private _resetComponentStates(routePathSegments: string[], state: LiteralComponentsState): void { + CollectionsUtil.iterateObject(state.components, (componentState, id) => { + state.components[id] = { ...componentState, status: IDLE }; + }); + + if (CollectionsUtil.isArrayEmpty(routePathSegments)) { + return; + } + + const routePathSegment = routePathSegments.shift(); + + this._resetComponentStates( + routePathSegments, + this._normalizeRoutePathSegments(state.routePathSegments, routePathSegment) + ); + } + + /** + * ** Delete all components state for a given route path segment. + */ + private _deleteRoutePathSegments(routePathSegments: string[], state: LiteralComponentsState): void { + const routePathSegment = routePathSegments.shift(); + + if (!routePathSegment) { + return; + } + + if (CollectionsUtil.isArrayEmpty(routePathSegments)) { + delete state.routePathSegments[routePathSegment]; + + return; + } + + this._deleteRoutePathSegments( + routePathSegments, + this._normalizeRoutePathSegments(state.routePathSegments, routePathSegment) + ); + } + + /** + * ** Normalize Route path segments. + */ + private _normalizeRoutePathSegments( + urlSegments: { [segmentId: string]: LiteralComponentsState }, + urlSegmentName: string): LiteralComponentsState { + + if (CollectionsUtil.isNil(urlSegments[urlSegmentName])) { + urlSegments[urlSegmentName] = { + components: {}, + routePathSegments: {} + }; + } + + return urlSegments[urlSegmentName]; + } + + private _shallowCloneComponentsState(source: LiteralComponentsState, target?: LiteralComponentsState): LiteralComponentsState { + const _source: LiteralComponentsState = source ?? { components: {}, routePathSegments: {} }; + const _target: LiteralComponentsState = target ?? { components: {}, routePathSegments: {} }; + + CollectionsUtil.iterateObject(_source.components, (value, key) => { + _target.components[key] = value; + }); + + CollectionsUtil.iterateObject(_source.routePathSegments, (value, key) => { + _target.routePathSegments[key] = { + components: {}, + routePathSegments: {} + }; + + this._shallowCloneComponentsState(value, _target.routePathSegments[key]); + }); + + return _target; + } +} diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/model/state/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/model/state/index.ts new file mode 100644 index 0000000000..c6369edf04 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/model/state/index.ts @@ -0,0 +1,10 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './component-status.model'; +export * from './component-state.model'; +export * from './components-state.model'; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/public-api.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/public-api.ts new file mode 100644 index 0000000000..ba142389bd --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/public-api.ts @@ -0,0 +1,8 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './index'; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/services/component.service.spec.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/services/component.service.spec.ts new file mode 100644 index 0000000000..ce79f1e9e3 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/services/component.service.spec.ts @@ -0,0 +1,734 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +/* eslint-disable @typescript-eslint/dot-notation,arrow-body-style,prefer-arrow/prefer-arrow-functions */ + +import { TestBed } from '@angular/core/testing'; + +import { Store } from '@ngrx/store'; +import { RouterReducerState } from '@ngrx/router-store'; + +import { of, throwError } from 'rxjs'; + +import { marbles } from 'rxjs-marbles/jasmine'; + +import { CollectionsUtil } from '../../../utils'; + +import { GenericAction, STORE_COMPONENTS, STORE_ROUTER, StoreState } from '../../ngrx'; +import { RouterService, RouterState, RouteSegments, RouteState } from '../../router'; + +import { + ComponentModel, + ComponentState, + ComponentStateImpl, + FAILED, + IDLE, + INITIALIZED, + LiteralComponentsState, + LiteralComponentState, + LOADED, + LOADING, + StatusType +} from '../model'; +import { ComponentIdle, ComponentInit, ComponentLoading, ComponentUpdate } from '../state'; + +import { ComponentService, ComponentServiceImpl } from './component.service'; + +describe('ComponentService -> ComponentServiceImpl', () => { + let storeStub$: jasmine.SpyObj>; + let routerServiceStub: jasmine.SpyObj; + + let service: ComponentService; + + beforeEach(() => { + storeStub$ = jasmine.createSpyObj>('store', ['select', 'dispatch']); + routerServiceStub = jasmine.createSpyObj('routerService', ['get', 'getState']); + + TestBed.configureTestingModule({ + providers: [ + { provide: Store, useValue: storeStub$ }, + { provide: RouterService, useValue: routerServiceStub }, + { provide: ComponentService, useClass: ComponentServiceImpl } + ] + }); + + service = TestBed.inject(ComponentService); + }); + + it('should verify service is created', () => { + // Then + expect(service).toBeDefined(); + }); + + describe('Methods::', () => { + let id: string; + let components: LiteralComponentsState; + let router: RouterState; + + beforeEach(() => { + id = 'testComponent10'; + router = createStoreState()[STORE_ROUTER]; + + routerServiceStub.getState.and.returnValue(of(router.state)); + routerServiceStub.get.and.returnValue(of(router)); + }); + + describe('|init|', () => { + it('should verify will initialize correct component and dispatch ComponentInit', () => { + // Given + const componentState = createComponentState(router, id, INITIALIZED); + + storeStub$.select.and.callFake((param) => { + if (param === STORE_COMPONENTS) { + components = createStoreState(componentState.toLiteral())[STORE_COMPONENTS]; + + return of(components); + } + + return of(createStoreState()); + }); + + // When + const response$ = service.init(id, router.state); + + // Then + expect(storeStub$.select.calls.argsFor(0)).toEqual([jasmine.any(Function)]); + expect(storeStub$.select.calls.argsFor(1)).toEqual([STORE_COMPONENTS]); + expect(storeStub$.dispatch).toHaveBeenCalledWith(ComponentInit.of(componentState)); + + response$.subscribe((model) => { + expect(model['_routerState']).toEqual(router); + expect(model['_componentState']).toEqual(getComponentState(components, id)); + expect(model.status).toEqual(INITIALIZED); + }); + + expect(routerServiceStub.getState).toHaveBeenCalled(); + expect(storeStub$.select.calls.argsFor(2)).toEqual([STORE_COMPONENTS]); + expect(routerServiceStub.get).toHaveBeenCalled(); + }, 1000); + + it('should verify wont dispatch ComponentInit', () => { + // Given + const componentState = createComponentState(router, id, LOADED); + + storeStub$.select.and.callFake((param) => { + if (param === STORE_COMPONENTS) { + components = createStoreState(componentState.toLiteral())[STORE_COMPONENTS]; + + return of(components); + } + + return of(createStoreState(componentState.toLiteral())); + }); + + // When + const response$ = service.init(id, router.state); + + // Then + expect(storeStub$.select.calls.argsFor(0)).toEqual([jasmine.any(Function)]); + expect(storeStub$.dispatch).not.toHaveBeenCalled(); + expect(storeStub$.select.calls.argsFor(1)).toEqual([STORE_COMPONENTS]); + + response$.subscribe((model) => { + expect(model['_routerState']).toEqual(router); + expect(model['_componentState']).toEqual(getComponentState(components, id)); + expect(model.status).toEqual(LOADED); + }); + + expect(routerServiceStub.getState).toHaveBeenCalled(); + expect(storeStub$.select.calls.argsFor(2)).toEqual([STORE_COMPONENTS]); + expect(routerServiceStub.get).toHaveBeenCalled(); + }, 1000); + }); + + describe('|idle|', () => { + it('should verify will dispatch ComponentIdle with provided ComponentState', () => { + // Given + const componentState = createComponentState(router, id, LOADED); + + // When + service.idle(componentState); + + // Then + expect(storeStub$.dispatch).toHaveBeenCalledWith(ComponentIdle.of(componentState)); + }); + }); + + describe('|load|', () => { + it('should verify will dispatch ComponentLoading with provided ComponentState', () => { + // Given + const componentState = createComponentState(router, id, FAILED); + routerServiceStub.get.and.returnValue(of(router)); + storeStub$.select + .and + .returnValue( + of(createStoreState(createComponentState(router, id, LOADED).toLiteral())[STORE_COMPONENTS]) + ); + + // When + const response$ = service.load(componentState); + + // Then + expect(routerServiceStub.get).toHaveBeenCalledTimes(1); + // @ts-ignore + expect(storeStub$.dispatch.calls.argsFor(0)).toEqual([ + ComponentLoading.of( + componentState.copy({ + navigationId: router.navigationId, + status: LOADING + }) + ) + ]); + + const assertionComponentState = createComponentState(router, id, LOADED); + response$.subscribe((model) => { + expect(model['_routerState']).toEqual(router); + expect(model['_componentState']).toEqual(assertionComponentState); + expect(model.status).toEqual(LOADED); + }); + + // @ts-ignore + expect(storeStub$.select).toHaveBeenCalledWith(STORE_COMPONENTS); + expect(routerServiceStub.getState).toHaveBeenCalled(); + expect(routerServiceStub.get).toHaveBeenCalledTimes(2); + }, 1000); + + it('should verify will dispatch ComponentIdle and ComponentLoading with provided component id', () => { + // Given + const componentState = createComponentState(router, id, INITIALIZED); + routerServiceStub.get.and.returnValue(of(router)); + storeStub$.select + .and + .returnValue( + of(createStoreState(createComponentState(router, id, LOADED).toLiteral())[STORE_COMPONENTS]) + ); + + // When + const response$ = service.load(componentState); + + // Then + expect(routerServiceStub.get).toHaveBeenCalledTimes(1); + // @ts-ignore + expect(storeStub$.dispatch.calls.argsFor(0)).toEqual([ + ComponentIdle.of( + createComponentState(router, id, IDLE).copy({ + navigationId: router.navigationId + }) + ) + ]); + expect(storeStub$.dispatch.calls.argsFor(1)).toEqual([ + ComponentLoading.of( + createComponentState(router, id, LOADING).copy({ + navigationId: router.navigationId + }) + ) + ]); + + const assertionComponentState = createComponentState(router, id, LOADED); + response$.subscribe((model) => { + expect(model['_routerState']).toEqual(router); + expect(model['_componentState']).toEqual(assertionComponentState); + expect(model.status).toEqual(LOADED); + }); + + // @ts-ignore + expect(storeStub$.select).toHaveBeenCalledWith(STORE_COMPONENTS); + expect(routerServiceStub.getState).toHaveBeenCalled(); + expect(routerServiceStub.get).toHaveBeenCalledTimes(2); + }, 1000); + }); + + describe('|update|', () => { + it('should verify will dispatch ComponentUpdate with provided ComponentState', () => { + // Given + const componentState = createComponentState(router, id, LOADED); + + // When + service.update(componentState); + + // Then + expect(routerServiceStub.get).toHaveBeenCalled(); + expect(storeStub$.dispatch).toHaveBeenCalledWith( + ComponentUpdate.of( + componentState.copy({ navigationId: router.navigationId }) + ) + ); + }); + }); + + describe('|hasInSegment|', () => { + it('should verify will return true when ComponentState exist', marbles((m) => { + // Given + const componentsStream$ = m.cold('---a', { + a: createStoreState( + createComponentState(router, 'someComponentId', LOADED).toLiteral())[STORE_COMPONENTS] + }); + const routeStateStream$ = m.cold('--a', { + a: router.state + }); + storeStub$.select.and.returnValue(componentsStream$); + routerServiceStub.getState.and.returnValue(routeStateStream$); + const expected$ = m.cold('---(a|)', { a: true }); + + // When + const response$ = service.hasInSegment('someComponentId', ['test_domain/context', 'test_entity/10']); + + // Then + m.expect(response$).toBeObservable(expected$); + })); + + it('should verify will return false when ComponentState not exist', marbles((m) => { + // Given + const componentsStream$ = m.cold('---a', { + a: createStoreState( + createComponentState(router, 'someComponentId', LOADED).toLiteral())[STORE_COMPONENTS] + }); + const routeStateStream$ = m.cold('---a', { + a: router.state + }); + storeStub$.select.and.returnValue(componentsStream$); + routerServiceStub.getState.and.returnValue(routeStateStream$); + const expected$ = m.cold('---(a|)', { a: false }); + + // When + const response$ = service.hasInSegment('newComponentId', ['test_domain/context', 'test_entity/10']); + + // Then + m.expect(response$).toBeObservable(expected$); + })); + }); + + describe('|onInit|', () => { + it(`should verify wont emit when state doesn't exist`, marbles((m) => { + // Given + const cA = createStoreState(createComponentState(router, 'testId10', INITIALIZED).toLiteral()); + const cB = createStoreState(createComponentState(router, 'testId10', IDLE).toLiteral()); + const cC = createStoreState(createComponentState(router, 'testId10', LOADING).toLiteral()); + const cD = createStoreState(createComponentState(router, 'testId10', LOADED).toLiteral()); + const cE = createStoreState(createComponentState(router, 'testId22', LOADED).toLiteral()); + const cF = createStoreState(createComponentState(router, 'testId33', LOADED).toLiteral()); + const componentsMarbleStream$ = m.cold('-a--bc-d-e--f-|', { + a: cA[STORE_COMPONENTS], + b: cB[STORE_COMPONENTS], + c: cC[STORE_COMPONENTS], + d: cD[STORE_COMPONENTS], + e: cE[STORE_COMPONENTS], + f: cF[STORE_COMPONENTS] + }); + const expectedModelMarbleStream$ = m.cold('--------------|'); + storeStub$.select.and.returnValue(componentsMarbleStream$); + + // When + const response$ = service.onInit(id, router.state.routePathSegments); + + // Then + m.expect(response$).toBeObservable(expectedModelMarbleStream$); + })); + + it('should verify will emit when state exist', () => { + // Given + const componentState = createComponentState(router, id, INITIALIZED); + const storeState = createStoreState(componentState.toLiteral()); + + storeStub$.select.and.returnValue(of(storeState[STORE_COMPONENTS])); + + // When + const response$ = service.onInit(id, router.state.routePathSegments); + + // Then + // @ts-ignore + expect(storeStub$.select.calls.argsFor(0)).toEqual([STORE_COMPONENTS]); + expect(routerServiceStub.getState).toHaveBeenCalled(); + + response$.subscribe((model) => { + expect(model).toBeDefined(); + expect(model.status).toEqual(INITIALIZED); + }); + + expect(storeStub$.select.calls.argsFor(1)).toEqual([STORE_COMPONENTS]); + expect(routerServiceStub.get).toHaveBeenCalled(); + }, 1000); + }); + + describe('|onLoaded|', () => { + it(`should verify wont emit when state doesn't exist`, marbles((m) => { + // Given + const cA = createStoreState(createComponentState(router, 'testId1', INITIALIZED).toLiteral()); + const cB = createStoreState(createComponentState(router, 'testId1', IDLE).toLiteral()); + const cC = createStoreState(createComponentState(router, 'testId1', LOADING).toLiteral()); + const cD = createStoreState(createComponentState(router, 'testId1', LOADED).toLiteral()); + const cE = createStoreState(createComponentState(router, 'testId2', LOADED).toLiteral()); + const cF = createStoreState(createComponentState(router, 'testId3', LOADED).toLiteral()); + const componentsMarbleStream$ = m.cold('-a--bc---d--e----f-|', { + a: cA[STORE_COMPONENTS], + b: cB[STORE_COMPONENTS], + c: cC[STORE_COMPONENTS], + d: cD[STORE_COMPONENTS], + e: cE[STORE_COMPONENTS], + f: cF[STORE_COMPONENTS] + }); + const expectedModelMarbleStream$ = m.cold('-------------------|'); + storeStub$.select.and.returnValue(componentsMarbleStream$); + + // When + const response$ = service.onLoaded(id, router.state.routePathSegments); + + // Then + m.expect(response$).toBeObservable(expectedModelMarbleStream$); + })); + + it('should verify wont emit when state status is not LOADED or FAILED', marbles((m) => { + // Given + const cA = createStoreState(createComponentState(router, id, INITIALIZED).toLiteral()); + const cB = createStoreState(createComponentState(router, id, IDLE).toLiteral()); + const cC = createStoreState(createComponentState(router, id, LOADING).toLiteral()); + const componentsMarbleStream$ = m.cold('-a--bc---|', { + a: cA[STORE_COMPONENTS], + b: cB[STORE_COMPONENTS], + c: cC[STORE_COMPONENTS] + }); + const expectedModelMarbleStream$ = m.cold('---------|'); + storeStub$.select.and.returnValue(componentsMarbleStream$); + + // When + const response$ = service.onLoaded(id, router.state.routePathSegments); + + // Then + m.expect(response$).toBeObservable(expectedModelMarbleStream$); + })); + + it('should verify will emit when state exist and its status is LOADED', marbles((m) => { + // Given + const cA = createStoreState(createComponentState(router, id, INITIALIZED).toLiteral()); + const cB = createStoreState(createComponentState(router, id, IDLE).toLiteral()); + const cC = createStoreState(createComponentState(router, id, LOADING).toLiteral()); + const cD = createStoreState(createComponentState(router, id, LOADED).toLiteral()); + const componentsMarbleStream$ = m.cold('-a--bc---d---', { + a: cA[STORE_COMPONENTS], + b: cB[STORE_COMPONENTS], + c: cC[STORE_COMPONENTS], + d: cD[STORE_COMPONENTS] + }); + const componentsMarbleStream1$ = m.cold('d----', { + d: cD[STORE_COMPONENTS] + }); + const expectedModelMarbleStream$ = m.cold('---------(a|)', { + a: ComponentModel.of(getComponentState(cD, id), cD[STORE_ROUTER]) + }); + let cnt = 0; + storeStub$.select.and.callFake(() => { + cnt++; + if (cnt === 1) { + return componentsMarbleStream$; + } + + if (cnt === 2) { + return componentsMarbleStream1$; + } + + return throwError(() => new Error(`This shouldn't be reachable`)); + }); + routerServiceStub.getState.and.returnValue(m.cold('a----', { a: router.state })); + routerServiceStub.get.and.returnValue(m.cold('a----', { a: router })); + + // When + const response$ = service.onLoaded(id, router.state.routePathSegments); + + // Then + m.expect(response$).toBeObservable(expectedModelMarbleStream$); + })); + + it('should verify will emit when state exist and its status is FAILED', marbles((m) => { + // Given + const cA = createStoreState(createComponentState(router, id, INITIALIZED).toLiteral()); + const cB = createStoreState(createComponentState(router, id, IDLE).toLiteral()); + const cC = createStoreState(createComponentState(router, id, LOADING).toLiteral()); + const cD = createStoreState(createComponentState(router, id, FAILED).toLiteral()); + const componentsMarbleStream$ = m.cold('-a--bc---d---', { + a: cA[STORE_COMPONENTS], + b: cB[STORE_COMPONENTS], + c: cC[STORE_COMPONENTS], + d: cD[STORE_COMPONENTS] + }); + const componentsMarbleStream1$ = m.cold('d----', { + d: cD[STORE_COMPONENTS] + }); + const expectedModelMarbleStream$ = m.cold('---------(a|)', { + a: ComponentModel.of(getComponentState(cD, id), cD[STORE_ROUTER]) + }); + let cnt = 0; + storeStub$.select.and.callFake(() => { + cnt++; + if (cnt === 1) { + return componentsMarbleStream$; + } + + if (cnt === 2) { + return componentsMarbleStream1$; + } + + return throwError(() => new Error(`This shouldn't be reachable`)); + }); + routerServiceStub.getState.and.returnValue(m.cold('a----', { a: router.state })); + routerServiceStub.get.and.returnValue(m.cold('a----', { a: router })); + + // When + const response$ = service.onLoaded(id, router.state.routePathSegments); + + // Then + m.expect(response$).toBeObservable(expectedModelMarbleStream$); + })); + }); + + describe('|getModel|', () => { + it(`should verify wont emit when state doesn't exist`, marbles((m) => { + // Given + const cA = createStoreState(createComponentState(router, 'testId21', INITIALIZED).toLiteral()); + const cB = createStoreState(createComponentState(router, 'testId21', IDLE).toLiteral()); + const cC = createStoreState(createComponentState(router, 'testId21', LOADING).toLiteral()); + const cD = createStoreState(createComponentState(router, 'testId21', LOADED).toLiteral()); + const cE = createStoreState(createComponentState(router, 'testId32', LOADED).toLiteral()); + const cF = createStoreState(createComponentState(router, 'testId33', LOADED).toLiteral()); + const componentsMarbleStream$ = m.cold('--a-bc---d----e--f---|', { + a: cA[STORE_COMPONENTS], + b: cB[STORE_COMPONENTS], + c: cC[STORE_COMPONENTS], + d: cD[STORE_COMPONENTS], + e: cE[STORE_COMPONENTS], + f: cF[STORE_COMPONENTS] + }); + const expectedModelMarbleStream$ = m.cold('---------------------|'); + storeStub$.select.and.returnValue(componentsMarbleStream$); + + // When + const response$ = service.getModel(id, router.state.routePathSegments); + + // Then + m.expect(response$).toBeObservable(expectedModelMarbleStream$); + })); + + it('should verify will emit on every status', marbles((m) => { + // Given + const cA = createStoreState(createComponentState(router, id, INITIALIZED).toLiteral()); + const cB = createStoreState(createComponentState(router, id, IDLE).toLiteral()); + const cC = createStoreState(createComponentState(router, id, LOADING).toLiteral()); + const cD = createStoreState(createComponentState(router, id, LOADED).toLiteral()); + const cE = createStoreState(createComponentState(router, id, FAILED).toLiteral()); + const componentsMarbleStream$ = m.cold('-a--bc---d---e-', { + a: cA[STORE_COMPONENTS], + b: cB[STORE_COMPONENTS], + c: cC[STORE_COMPONENTS], + d: cD[STORE_COMPONENTS], + e: cE[STORE_COMPONENTS] + }); + const expectedModelMarbleStream$ = m.cold('-a--bc---d---e-', { + a: ComponentModel.of(getComponentState(cA, id), cA[STORE_ROUTER]), + b: ComponentModel.of(getComponentState(cB, id), cB[STORE_ROUTER]), + c: ComponentModel.of(getComponentState(cC, id), cC[STORE_ROUTER]), + d: ComponentModel.of(getComponentState(cD, id), cD[STORE_ROUTER]), + e: ComponentModel.of(getComponentState(cE, id), cE[STORE_ROUTER]) + }); + storeStub$.select.and.returnValue(componentsMarbleStream$); + routerServiceStub.get.and.returnValue(m.cold('a', { a: router })); + + // When + const response$ = service.getModel(id, router.state.routePathSegments, ['*']); + + // Then + m.expect(response$).toBeObservable(expectedModelMarbleStream$); + })); + + it('should verify will emit on requested statuses', marbles((m) => { + // Given + const cA = createStoreState(createComponentState(router, id, INITIALIZED).toLiteral()); + const cB = createStoreState(createComponentState(router, id, IDLE).toLiteral()); + const cC = createStoreState(createComponentState(router, id, LOADING).toLiteral()); + const cD = createStoreState(createComponentState(router, id, LOADED).toLiteral()); + const cE = createStoreState(createComponentState(router, id, FAILED).toLiteral()); + const componentsMarbleStream$ = m.cold('-a--bc---d---e-', { + a: cA[STORE_COMPONENTS], + b: cB[STORE_COMPONENTS], + c: cC[STORE_COMPONENTS], + d: cD[STORE_COMPONENTS], + e: cE[STORE_COMPONENTS] + }); + const expectedModelMarbleStream$ = m.cold('-----c-------e-', { + c: ComponentModel.of(getComponentState(cC, id), cC[STORE_ROUTER]), + e: ComponentModel.of(getComponentState(cE, id), cE[STORE_ROUTER]) + }); + storeStub$.select.and.returnValue(componentsMarbleStream$); + routerServiceStub.get.and.returnValue(m.cold('a', { a: router })); + + // When + const response$ = service.getModel(id, router.state.routePathSegments, [LOADING, FAILED]); + + // Then + m.expect(response$).toBeObservable(expectedModelMarbleStream$); + })); + + it('should verify will emit on default statuses', marbles((m) => { + // Given + const cA = createStoreState(createComponentState(router, id, INITIALIZED).toLiteral()); + const cB = createStoreState(createComponentState(router, id, IDLE).toLiteral()); + const cC = createStoreState(createComponentState(router, id, LOADING).toLiteral()); + const cD = createStoreState(createComponentState(router, id, LOADED).toLiteral()); + const cE = createStoreState(createComponentState(router, id, LOADING).toLiteral()); + const cF = createStoreState(createComponentState(router, id, FAILED).toLiteral()); + const componentsMarbleStream$ = m.cold('-a--bc---d---e---f-', { + a: cA[STORE_COMPONENTS], + b: cB[STORE_COMPONENTS], + c: cC[STORE_COMPONENTS], + d: cD[STORE_COMPONENTS], + e: cE[STORE_COMPONENTS], + f: cF[STORE_COMPONENTS] + }); + const expectedModelMarbleStream$ = m.cold('---------d-------f-', { + d: ComponentModel.of(getComponentState(cD, id), cD[STORE_ROUTER]), + f: ComponentModel.of(getComponentState(cF, id), cF[STORE_ROUTER]) + }); + storeStub$.select.and.returnValue(componentsMarbleStream$); + routerServiceStub.get.and.returnValue(m.cold('a', { a: router })); + + // When + const response$ = service.getModel(id, router.state.routePathSegments); + + // Then + m.expect(response$).toBeObservable(expectedModelMarbleStream$); + })); + }); + + describe('|dispatchAction|', () => { + it('should verify will invoke correct methods', () => { + // Given + const componentState = createComponentState(router, id, LOADING); + const typeStub = '[component] Some Action'; + const modelStub = { + getComponentState: () => componentState + } as ComponentModel; + const getModelSpy = spyOn(service, 'getModel').and.returnValue(of(modelStub)); + const actionStub = { type: typeStub, payload: componentState, task: undefined }; + const genericActionOfSpy = spyOn(GenericAction, 'of').and.returnValue(actionStub); + + // When + service.dispatchAction(typeStub, componentState); + + // Then + expect(getModelSpy).toHaveBeenCalledWith(componentState.id, componentState.routePathSegments, ['*']); + expect(genericActionOfSpy).toHaveBeenCalledWith(typeStub, componentState, undefined); + expect(storeStub$.dispatch).toHaveBeenCalledWith(actionStub); + }); + + it('should verify will invoke correct methods when Task is provided', () => { + // Given + const taskIdentifier = `search_data __ ${ new Date().toISOString() }`; + const componentState = createComponentState(router, id, INITIALIZED); + const typeStub = '[component] Load Users'; + const modelStub = { + getComponentState: () => componentState + } as ComponentModel; + const getModelSpy = spyOn(service, 'getModel').and.returnValue(of(modelStub)); + const actionStub = { + type: typeStub, + payload: componentState, + task: taskIdentifier + }; + const genericActionOfSpy = spyOn(GenericAction, 'of').and.returnValue(actionStub); + + // When + service.dispatchAction(typeStub, componentState, taskIdentifier); + + // Then + expect(getModelSpy).toHaveBeenCalledWith(componentState.id, componentState.routePathSegments, ['*']); + expect(genericActionOfSpy).toHaveBeenCalledWith(typeStub, componentState, taskIdentifier); + expect(storeStub$.dispatch).toHaveBeenCalledWith(actionStub); + }); + }); + }); +}); + +const createComponentState = (router: RouterReducerState, id: string, status: StatusType) => { + return ComponentStateImpl.of({ + id, + status, + routePath: router?.state.routePath ?? null, + routePathSegments: router?.state.routePathSegments ?? [], + navigationId: router.navigationId + }); +}; + +const getComponentState = (state: StoreState | LiteralComponentsState, id: string): ComponentState => { + if (CollectionsUtil.isNil(state)) { + return ComponentStateImpl.of({}); + } + + const componentsState = ( + CollectionsUtil.isNil((state as LiteralComponentsState).routePathSegments) + ? state[STORE_COMPONENTS] + : state + ) as LiteralComponentsState; + + return ComponentStateImpl.fromLiteralComponentState( + componentsState + .routePathSegments['test_domain/context'] + .routePathSegments['test_entity/10'] + .components[id] + ); +}; + +const createStoreState = (state?: LiteralComponentState): StoreState => { + return CollectionsUtil.isNil(state) + ? { + [STORE_ROUTER]: RouterState.of( + RouteState.of( + RouteSegments.of( + 'test_entity/10', + {}, + { entity: 10 }, + null, + RouteSegments.of('test_domain/context') + ), + '/domain/context/entity/10' + ), + 3 + ), + [STORE_COMPONENTS]: { + components: {}, + routePathSegments: {} + } + } + : { + [STORE_ROUTER]: RouterState.of( + RouteState.of( + RouteSegments.of( + 'test_entity/10', + {}, + { entity: 10 }, + null, + RouteSegments.of('test_domain/context') + ), + '/domain/context/entity/10' + ), + 3 + ), + [STORE_COMPONENTS]: { + components: {}, + routePathSegments: { + 'test_domain/context': { + components: {}, + routePathSegments: { + 'test_entity/10': { + components: { + [state.id]: state + }, + routePathSegments: {} + } + } + } + } + } + }; +}; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/services/component.service.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/services/component.service.ts new file mode 100644 index 0000000000..454995e8b6 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/services/component.service.ts @@ -0,0 +1,396 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +/* eslint-disable @typescript-eslint/unified-signatures,ngrx/avoid-mapping-selectors */ + +import { Injectable } from '@angular/core'; + +import { Observable } from 'rxjs'; +import { filter, map, switchMap, take, withLatestFrom } from 'rxjs/operators'; + +import { Store } from '@ngrx/store'; + +import { CollectionsUtil } from '../../../utils'; + +import { RouterService, RouterState, RouteState } from '../../router'; +import { GenericAction, STORE_COMPONENTS, StoreState } from '../../ngrx'; + +import { ComponentIdle, ComponentInit, ComponentLoading, ComponentUpdate } from '../state'; +import { + ComponentsStateHelper, + ComponentState, + ComponentStateImpl, + FAILED, + IDLE, + INITIALIZED, + LiteralComponentsState, + LOADED, + LOADING, + StatusType +} from '../model/state'; +import { ComponentModel } from '../model'; + +/** + * ** Service that manage Components State. + * + * + */ +export abstract class ComponentService { + /** + * ** Initialize Component State and return Model. + */ + abstract init(id: string, routeState: RouteState): Observable; + + /** + * ** Set Component status to IDLE. + */ + abstract idle(componentState: ComponentState): void; + + /** + * ** Load Component State and return Model. + */ + abstract load(componentState: ComponentState): Observable; + + /** + * ** Update Component State. + */ + abstract update(componentState: ComponentState): void; + + /** + * ** Acknowledge if has ComponentState in segment. + * + * - true - has ComponentState. + * - false - doesn't have ComponentState. + */ + abstract hasInSegment(id: string, routePathSegments: string[]): Observable; + + /** + * ** Listener that fires once after successful Component State initialization and returns Model. + */ + abstract onInit(id: string, routePathSegments: string[]): Observable; + + /** + * ** Listener that fires once after successful Component State load and returns Model. + */ + abstract onLoaded(id: string, routePathSegments: string[]): Observable; + + /** + * ** Returns stream with value Component Model and fires whenever Component State changes in Store. + * + * - If no statusWatch provided by default will listen for statuses {@link LOADED} and {@link FAILED}. + */ + abstract getModel(id: string, routePathSegments: string[], statusWatch?: Array): Observable; + + /** + * ** Dispatch GenericAction with provided Type, ComponentState and optionally task. + */ + abstract dispatchAction(type: string, componentState: ComponentState, task?: string): void; + + /** + * ** Initialize Service. + */ + abstract initialize(): void; +} + +/** + * @inheritDoc + */ +@Injectable() +export class ComponentServiceImpl extends ComponentService { + private readonly componentsStateHelper: ComponentsStateHelper; + + /** + * ** Constructor. + */ + constructor(private readonly store$: Store, + private readonly routerService: RouterService) { + super(); + + this.componentsStateHelper = new ComponentsStateHelper(); + } + + /** + * @inheritDoc + */ + init(id: string, routeState: RouteState): Observable { + this.store$ + .select((store) => store) + .pipe( + take(1), + map((store) => this._getComponentState(id, routeState.routePathSegments, store.router, store.components)) + ) + .subscribe((componentState) => { + if (componentState.status === INITIALIZED) { + this.store$.dispatch( + ComponentInit.of(componentState) + ); + } + }); + + return this.onInit(id, routeState.routePathSegments); + } + + /** + * @inheritDoc + */ + idle(componentState: ComponentState): void { + this.store$.dispatch( + ComponentIdle.of(componentState) + ); + } + + /** + * @inheritDoc + */ + load(componentState: ComponentState): Observable { + this.routerService + .get() + .pipe( + take(1) + ) + .subscribe((routerState: RouterState) => { + if (componentState.status === INITIALIZED) { + this.store$.dispatch( + ComponentIdle.of( + componentState.copy({ + status: IDLE, + navigationId: routerState.navigationId + }) + ) + ); + } + + this.store$.dispatch( + ComponentLoading.of( + componentState.copy({ + status: LOADING, + navigationId: routerState.navigationId + }) + ) + ); + }); + + return this.onLoaded(componentState.id, componentState.routePathSegments); + } + + /** + * @inheritDoc + */ + update(componentState: ComponentState): void { + this.routerService + .get() + .pipe( + take(1) + ) + .subscribe((routerState) => { + this.store$.dispatch( + ComponentUpdate.of( + componentState.copy({ + navigationId: routerState.navigationId + }) + ) + ); + }); + } + + /** + * @inheritDoc + */ + hasInSegment(id: string, routePathSegments: string[]): Observable { + return this.store$ + .select(STORE_COMPONENTS) + .pipe( + withLatestFrom(this.routerService.getState()), + map(([literalComponentsState, routeState]) => + this._isComponentInStatus(id, routePathSegments, literalComponentsState, routeState, ['*'])), + take(1) + ); + } + + /** + * @inheritDoc + */ + onInit(id: string, routePathSegments: string[]): Observable { + return this.store$ + .select(STORE_COMPONENTS) + .pipe( + withLatestFrom(this.routerService.getState()), + map(([literalComponentsState, routeState]) => + this._isComponentInStatus(id, routePathSegments, literalComponentsState, routeState, ['*'])), + filter((isInitialized) => isInitialized), + switchMap(() => this.getModel(id, routePathSegments, ['*'])), + take(1) + ); + } + + /** + * @inheritDoc + */ + onLoaded(id: string, routePathSegments: string[]): Observable { + return this.store$ + .select(STORE_COMPONENTS) + .pipe( + withLatestFrom(this.routerService.getState()), + map(([literalComponentsState, routeState]) => + this._isComponentInStatus(id, routePathSegments, literalComponentsState, routeState, [LOADED, FAILED])), + filter((isLoaded) => isLoaded), + switchMap(() => this.getModel(id, routePathSegments)), + take(1) + ); + } + + /** + * @inheritDoc + */ + getModel(id: string, routePathSegments: string[], statusWatch?: Array): Observable { + const _statusWatch = statusWatch ?? [LOADED, FAILED]; + + return this.store$ + .select(STORE_COMPONENTS) + .pipe( + switchMap((literalComponentsState) => this.routerService.get().pipe( + map((routerState) => [literalComponentsState, routerState]) + )), + filter(([literalComponentsState, routerState]: [LiteralComponentsState, RouterState]) => + this._isComponentInStatus(id, routePathSegments, literalComponentsState, routerState.state, _statusWatch)), + map(([literalComponentsState, routerState]) => + this._createModel(id, routePathSegments, literalComponentsState, routerState)) + ); + } + + /** + * @inheritDoc + */ + dispatchAction(type: string, componentState: ComponentState, task?: string): void { + this.getModel(componentState.id, componentState.routePathSegments, ['*']) + .pipe( + take(1) + ) + .subscribe((model) => + this.store$.dispatch( + GenericAction.of( + type, + model.getComponentState(), + task + ) + ) + ); + } + + /** + * @inheritDoc + */ + initialize() { + // No-op. + } + + // Get Component State from Store if exist, otherwise create new State. + private _getComponentState( + id: string, + routePathSegments: string[], + routerState: RouterState, + literalComponentsState: LiteralComponentsState): ComponentState { + + let _navigationId: number = null; + let _routePath: string = null; + let _routePathSegments: string[] = []; + + if (routerState) { + _navigationId = routerState.navigationId; + + if (routerState.state && !routePathSegments) { + _routePath = routerState.state.routePath; + _routePathSegments = routerState.state.routePathSegments; + } + } + + if (routePathSegments) { + _routePath = routePathSegments.slice().pop(); + _routePathSegments = routePathSegments; + } + + let componentState = this.componentsStateHelper + .setState(literalComponentsState) + .getComponentState( + id, + _routePathSegments + ); + + if (componentState) { + return componentState; + } + + componentState = ComponentStateImpl.of({ + id, + status: INITIALIZED, + routePath: _routePath, + routePathSegments: _routePathSegments, + navigationId: _navigationId + }); + + return componentState; + } + + // Utility method that filter if provided state is in desired status. + private _isComponentInStatus( + id: string, + routePathSegments: string[], + literalComponentsState: LiteralComponentsState, + routeState: RouteState, + statusWatch: Array): boolean { + + let _routePathSegments: string[] = []; + + if (CollectionsUtil.isArray(routePathSegments)) { + _routePathSegments = routePathSegments; + } else if (routeState) { + _routePathSegments = routeState.routePathSegments; + } + + const componentLiteralState = this.componentsStateHelper + .setState(literalComponentsState) + .getLiteralComponentState( + id, + _routePathSegments + ); + + if (!componentLiteralState) { + return false; + } + + if (statusWatch.indexOf('*') !== -1) { + return true; + } + + return statusWatch.indexOf(componentLiteralState.status) !== -1; + } + + // Creates Model from provided data. + private _createModel( + id: string, + routePathSegments: string[], + literalComponentsState: LiteralComponentsState, + routerState: RouterState): ComponentModel { + + let _routePathSegments: string[] = []; + + if (CollectionsUtil.isArray(routePathSegments)) { + _routePathSegments = routePathSegments; + } else if (routerState && routerState.state) { + _routePathSegments = routerState.state.routePathSegments; + } + + const componentState = this.componentsStateHelper + .setState(literalComponentsState) + .getComponentState( + id, + _routePathSegments + ); + + return ComponentModel.of(componentState, CollectionsUtil.cloneDeep(routerState)); + } +} diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/services/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/services/index.ts new file mode 100644 index 0000000000..fab0d2dcd2 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/services/index.ts @@ -0,0 +1,8 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './component.service'; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/state/actions/component.actions.spec.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/state/actions/component.actions.spec.ts new file mode 100644 index 0000000000..36ca688537 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/state/actions/component.actions.spec.ts @@ -0,0 +1,293 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { + COMPONENT_CLEAR_DATA, + COMPONENT_FAILED, + COMPONENT_IDLE, + COMPONENT_INIT, + COMPONENT_LOADED, + COMPONENT_LOADING, + COMPONENT_UPDATE, + ComponentClearData, + ComponentFailed, + ComponentIdle, + ComponentInit, + ComponentLoaded, + ComponentLoading, + ComponentUpdate +} from './component.actions'; +import { ComponentStateImpl } from '../../model'; +import { BaseAction, BaseActionWithPayload } from '../../../ngrx'; + +describe('ComponentActions', () => { + describe('ComponentInit', () => { + it('should verify instance is created', () => { + // Then + expect(() => new ComponentInit(ComponentStateImpl.of({}))).toBeDefined(); + }); + + it('should verify correct type is assigned', () => { + // When + const instance = new ComponentInit(ComponentStateImpl.of({})); + + // Then + expect(instance.type).toEqual(COMPONENT_INIT); + }); + + it('should verify prototype chaining', () => { + // When + const instance = new ComponentInit(ComponentStateImpl.of({})); + + // Then + expect(instance).toBeInstanceOf(BaseActionWithPayload); + expect(instance).toBeInstanceOf(BaseAction); + }); + + describe('Statics::', () => { + describe('Methods::', () => { + describe('|of|', () => { + it('should verify factory method will create instance', () => { + // When + const instance = ComponentInit.of(ComponentStateImpl.of({})); + + // Then + expect(instance).toBeInstanceOf(ComponentInit); + }); + }); + }); + }); + }); + + describe('ComponentIdle', () => { + it('should verify instance is created', () => { + // Then + expect(() => new ComponentIdle(ComponentStateImpl.of({}))).toBeDefined(); + }); + + it('should verify correct type is assigned', () => { + // When + const instance = new ComponentIdle(ComponentStateImpl.of({})); + + // Then + expect(instance.type).toEqual(COMPONENT_IDLE); + }); + + it('should verify prototype chaining', () => { + // When + const instance = new ComponentIdle(ComponentStateImpl.of({})); + + // Then + expect(instance).toBeInstanceOf(BaseActionWithPayload); + expect(instance).toBeInstanceOf(BaseAction); + }); + + describe('Statics::', () => { + describe('Methods::', () => { + describe('|of|', () => { + it('should verify factory method will create instance', () => { + // When + const instance = ComponentIdle.of(ComponentStateImpl.of({})); + + // Then + expect(instance).toBeInstanceOf(ComponentIdle); + }); + }); + }); + }); + }); + + describe('ComponentLoading', () => { + it('should verify instance is created', () => { + // Then + expect(() => new ComponentLoading(ComponentStateImpl.of({}))).toBeDefined(); + }); + + it('should verify correct type is assigned', () => { + // When + const instance = new ComponentLoading(ComponentStateImpl.of({})); + + // Then + expect(instance.type).toEqual(COMPONENT_LOADING); + }); + + it('should verify prototype chaining', () => { + // When + const instance = new ComponentLoading(ComponentStateImpl.of({})); + + // Then + expect(instance).toBeInstanceOf(BaseActionWithPayload); + expect(instance).toBeInstanceOf(BaseAction); + }); + + describe('Statics::', () => { + describe('Methods::', () => { + describe('|of|', () => { + it('should verify factory method will create instance', () => { + // When + const instance = ComponentLoading.of(ComponentStateImpl.of({})); + + // Then + expect(instance).toBeInstanceOf(ComponentLoading); + }); + }); + }); + }); + }); + + describe('ComponentLoaded', () => { + it('should verify instance is created', () => { + // Then + expect(() => new ComponentLoaded(ComponentStateImpl.of({}))).toBeDefined(); + }); + + it('should verify correct type is assigned', () => { + // When + const instance = new ComponentLoaded(ComponentStateImpl.of({})); + + // Then + expect(instance.type).toEqual(COMPONENT_LOADED); + }); + + it('should verify prototype chaining', () => { + // When + const instance = new ComponentLoaded(ComponentStateImpl.of({})); + + // Then + expect(instance).toBeInstanceOf(BaseActionWithPayload); + expect(instance).toBeInstanceOf(BaseAction); + }); + + describe('Statics::', () => { + describe('Methods::', () => { + describe('|of|', () => { + it('should verify factory method will create instance', () => { + // When + const instance = ComponentLoaded.of(ComponentStateImpl.of({})); + + // Then + expect(instance).toBeInstanceOf(ComponentLoaded); + }); + }); + }); + }); + }); + + describe('ComponentFailed', () => { + it('should verify instance is created', () => { + // Then + expect(() => new ComponentFailed(ComponentStateImpl.of({}))).toBeDefined(); + }); + + it('should verify correct type is assigned', () => { + // When + const instance = new ComponentFailed(ComponentStateImpl.of({})); + + // Then + expect(instance.type).toEqual(COMPONENT_FAILED); + }); + + it('should verify prototype chaining', () => { + // When + const instance = new ComponentFailed(ComponentStateImpl.of({})); + + // Then + expect(instance).toBeInstanceOf(BaseActionWithPayload); + expect(instance).toBeInstanceOf(BaseAction); + }); + + describe('Statics::', () => { + describe('Methods::', () => { + describe('|of|', () => { + it('should verify factory method will create instance', () => { + // When + const instance = ComponentFailed.of(ComponentStateImpl.of({})); + + // Then + expect(instance).toBeInstanceOf(ComponentFailed); + }); + }); + }); + }); + }); + + describe('ComponentUpdate', () => { + it('should verify instance is created', () => { + // Then + expect(() => new ComponentUpdate(ComponentStateImpl.of({}))).toBeDefined(); + }); + + it('should verify correct type is assigned', () => { + // When + const instance = new ComponentUpdate(ComponentStateImpl.of({})); + + // Then + expect(instance.type).toEqual(COMPONENT_UPDATE); + }); + + it('should verify prototype chaining', () => { + // When + const instance = new ComponentUpdate(ComponentStateImpl.of({})); + + // Then + expect(instance).toBeInstanceOf(BaseActionWithPayload); + expect(instance).toBeInstanceOf(BaseAction); + }); + + describe('Statics::', () => { + describe('Methods::', () => { + describe('|of|', () => { + it('should verify factory method will create instance', () => { + // When + const instance = ComponentUpdate.of(ComponentStateImpl.of({})); + + // Then + expect(instance).toBeInstanceOf(ComponentUpdate); + }); + }); + }); + }); + }); + + describe('ComponentClearData', () => { + it('should verify instance is created', () => { + // Then + expect(() => new ComponentClearData(ComponentStateImpl.of({}))).toBeDefined(); + }); + + it('should verify correct type is assigned', () => { + // When + const instance = new ComponentClearData(ComponentStateImpl.of({})); + + // Then + expect(instance.type).toEqual(COMPONENT_CLEAR_DATA); + }); + + it('should verify prototype chaining', () => { + // When + const instance = new ComponentClearData(ComponentStateImpl.of({})); + + // Then + expect(instance).toBeInstanceOf(BaseActionWithPayload); + expect(instance).toBeInstanceOf(BaseAction); + }); + + describe('Statics::', () => { + describe('Methods::', () => { + describe('|of|', () => { + it('should verify factory method will create instance', () => { + // When + const instance = ComponentClearData.of(ComponentStateImpl.of({})); + + // Then + expect(instance).toBeInstanceOf(ComponentClearData); + }); + }); + }); + }); + }); +}); diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/state/actions/component.actions.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/state/actions/component.actions.ts new file mode 100644 index 0000000000..467aca13e0 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/state/actions/component.actions.ts @@ -0,0 +1,177 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { BaseActionWithPayload } from '../../../ngrx/actions'; + +import { ComponentState } from '../../model'; + +/** + * ** Action Identifier for Component Initialization. + */ +export const COMPONENT_INIT = '[component] Init'; + +/** + * ** Action Identifier for Component Idle. + */ +export const COMPONENT_IDLE = '[component] Idle'; + +/** + * ** Action Identifier for Component start Loading data. + */ +export const COMPONENT_LOADING = '[component] Loading'; + +/** + * ** Action Identifier for Component Loaded data. + */ +export const COMPONENT_LOADED = '[component] Loaded'; + +/** + * ** Action Identifier for Component Failed loading data. + */ +export const COMPONENT_FAILED = '[component] Failed'; + +/** + * ** Action Identifier for Component Update state. + */ +export const COMPONENT_UPDATE = '[component] Update'; + +/** + * ** Action Identifier for Component Clear data. + */ +export const COMPONENT_CLEAR_DATA = '[component] Clear data'; + +/** + * ** Action for Component Initialization. + * + * + */ +export class ComponentInit extends BaseActionWithPayload { + constructor(payload: ComponentState) { + super(COMPONENT_INIT, payload); + } + + /** + * ** Factory method. + */ + static override of(payload: ComponentState) { + return new ComponentInit(payload); + } +} + +/** + * ** Action for Component Idle. + * + * + */ +export class ComponentIdle extends BaseActionWithPayload { + constructor(payload: ComponentState) { + super(COMPONENT_IDLE, payload); + } + + /** + * ** Factory method. + */ + static override of(payload: ComponentState) { + return new ComponentIdle(payload); + } +} + +/** + * ** Action for Component Loading. + * + * + */ +export class ComponentLoading extends BaseActionWithPayload { + constructor(payload: ComponentState) { + super(COMPONENT_LOADING, payload); + } + + /** + * ** Factory method. + */ + static override of(payload: ComponentState) { + return new ComponentLoading(payload); + } +} + +/** + * ** Action for Component Loaded. + * + * + */ +export class ComponentLoaded extends BaseActionWithPayload { + constructor(payload: ComponentState) { + super(COMPONENT_LOADED, payload); + } + + /** + * ** Factory method. + */ + static override of(payload: ComponentState) { + return new ComponentLoaded(payload); + } +} + +/** + * ** Action for Component Failed. + * + * + */ +export class ComponentFailed extends BaseActionWithPayload { + constructor(payload: ComponentState) { + super(COMPONENT_FAILED, payload); + } + + /** + * ** Factory method. + */ + static override of(payload: ComponentState) { + return new ComponentFailed(payload); + } +} + +/** + * ** Action for Component Update. + * + * + */ +export class ComponentUpdate extends BaseActionWithPayload { + constructor(payload: ComponentState) { + super(COMPONENT_UPDATE, payload); + } + + /** + * ** Factory method. + */ + static override of(payload: ComponentState) { + return new ComponentUpdate(payload); + } +} + +/** + * ** Action for Component Clear Data. + * + * + */ +export class ComponentClearData extends BaseActionWithPayload { + constructor(payload: ComponentState) { + super(COMPONENT_CLEAR_DATA, payload); + } + + /** + * ** Factory method. + */ + static override of(payload: ComponentState) { + return new ComponentClearData(payload); + } +} + +/** + * ** Union of all Actions that could be use as a type in Effects/Reducers/etc... + */ +export type ComponentActions = ComponentInit | ComponentIdle | ComponentLoading | + ComponentLoaded | ComponentFailed | ComponentUpdate | ComponentClearData; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/state/actions/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/state/actions/index.ts new file mode 100644 index 0000000000..f52a972a87 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/state/actions/index.ts @@ -0,0 +1,8 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './component.actions'; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/state/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/state/index.ts new file mode 100644 index 0000000000..22087bff61 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/state/index.ts @@ -0,0 +1,10 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './actions'; +export * from './reducers'; +export * from './operators'; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/state/operators/component-rx.operators.spec.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/state/operators/component-rx.operators.spec.ts new file mode 100644 index 0000000000..dba55fb8fd --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/state/operators/component-rx.operators.spec.ts @@ -0,0 +1,234 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { throwError } from 'rxjs'; + +import { marbles } from 'rxjs-marbles/jasmine'; + +import { RouterState, RouteSegments, RouteState } from '../../../router'; + +import { BaseActionWithPayload } from '../../../ngrx'; + +import { ComponentModel, ComponentState, ComponentStateImpl, FAILED, INITIALIZED, LOADED } from '../../model'; +import { ComponentService } from '../../services'; + +import { ComponentFailed } from '../actions'; + +import { getModel, getModelAndTask, handleActionError } from './component-rx.operators'; +import { CollectionsUtil } from '../../../../utils'; + +describe('rxjs operators', () => { + describe('|getModel|', () => { + it('should verify will create ComponentModel down the stream', marbles((m) => { + // Given + const inputActionStream$ = m.cold('----a---b----', { + a: new ActionStub(ComponentStateImpl.of({ + id: 'test1', + status: INITIALIZED, + routePathSegments: ['entity/10'] + })), + b: new ActionStub(ComponentStateImpl.of({ + id: 'test2', status: LOADED, + routePathSegments: ['entity/225'] + })) + }); + const getModelStream1$ = m.cold('-a', { + a: ComponentModel.of( + ComponentStateImpl.of({ id: 'test1', status: INITIALIZED }), + RouterState.of( + RouteState.of( + RouteSegments.of('entity/10'), + 'entity/5' + ), + 3 + ) + ) + }); + const getModelStream2$ = m.cold('--a', { + a: ComponentModel.of( + ComponentStateImpl.of({ id: 'test5', status: LOADED }), + RouterState.of( + RouteState.of( + RouteSegments.of('entity/225'), + 'entity/225' + ), + 3 + ) + ) + }); + const expectedOutputStream$ = m.cold('-----a----b----', { + a: ComponentModel.of( + ComponentStateImpl.of({ id: 'test1', status: INITIALIZED }), + RouterState.of( + RouteState.of( + RouteSegments.of('entity/10'), + 'entity/5' + ), + 3 + ) + ), + b: ComponentModel.of( + ComponentStateImpl.of({ id: 'test5', status: LOADED }), + RouterState.of( + RouteState.of( + RouteSegments.of('entity/225'), + 'entity/225' + ), + 3 + ) + ) + }); + + const componentServiceStub = jasmine.createSpyObj('componentService', ['getModel']); + let cnt = 0; + componentServiceStub.getModel.and.callFake(() => { + cnt++; + if (cnt === 1) { + return getModelStream1$; + } else { + return getModelStream2$; + } + }); + + // When + const response$ = getModel(componentServiceStub)(inputActionStream$); + + // Then + m.expect(response$).toBeObservable(expectedOutputStream$); + })); + }); + + describe('|getModelAndTask|', () => { + it('should verify will create ComponentModel down the stream', marbles((m) => { + // Given + const taskIdentifier1 = `patch_entity __ ${ new Date().toISOString() }`; + const taskIdentifier2 = `patch_dataset __ ${ new Date().toISOString() }`; + spyOn(CollectionsUtil, 'interpolateString').and.returnValues(taskIdentifier1, taskIdentifier2); + const inputActionStream$ = m.cold('----a---b----', { + a: new ActionStub( + ComponentStateImpl.of({ + id: 'test1', + status: INITIALIZED, + routePathSegments: ['entity/10'] + }), + taskIdentifier1 + ), + b: new ActionStub( + ComponentStateImpl.of({ + id: 'test2', status: LOADED, + routePathSegments: ['entity/225'] + }), + taskIdentifier2 + ) + }); + const getModelStream1$ = m.cold('-a', { + a: ComponentModel.of( + ComponentStateImpl.of({ id: 'test1', status: INITIALIZED }), + RouterState.of( + RouteState.of( + RouteSegments.of('entity/10'), + 'entity/5' + ), + 3 + ) + ) + }); + const getModelStream2$ = m.cold('--a', { + a: ComponentModel.of( + ComponentStateImpl.of({ id: 'test5', status: LOADED }), + RouterState.of( + RouteState.of( + RouteSegments.of('entity/225'), + 'entity/225' + ), + 3 + ) + ) + }); + const expectedOutputStream$ = m.cold('-----a----b----', { + a: [ + ComponentModel.of( + ComponentStateImpl.of({ id: 'test1', status: INITIALIZED }), + RouterState.of( + RouteState.of( + RouteSegments.of('entity/10'), + 'entity/5' + ), + 3 + ) + ), + taskIdentifier1 + ] as [ComponentModel, string], + b: [ + ComponentModel.of( + ComponentStateImpl.of({ id: 'test5', status: LOADED }), + RouterState.of( + RouteState.of( + RouteSegments.of('entity/225'), + 'entity/225' + ), + 3 + ) + ), + taskIdentifier2 + ] as [ComponentModel, string] + }); + + const componentServiceStub = jasmine.createSpyObj('componentService', ['getModel']); + let cnt = 0; + componentServiceStub.getModel.and.callFake(() => { + cnt++; + if (cnt === 1) { + return getModelStream1$; + } else { + return getModelStream2$; + } + }); + + // When + const response$ = getModelAndTask(componentServiceStub)(inputActionStream$); + + // Then + m.expect(response$).toBeObservable(expectedOutputStream$); + })); + }); + + describe('|handleActionError|', () => { + it('should verify will handle Stream error and return ComponentFailed action', marbles((m) => { + // Given + const inputModel = ComponentModel.of( + ComponentStateImpl.of({ id: 'test1', status: INITIALIZED, navigationId: 3 }), + RouterState.of( + RouteState.of( + RouteSegments.of('entity/10'), + 'entity/5' + ), + 3 + ) + ); + const error = new Error('Http Failure'); + const getResponseStream$ = throwError(error); + const expectedOutputStream$ = m.cold('(a|)', { + a: ComponentFailed.of( + ComponentStateImpl.of({ id: 'test1', status: FAILED, navigationId: 3, error }) + ) + }); + + // When + const response$ = handleActionError(inputModel)(getResponseStream$); + + // Then + m.expect(response$).toBeObservable(expectedOutputStream$); + })); + }); +}); + +class ActionStub extends BaseActionWithPayload { + constructor(payload: ComponentState, task?: string) { + super('[component] action stub', payload, task); + } +} diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/state/operators/component-rx.operators.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/state/operators/component-rx.operators.ts new file mode 100644 index 0000000000..d9d9374ef8 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/state/operators/component-rx.operators.ts @@ -0,0 +1,99 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { iif, Observable, of, throwError } from 'rxjs'; +import { catchError, map, mergeMap, switchMap, take } from 'rxjs/operators'; + +import { BaseActionWithPayload } from '../../../ngrx/actions'; + +import { ComponentService } from '../../services'; +import { ComponentModel, ComponentState } from '../../model'; + +import { ComponentFailed } from '../actions'; + +/** + * ** RXJS Operator that fetch Component Model from Store using provided ComponentService. + * + * - Returns Observable + * + * + */ +export const getModel = (componentService: ComponentService) => + (source$: Observable>): Observable => + source$.pipe( + extractAction, + switchMap((action) => subscribeForModel(action, componentService)) + ); + +/** + * ** RXJS Operator that fetch Component Model from Store using provided ComponentService and consume task from Action. + * + * - Returns Observable<[ComponentModel, string]> + * + * + */ +export const getModelAndTask = (componentService: ComponentService) => + (source$: Observable>): Observable<[ComponentModel, string]> => + source$.pipe( + extractAction, + switchMap((action) => + subscribeForModel(action, componentService).pipe( + map((model) => [model, action.task] as [ComponentModel, string]) + ) + ) + ); + +/** + * ** Handle Error from some Action, ideal to use for API response. + * + * - Catch error if happen, append it to Model, set status to FAILED, + * create instance of {@link ComponentFailed} and return stream in success state using {@link of} rjxs operator. + * + * + */ +export const handleActionError = (model: ComponentModel) => + (source$: Observable>): Observable> => + source$.pipe( + catchError((error: unknown) => of( + ComponentFailed.of( + model + .withError(error as Error) + .withStatusFailed() + .getComponentState() + ) + )) + ); + +/** + * ** Validate if Action is instance of {@link BaseActionWithPayload} and pass down the stream. + * + * + */ +const extractAction = (source$: Observable>) => + source$.pipe( + mergeMap((action) => + iif( + () => action instanceof BaseActionWithPayload, + of(action), + throwError( + new Error('Unsupported Action type. It should be subclass of BaseActionWithPayload') + ) + ) + ) + ); + +/** + * ** Make actual subscription to Store for ComponentModel. + * + * + */ +const subscribeForModel = (action: BaseActionWithPayload, componentService: ComponentService) => + componentService + .getModel(action.payload.id, action.payload.routePathSegments, ['*']) + .pipe( + take(1) + ); diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/state/operators/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/state/operators/index.ts new file mode 100644 index 0000000000..ec099b75ac --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/state/operators/index.ts @@ -0,0 +1,8 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './component-rx.operators'; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/state/reducers/component.reducer.spec.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/state/reducers/component.reducer.spec.ts new file mode 100644 index 0000000000..801b50a8b3 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/state/reducers/component.reducer.spec.ts @@ -0,0 +1,425 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +/* eslint-disable arrow-body-style,prefer-arrow/prefer-arrow-functions */ + +import { ROUTER_NAVIGATION, RouterNavigationPayload } from '@ngrx/router-store'; + +import { CallFake } from '../../../../unit-testing'; + +import { CollectionsUtil } from '../../../../utils'; + +import { BaseActionWithPayload } from '../../../ngrx'; + +import { RouteSegments, RouteState } from '../../../router'; + +import { + ComponentsStateHelper, + ComponentState, + FAILED, + IDLE, + INITIALIZED, + LiteralComponentsState, + LiteralComponentState, + LOADED, + LOADING, + StatusType +} from '../../model'; + +import { + COMPONENT_CLEAR_DATA, + COMPONENT_FAILED, + COMPONENT_INIT, + COMPONENT_LOADING, + COMPONENT_UPDATE, + ComponentIdle, + ComponentLoaded +} from '../actions'; + +import { componentReducer } from './component.reducer'; + +describe('componentReducer', () => { + let id: string; + let routePathSegments: string[]; + let literalComponentsState: LiteralComponentsState; + let updateComponentHelperSpy: jasmine.Spy; + let getComponentHelperSpy: jasmine.Spy; + let getStateHelperSpy: jasmine.Spy; + + beforeEach(() => { + id = 'testComponent1'; + routePathSegments = ['test_entity/15']; + literalComponentsState = { + components: {}, + routePathSegments: {} + }; + updateComponentHelperSpy = spyOn(ComponentsStateHelper.prototype, 'updateLiteralComponentState').and.callFake(CallFake); + getComponentHelperSpy = spyOn(ComponentsStateHelper.prototype, 'getLiteralComponentState'); + getStateHelperSpy = spyOn(ComponentsStateHelper.prototype, 'getState'); + }); + + describe('Actions::', () => { + describe('|COMPONENT_INIT|', () => { + it('should verify invokes correct methods', () => { + // Given + const action = new ActionStub(COMPONENT_INIT, createComponentState(null)); + getStateHelperSpy + .and + .returnValue(createComponentsState(createComponentState(INITIALIZED).toLiteral())); + + // When + const result = componentReducer(literalComponentsState, action); + + // Then + expect(result).toEqual(createComponentsState(createComponentState(INITIALIZED).toLiteral())); + expect(updateComponentHelperSpy).toHaveBeenCalledWith(createComponentState(INITIALIZED).toLiteral()); + }); + }); + + describe('|COMPONENT_IDLE|', () => { + it('should verify invokes correct methods', () => { + // Given + const action = ComponentIdle.of(createComponentState(INITIALIZED, 'test-team', 4)); + getComponentHelperSpy + .and + .returnValue(createComponentState(INITIALIZED).toLiteral()); + getStateHelperSpy + .and + .returnValue(createComponentsState(createComponentState(IDLE, 'test-team', 4).toLiteral())); + + // When + const result = componentReducer(literalComponentsState, action); + + // Then + expect(getComponentHelperSpy).toHaveBeenCalledWith(id, routePathSegments); + expect(result).toEqual(createComponentsState(createComponentState(IDLE, 'test-team', 4).toLiteral())); + expect(updateComponentHelperSpy).toHaveBeenCalledWith(createComponentState(IDLE, 'test-team', 4).toLiteral()); + }); + }); + + describe('|COMPONENT_LOADING|', () => { + it('should verify invokes correct methods', () => { + // Given + const action = new ActionStub(COMPONENT_LOADING, createComponentState(IDLE, 'test-team', 4)); + getComponentHelperSpy + .and + .returnValue(createComponentState(IDLE).toLiteral()); + getStateHelperSpy + .and + .returnValue(createComponentsState(createComponentState(LOADING, 'test-team', 4).toLiteral())); + + // When + const result = componentReducer(literalComponentsState, action); + + // Then + expect(getComponentHelperSpy).toHaveBeenCalledWith(id, routePathSegments); + expect(result).toEqual(createComponentsState(createComponentState(LOADING, 'test-team', 4).toLiteral())); + expect(updateComponentHelperSpy).toHaveBeenCalledWith(createComponentState(LOADING, 'test-team', 4).toLiteral()); + }); + }); + + describe('|COMPONENT_UPDATE|', () => { + it('should verify invokes correct methods (data from action)', () => { + // Given + const data = new Map([['users', []], ['cities', {}]]); + const action = new ActionStub( + COMPONENT_UPDATE, + createComponentState(IDLE, 'test-team', 7, data) + ); + getComponentHelperSpy + .and + .returnValue(createComponentState(LOADING).toLiteral()); + getStateHelperSpy + .and + .returnValue( + createComponentsState( + createComponentState(IDLE, 'test-team', 7, data).toLiteral() + ) + ); + + // When + const result = componentReducer(literalComponentsState, action); + + // Then + expect(getComponentHelperSpy).toHaveBeenCalledWith(id, routePathSegments); + expect(result).toEqual( + createComponentsState( + createComponentState(IDLE, 'test-team', 7, data).toLiteral() + ) + ); + expect(updateComponentHelperSpy).toHaveBeenCalledWith( + createComponentState(IDLE, 'test-team', 7, data).toLiteral() + ); + }); + + it('should verify invokes correct methods (data from store)', () => { + // Given + const data = new Map([['countries', []], ['payload', {}]]); + const action = new ActionStub( + COMPONENT_UPDATE, + createComponentState(FAILED, 'test-team', 6) + ); + getComponentHelperSpy + .and + .returnValue(createComponentState(LOADING, 'test-team', 4, data).toLiteral()); + getStateHelperSpy + .and + .returnValue( + createComponentsState( + createComponentState(FAILED, 'test-team', 6, data).toLiteral() + ) + ); + + // When + const result = componentReducer(literalComponentsState, action); + + // Then + expect(getComponentHelperSpy).toHaveBeenCalledWith(id, routePathSegments); + expect(result).toEqual( + createComponentsState( + createComponentState(FAILED, 'test-team', 6, data).toLiteral() + ) + ); + expect(updateComponentHelperSpy).toHaveBeenCalledWith( + createComponentState(FAILED, 'test-team', 6, data).toLiteral() + ); + }); + }); + + describe('|COMPONENT_LOADED|', () => { + it('should verify invokes correct methods', () => { + // Given + const action = ComponentLoaded.of(createComponentState(LOADING, 'test-team', 11)); + getComponentHelperSpy + .and + .returnValue(createComponentState(IDLE, 'test-team', 7).toLiteral()); + getStateHelperSpy + .and + .returnValue( + createComponentsState( + createComponentState(LOADED, 'test-team', 11).toLiteral() + ) + ); + + // When + const result = componentReducer(literalComponentsState, action); + + // Then + expect(getComponentHelperSpy).toHaveBeenCalledWith(id, routePathSegments); + expect(result).toEqual( + createComponentsState( + createComponentState(LOADED, 'test-team', 11).toLiteral() + ) + ); + expect(updateComponentHelperSpy).toHaveBeenCalledWith( + createComponentState(LOADED, 'test-team', 11).toLiteral() + ); + }); + }); + + describe('|COMPONENT_FAILED|', () => { + it('should verify invokes correct methods (data from action)', () => { + // Given + const data = new Map([['data', []], ['invokes', {}]]); + const action = new ActionStub( + COMPONENT_FAILED, + createComponentState(IDLE, 'test-team', 9, data) + ); + getComponentHelperSpy + .and + .returnValue(createComponentState(LOADING).toLiteral()); + getStateHelperSpy + .and + .returnValue( + createComponentsState( + createComponentState(FAILED, 'test-team', 9, data).toLiteral() + ) + ); + + // When + const result = componentReducer(literalComponentsState, action); + + // Then + expect(getComponentHelperSpy).toHaveBeenCalledWith(id, routePathSegments); + expect(result).toEqual( + createComponentsState( + createComponentState(FAILED, 'test-team', 9, data).toLiteral() + ) + ); + expect(updateComponentHelperSpy).toHaveBeenCalledWith( + createComponentState(FAILED, 'test-team', 9, data).toLiteral() + ); + }); + + it('should verify invokes correct methods (data from store)', () => { + // Given + const data = new Map([['wifi', []], ['routers', {}]]); + const action = new ActionStub( + COMPONENT_FAILED, + createComponentState(LOADING, 'test-team', 16) + ); + getComponentHelperSpy + .and + .returnValue(createComponentState(LOADING, 'test-team', 14, data).toLiteral()); + getStateHelperSpy + .and + .returnValue( + createComponentsState( + createComponentState(FAILED, 'test-team', 16, data).toLiteral() + ) + ); + + // When + const result = componentReducer(literalComponentsState, action); + + // Then + expect(getComponentHelperSpy).toHaveBeenCalledWith(id, routePathSegments); + expect(result).toEqual( + createComponentsState( + createComponentState(FAILED, 'test-team', 16, data).toLiteral() + ) + ); + expect(updateComponentHelperSpy).toHaveBeenCalledWith( + createComponentState(FAILED, 'test-team', 16, data).toLiteral() + ); + }); + }); + + describe('|COMPONENT_CLEAR_DATA|', () => { + it('should verify invokes correct methods', () => { + // Given + const data = new Map([['admins', []], ['telcos', {}]]); + const action = new ActionStub( + COMPONENT_CLEAR_DATA, + createComponentState(LOADED, 'test-team', 21) + ); + getComponentHelperSpy + .and + .returnValue(createComponentState(LOADED, 'test-team', 15, data).toLiteral()); + getStateHelperSpy + .and + .returnValue( + createComponentsState( + createComponentState(LOADING, 'test-team', 21).toLiteral() + ) + ); + + // When + const result = componentReducer(literalComponentsState, action); + + // Then + expect(getComponentHelperSpy).toHaveBeenCalledWith(id, routePathSegments); + expect(result).toEqual( + createComponentsState( + createComponentState(LOADING, 'test-team', 21).toLiteral() + ) + ); + expect(updateComponentHelperSpy).toHaveBeenCalledWith( + createComponentState(LOADING, 'test-team', 21).toLiteral() + ); + }); + }); + + describe('|ROUTER_NAVIGATION|', () => { + it('should verify invokes correct methods', () => { + // Given + const routerNavigationPayload = createRouterNavigationPayload(); + const action = new ActionStub>( + ROUTER_NAVIGATION, + routerNavigationPayload + ); + const resetComponentsHelperSpy = spyOn(ComponentsStateHelper.prototype, 'resetComponentStates').and.callFake(CallFake); + getStateHelperSpy + .and + .returnValue( + createComponentsState( + createComponentState(IDLE, 'test-team', 17).toLiteral() + ) + ); + + // When + const result = componentReducer(literalComponentsState, action); + + // Then + expect(resetComponentsHelperSpy).toHaveBeenCalledWith(routerNavigationPayload.routerState.routePathSegments); + expect(result).toEqual( + createComponentsState( + createComponentState(IDLE, 'test-team', 17).toLiteral() + ) + ); + }); + }); + + describe('|UNKNOWN_ACTION for this Reducer|', () => { + it('should verify will return state as it is', () => { + // Given + const action = new ActionStub('[action] unknown type', {}); + + // When + const result = componentReducer(literalComponentsState, action); + + // Then + expect(result).toBe(literalComponentsState); + }); + }); + }); +}); + +class ActionStub extends BaseActionWithPayload { + constructor(type: string, payload: T) { + super(type, payload); + } +} + +const createComponentState = ( + status: StatusType, + search = '', + navigationId = 3, + data: Map = new Map()): ComponentState => { + + const literalComponentState: LiteralComponentState = { + id: 'testComponent1', + status, + routePath: 'test_entity/15', + routePathSegments: ['test_entity/15'], + navigationId, + search, + data: CollectionsUtil.transformMapToObject(data) + }; + + return { + ...literalComponentState as any, + data, + copy: CallFake, + toLiteral: () => literalComponentState, + toLiteralDeepClone: () => literalComponentState + } as ComponentState; +}; + +const createComponentsState = (componentState: LiteralComponentState): LiteralComponentsState => { + return { + components: {}, + routePathSegments: { + 'test_entity/15': { + components: { + testComponent1: componentState + }, + routePathSegments: {} + } + } + }; +}; + +const createRouterNavigationPayload = (): RouterNavigationPayload => { + return { + routerState: RouteState.of( + RouteSegments.of('entity/21'), + 'entity/21' + ), + event: null + }; +}; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/state/reducers/component.reducer.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/state/reducers/component.reducer.ts new file mode 100644 index 0000000000..bdb6ec45de --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/state/reducers/component.reducer.ts @@ -0,0 +1,155 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +/* eslint-disable arrow-body-style,prefer-arrow/prefer-arrow-functions */ + +import { ROUTER_NAVIGATION, RouterNavigationAction } from '@ngrx/router-store'; + +import { CollectionsUtil } from '../../../../utils'; + +import { NavigationActions, RouteSegments, RouteState } from '../../../router'; + +import { ComponentsStateHelper, ComponentState, FAILED, IDLE, INITIALIZED, LiteralComponentState, LOADED, LOADING } from '../../model'; + +import { + COMPONENT_CLEAR_DATA, + COMPONENT_FAILED, + COMPONENT_IDLE, + COMPONENT_INIT, + COMPONENT_LOADED, + COMPONENT_LOADING, + COMPONENT_UPDATE, + ComponentActions, + ComponentClearData, + ComponentIdle, + ComponentInit, + ComponentLoaded, + ComponentLoading, + ComponentUpdate +} from '../actions'; + +const stateHelper = new ComponentsStateHelper(); + +/** + * ** Reducer for Components Actions. + * + * + */ +export function componentReducer( + state = stateHelper.getState(), + action: ComponentActions | NavigationActions = { type: null, payload: null }) { + + let actionComponentState: ComponentState; + let actionLiteralComponentState: LiteralComponentState; + let storeLiteralComponentState: LiteralComponentState; + + stateHelper.setState(state); + + switch (action.type) { + case COMPONENT_INIT: + actionComponentState = (action as ComponentInit).payload; + + stateHelper.updateLiteralComponentState({ + ...actionComponentState.toLiteralDeepClone(), + status: INITIALIZED + }); + + return stateHelper.getState(); + case COMPONENT_IDLE: + case COMPONENT_LOADING: + actionComponentState = (action as ComponentIdle | ComponentLoading).payload; + storeLiteralComponentState = stateHelper + .getLiteralComponentState(actionComponentState.id, actionComponentState.routePathSegments); + + actionLiteralComponentState = actionComponentState.toLiteralDeepClone(); + + stateHelper.updateLiteralComponentState({ + ...storeLiteralComponentState, + ...actionLiteralComponentState, + status: action instanceof ComponentIdle + ? IDLE + : LOADING + }); + + return stateHelper.getState(); + case COMPONENT_UPDATE: + case COMPONENT_LOADED: + actionComponentState = (action as (ComponentLoaded | ComponentUpdate)).payload; + storeLiteralComponentState = stateHelper + .getLiteralComponentState(actionComponentState.id, actionComponentState.routePathSegments); + + actionLiteralComponentState = actionComponentState.toLiteralDeepClone(); + + stateHelper.updateLiteralComponentState({ + ...storeLiteralComponentState, + ...actionLiteralComponentState, + status: action instanceof ComponentLoaded + ? LOADED + : actionComponentState.status, + data: getComponentStateData(actionLiteralComponentState, storeLiteralComponentState) + }); + + return stateHelper.getState(); + case COMPONENT_FAILED: + actionComponentState = (action as (ComponentLoaded | ComponentUpdate)).payload; + storeLiteralComponentState = stateHelper + .getLiteralComponentState(actionComponentState.id, actionComponentState.routePathSegments); + + actionLiteralComponentState = actionComponentState.toLiteralDeepClone(); + + stateHelper.updateLiteralComponentState({ + ...storeLiteralComponentState, + ...actionLiteralComponentState, + data: getComponentStateData(actionLiteralComponentState, storeLiteralComponentState), + status: FAILED + }); + + return stateHelper.getState(); + case COMPONENT_CLEAR_DATA: + actionComponentState = (action as ComponentClearData).payload; + storeLiteralComponentState = stateHelper.getLiteralComponentState( + actionComponentState.id, + actionComponentState.routePathSegments); + + stateHelper.updateLiteralComponentState({ + ...storeLiteralComponentState, + ...actionComponentState.toLiteralDeepClone(), + data: {}, + status: LOADING + }); + + return stateHelper.getState(); + case ROUTER_NAVIGATION: + const routeSegments = (action as RouterNavigationAction).payload.routerState.routeSegments; + + if (!(routeSegments instanceof RouteSegments)) { + return state; + } + + stateHelper.resetComponentStates(routeSegments.routePathSegments); + + return stateHelper.getState(); + default: + return state; + } +} + +const getComponentStateData = ( + actionComponentState: LiteralComponentState, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + storeLiteralComponentState: LiteralComponentState): { [key: string]: any } => { + + if (CollectionsUtil.isLiteralObjectWithProperties(actionComponentState.data)) { + return actionComponentState.data; + } + + if (CollectionsUtil.isDefined(storeLiteralComponentState)) { + return storeLiteralComponentState.data; + } + + return {}; +}; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/state/reducers/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/state/reducers/index.ts new file mode 100644 index 0000000000..56ef825780 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/component/state/reducers/index.ts @@ -0,0 +1,8 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './component.reducer'; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/index.ts new file mode 100644 index 0000000000..f76250a9ae --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/index.ts @@ -0,0 +1,13 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './component'; +export * from './navigation'; +export * from './ngrx'; +export * from './router'; +export * from './system-events'; +export * from './url-state-manager'; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/navigation/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/navigation/index.ts new file mode 100644 index 0000000000..4117a214b3 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/navigation/index.ts @@ -0,0 +1,8 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './services'; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/navigation/public-api.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/navigation/public-api.ts new file mode 100644 index 0000000000..61c133a9d8 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/navigation/public-api.ts @@ -0,0 +1,8 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +export { NavigationService } from './index'; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/navigation/services/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/navigation/services/index.ts new file mode 100644 index 0000000000..c438c6ff08 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/navigation/services/index.ts @@ -0,0 +1,8 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './navigation.service'; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/navigation/services/navigation.service.spec.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/navigation/services/navigation.service.spec.ts new file mode 100644 index 0000000000..bff88152f1 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/navigation/services/navigation.service.spec.ts @@ -0,0 +1,443 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +/* eslint-disable @typescript-eslint/dot-notation */ + +import { NavigationExtras, Router } from '@angular/router'; +import { TestBed } from '@angular/core/testing'; + +import { of } from 'rxjs'; + +import { TaurusRouteData } from '../../../common'; + +import { RouterService, RouteSegments, RouteState } from '../../router'; + +import { NavigationService } from './navigation.service'; + +describe('NavigationService', () => { + let routerStub: jasmine.SpyObj; + let routerServiceStub: jasmine.SpyObj; + + let service: NavigationService; + + beforeEach(() => { + routerStub = jasmine.createSpyObj('router', ['navigate']); + routerServiceStub = jasmine.createSpyObj('routerService', ['getState', 'initialize']); + + TestBed.configureTestingModule({ + providers: [ + { provide: Router, useValue: routerStub }, + { provide: RouterService, useValue: routerServiceStub }, + NavigationService + ] + }); + + service = TestBed.inject(NavigationService); + + // @ts-ignore + service['router'] = routerStub; + // @ts-ignore + service['routerService'] = routerServiceStub; + }); + + it('should verify instance is created', () => { + // Then + expect(service).toBeDefined(); + }); + + describe('Methods::()', () => { + describe('|initialize|', () => { + it('should verify method exist and is callable', () => { + // Then + expect(service.initialize).toBeDefined(); + expect(() => service.initialize()).not.toThrow(); + expect(routerServiceStub.initialize).toHaveBeenCalled(); + }); + }); + + describe('|navigate|', () => { + let url: string; + let urlChunks: string[]; + let extras: NavigationExtras; + + beforeEach(() => { + url = '/explore/data-jobs'; + urlChunks = ['explore', 'data-jobs']; + extras = { + queryParams: { + search: 'vdk' + }, + replaceUrl: true, + queryParamsHandling: 'merge' + }; + // @ts-ignore + service['router'] = routerStub; + }); + + it('should verify will call Router.navigate() from Angular', () => { + // Given + routerStub.navigate.and.returnValue(Promise.resolve(true)); + + // When + service.navigate(url); + + // Then + expect(routerStub.navigate).toHaveBeenCalledWith(['/explore', 'data-jobs']); + }); + + it('should verify will return Promise', () => { + // Given + routerStub.navigate.and.returnValue(Promise.resolve(true)); + + // When + const returnedValue = service.navigate(url); + + // Then + expect(returnedValue).toBeInstanceOf(Promise); + }); + + it('should verify will return Promise.resolve(false) on Nil value for url', () => { + // When + const returnedValue = service.navigate(null); + + // Then + returnedValue.then((v) => expect(v).toBeFalse()); + expect(routerStub.navigate).not.toHaveBeenCalled(); + }); + + it('should verify will handle both url string and url Array', () => { + // Given + routerStub.navigate.and.returnValue(Promise.resolve(true)); + + // When + service.navigate(url); + service.navigate(urlChunks); + + // Then + expect(routerStub.navigate).toHaveBeenCalledTimes(2); + expect(routerStub.navigate.calls.argsFor(0)).toEqual([['/explore', 'data-jobs']]); + expect(routerStub.navigate.calls.argsFor(1)).toEqual([['/explore', 'data-jobs']]); + }); + + it('should verify will handle extras parameters', () => { + // Given + routerStub.navigate.and.returnValue(Promise.resolve(true)); + + // When + service.navigate(url, extras); + + // Then + expect(routerStub.navigate).toHaveBeenCalledWith(['/explore', 'data-jobs'], extras); + }); + }); + + describe('|_navigationSystemEventHandler_|', () => { + it('should verify will skip navigation if payload is Nil', () => { + // Given + const payload: { url: string | string[]; extras?: NavigationExtras } = null; + const navigateSpy = spyOn(service, 'navigate').and.callFake(() => Promise.resolve(false)); + + // When + const returnedValue = service._navigationSystemEventHandler_(payload); + + // Then + returnedValue.then((v) => expect(v).toBeFalse()); + expect(navigateSpy).not.toHaveBeenCalled(); + }); + + it('should verify will execute navigation when payload is provided', () => { + // Given + const payload = { url: '/explore/data-jobs', extras: { queryParams: { search: 'vdk' } } }; + spyOn(service, 'navigate').and.callFake(() => Promise.resolve(true)); + + // When + const returnedValue = service._navigationSystemEventHandler_(payload); + + // Then + returnedValue.then((v) => expect(v).toBeTrue()); + expect(service.navigate).toHaveBeenCalledWith(payload.url, payload.extras); + }); + }); + + describe('|navigateBack|', () => { + it('should verify will invoke correct methods when NavigationAction provided', (done) => { + // Given + const routeState = RouteState.of( + RouteSegments.of( + 'delivery', + { + navigateBack: { + path: '$.parent', + queryParamsHandling: 'preserve', + queryParams: { randomParam: 'abc' } + } + } as TaurusRouteData, + {}, + { search: 'team-test' }, + RouteSegments.of( + 'entity/25', + { + navigateTo: { + path: '/domain/context/entity/{0}/update/information', + replacers: [ + { searchValue: '{0}', replaceValue: '$.entity' } + ], + queryParams: { showWizard: true } + } + } as TaurusRouteData, + { entityId: 25 }, + { search: 'team-test' }, + RouteSegments.of( + 'domain/context', + {}, + {}, + { search: 'team-test' }, + null, + 'domain/context' + ), + 'entity/:entityId' + ), + 'delivery' + ), + 'domain/context/entity/25/delivery' + ); + routerServiceStub.getState.and.returnValue(of(routeState)); + const navigateSpy = spyOn(service, 'navigate').and.returnValue(Promise.resolve(true)); + + // When + const promise = service.navigateBack(); + + // Then + expect(promise).toBeInstanceOf(Promise); + expect(routerServiceStub.getState).toHaveBeenCalled(); + promise.then((value) => { + expect(value).toBeTrue(); + expect(navigateSpy).toHaveBeenCalledWith( + '/domain/context/entity/25', + { + queryParamsHandling: 'preserve' + } + ); + + done(); + }); + }); + + it('should verify will invoke correct methods when no NavigationAction provided', (done) => { + // Given + const routeState = RouteState.of( + RouteSegments.of( + 'delivery', + {}, + {}, + { search: 'team-test' }, + RouteSegments.of( + 'entity/25', + {}, + { entityId: 25 }, + { search: 'team-test' }, + RouteSegments.of( + 'domain/context', + {}, + {}, + { search: 'team-test' }, + null, + 'domain/context' + ), + 'entity/:entityId' + ), + 'delivery' + ), + 'domain/context/entity/25/delivery' + ); + routerServiceStub.getState.and.returnValue(of(routeState)); + const navigateSpy = spyOn(service, 'navigate').and.returnValue(Promise.resolve(true)); + + // When + const promise = service.navigateBack(); + + // Then + expect(promise).toBeInstanceOf(Promise); + expect(routerServiceStub.getState).toHaveBeenCalled(); + promise.then((value) => { + expect(value).toBeTrue(); + expect(navigateSpy).toHaveBeenCalledWith( + '/domain/context/entity/25', + { + queryParamsHandling: 'merge' + } + ); + + done(); + }); + }); + }); + + describe('|navigateTo|', () => { + it('should verify will invoke correct methods preserve routerState QueryParams', (done) => { + // Given + const routeState = RouteState.of( + RouteSegments.of( + 'delivery', + { + navigateBack: { + path: '$.parent', + queryParamsHandling: 'preserve', + queryParams: { randomParam: 'abc' } + } + } as TaurusRouteData, + {}, + { search: 'team-test' }, + RouteSegments.of( + 'entity/25', + { + navigateTo: { + path: '/domain/context/entity/{0}/update/information', + replacers: [ + { searchValue: '{0}', replaceValue: '$.entity' } + ], + queryParamsHandling: 'preserve', + queryParams: { showWizard: true } + } + } as TaurusRouteData, + { entityId: 25 }, + { search: 'team-test' }, + RouteSegments.of( + 'domain/context', + {}, + {}, + { search: 'team-test' }, + null, + 'domain/context' + ), + 'entity/:entityId' + ), + 'delivery' + ), + 'domain/context/entity/25/delivery' + ); + routerServiceStub.getState.and.returnValue(of(routeState)); + const navigateSpy = spyOn(service, 'navigate').and.returnValue(Promise.resolve(true)); + + // When + const promise = service.navigateTo({ '$.entity': '101' }); + + // Then + expect(promise).toBeInstanceOf(Promise); + expect(routerServiceStub.getState).toHaveBeenCalled(); + promise.then((value) => { + expect(value).toBeTrue(); + expect(navigateSpy).toHaveBeenCalledWith( + '/domain/context/entity/101/update/information', + { + queryParamsHandling: 'preserve' + } + ); + + done(); + }); + }); + + it('should verify will invoke correct methods merge QueryParams, provided and from routeState', (done) => { + // Given + const routeState = RouteState.of( + RouteSegments.of( + 'delivery', + { + navigateBack: { + path: '$.parent', + queryParamsHandling: 'preserve', + queryParams: { randomParam: 'abc' } + } + } as TaurusRouteData, + {}, + { search: 'team-test' }, + RouteSegments.of( + 'entity/25', + { + navigateTo: { + path: '/domain/context/entity/{0}/update/information', + replacers: [ + { searchValue: '{0}', replaceValue: '$.entity' } + ], + queryParams: { showWizard: true }, + queryParamsHandling: 'merge' + } + } as TaurusRouteData, + { entityId: 25 }, + { search: 'team-test' }, + RouteSegments.of( + 'domain/context', + {}, + {}, + { search: 'team-test' }, + null, + 'domain/context' + ), + 'entity/:entityId' + ), + 'delivery' + ), + 'domain/context/entity/25/delivery' + ); + routerServiceStub.getState.and.returnValue(of(routeState)); + const navigateSpy = spyOn(service, 'navigate').and.returnValue(Promise.resolve(true)); + + // When + const promise = service.navigateTo({ '$.entity': '101' }); + + // Then + expect(promise).toBeInstanceOf(Promise); + expect(routerServiceStub.getState).toHaveBeenCalled(); + promise.then((value) => { + expect(value).toBeTrue(); + expect(navigateSpy).toHaveBeenCalledWith( + '/domain/context/entity/101/update/information', + { + queryParamsHandling: 'merge', + queryParams: { search: 'team-test', showWizard: true } + } + ); + + done(); + }); + }); + + it('should verify will reject Promise when no NavigationAction is provided', (done) => { + // Given + const routeState = RouteState.of( + RouteSegments.of( + 'delivery', + {}, + {}, + { search: 'team-test' }, + null, + 'delivery' + ), + 'domain/context/entity/25/delivery' + ); + routerServiceStub.getState.and.returnValue(of(routeState)); + const navigateSpy = spyOn(service, 'navigate').and.returnValue(Promise.resolve(true)); + const consoleErrorSpy = spyOn(console, 'error').and.callThrough(); + + // When + const promise = service.navigateTo(); + + // Then + expect(promise).toBeInstanceOf(Promise); + expect(routerServiceStub.getState).toHaveBeenCalled(); + promise.then((value) => { + expect(value).toBeFalse(); + expect(navigateSpy).not.toHaveBeenCalled(); + expect(consoleErrorSpy).toHaveBeenCalled(); + + done(); + }); + }); + }); + }); +}); diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/navigation/services/navigation.service.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/navigation/services/navigation.service.ts new file mode 100644 index 0000000000..e7819df918 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/navigation/services/navigation.service.ts @@ -0,0 +1,349 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Injectable } from '@angular/core'; +import { ActivatedRoute, ActivatedRouteSnapshot, NavigationExtras, Router } from '@angular/router'; + +import { take } from 'rxjs/operators'; + +import { ArrayElement, CollectionsUtil } from '../../../utils'; + +import { Replacer, TaurusNavigateAction } from '../../../common'; + +import { SE_NAVIGATE, SystemEventHandler, SystemEventHandlerClass } from '../../system-events'; + +import { RouterService, RouteState } from '../../router'; +import { RouteStateFactory } from '../../router/factory'; + +/** + * ** Service should be provided from the Root injector in Application. + * + * + */ +@Injectable() +@SystemEventHandlerClass() +export class NavigationService { + private readonly _routeStateFactory: RouteStateFactory; + + /** + * ** Constructor. + */ + constructor(private readonly router: Router, + private readonly routerService: RouterService) { + this._routeStateFactory = new RouteStateFactory(); + } + + /** + * ** Intercept SE_NAVIGATE Event and handle (react) on it. + */ + @SystemEventHandler(SE_NAVIGATE) + _navigationSystemEventHandler_(payload: { url: string | string[]; extras?: NavigationExtras }): Promise { + if (CollectionsUtil.isNil(payload)) { + return Promise.resolve(false); + } + + return this.navigate(payload.url, payload.extras); + } + + /** + * ** Navigate to url with provided extras. + */ + navigate(url: string | string[], extras?: NavigationExtras): Promise { + let urlChunks: string[]; + + if (CollectionsUtil.isArray(url)) { + urlChunks = url + .map((v, i) => + i === 0 + ? `/${ v.replace(/^\//, '') }` + : v + ); + } else if (CollectionsUtil.isString(url)) { + if (url.trim() === '/') { + urlChunks = ['/']; + } else { + urlChunks = url + .split('/') + .filter((v) => !!v) + .map((v, i) => + i === 0 + ? `/${ v }` + : v + ); + } + } else { + return Promise.resolve(false); + } + + return CollectionsUtil.isLiteralObject(extras) + ? this.router.navigate(urlChunks, extras) + : this.router.navigate(urlChunks); + } + + /** + * ** Navigate to using Route config, RouteState and leveraging provided optional replaceValueResolver. + * + * @param replaceValues is Object with mapping between Replacer.replaceValue key pointer to dynamic value specific for the Page. + * - What does it mean? + * Replacer has searchValue and replaceValue. When searchValue match, it replace with replaceValue, but + * replaceValue could be something like '$.team', '$.job', '{0}', '{1}' etc... dynamic params, so those params + * could be key in this Map to construct correct url depending of the logic in RouteConfig. + *

+ * Important!!! + * - If replaceValues is not provided or some key doesn't exist there, replaceValue would be use as it is in the RouteConfig. + *

+ *

+ * @param activatedRoute is optional and could be provided if you want those state to be used to extract RouteConfig and NavigationAction. + */ + navigateTo(replaceValues?: { [key: string]: ArrayElement['replaceValue'] }, + activatedRoute?: ActivatedRoute | ActivatedRouteSnapshot): Promise { + return this._navigateWithAction(replaceValues, activatedRoute, 'navigateTo'); + } + + /** + * ** Navigate back using Route config, RouteState and leveraging provided optional replaceValueResolver. + * + * @param replaceValues is Object with mapping between Replacer.replaceValue key pointer to dynamic value specific for the Page. + * - What does it mean? + * Replacer has searchValue and replaceValue. When searchValue match, it replace with replaceValue, but + * replaceValue could be something like '$.team', '$.job', '{0}', '{1}' etc... dynamic params, so those params + * could be key in this Map to construct correct url depending of the logic in RouteConfig. + *

+ * Important!!! + * - If replaceValues is not provided or some key doesn't exist there, replaceValue would be use as it is in the RouteConfig. + *

+ *

+ * @param activatedRoute is optional and could be provided if you want those state to be used to extract RouteConfig and NavigationAction. + */ + navigateBack(replaceValues?: { [key: string]: ArrayElement['replaceValue'] }, + activatedRoute?: ActivatedRoute | ActivatedRouteSnapshot): Promise { + return this._navigateWithAction(replaceValues, activatedRoute, 'navigateBack'); + } + + /** + * ** Redirect using Route config, RouteState and leveraging provided optional replaceValueResolver. + * + * @param replaceValues is Object with mapping between Replacer.replaceValue key pointer to dynamic value specific for the Page. + * - What does it mean? + * Replacer has searchValue and replaceValue. When searchValue match, it replace with replaceValue, but + * replaceValue could be something like '$.team', '$.job', '{0}', '{1}' etc... dynamic params, so those params + * could be key in this Map to construct correct url depending of the logic in RouteConfig. + *

+ * Important!!! + * - If replaceValues is not provided or some key doesn't exist there, replaceValue would be use as it is in the RouteConfig. + *

+ *

+ * @param activatedRoute is optional and could be provided if you want those state to be used to extract RouteConfig and NavigationAction. + */ + redirect(replaceValues?: { [key: string]: ArrayElement['replaceValue'] }, + activatedRoute?: ActivatedRoute | ActivatedRouteSnapshot): Promise { + return this._navigateWithAction(replaceValues, activatedRoute, 'redirect') + } + + /** + * ** Resolve NavigationAction using Route config, RouteState and leveraging provided optional replaceValueResolver. + * @param navigateActionType is the type of the NavigationAction we want to resolve from Route config. + * @param replaceValues is Object with mapping between Replacer.replaceValue key pointer to dynamic value specific for the Page. + * - What does it mean? + * Replacer has searchValue and replaceValue. When searchValue match, it replace with replaceValue, but + * replaceValue could be something like '$.team', '$.job', '{0}', '{1}' etc... dynamic params, so those params + * could be key in this Map to construct correct url depending of the logic in RouteConfig. + *

+ * Important!!! + * - If replaceValues is not provided or some key doesn't exist there, replaceValue would be use as it is in the RouteConfig. + *

+ *

+ * @param activatedRoute is optional and could be provided if you want those state to be used to extract RouteConfig and NavigationAction. + */ + resolveNavigateActionUrl(navigateActionType: 'navigateTo' | 'navigateBack' | 'redirect', + replaceValues?: { [key: string]: ArrayElement['replaceValue'] }, + activatedRoute?: ActivatedRoute | ActivatedRouteSnapshot | RouteState): Promise<[string, NavigationExtras]> { + + const _replaceValues = CollectionsUtil.isLiteralObject(replaceValues) + ? replaceValues + : {}; + + return this + ._extractRouteState(activatedRoute) + .then((state) => this._constructNavigateParameters(state, navigateActionType)) + .then((params) => + this._resolveNavigateValues( + params.url, + params.replacers, + _replaceValues, + params.navigationExtras + ) + ); + } + + /** + * ** Initialize the Service without any operation, just to create singleton Service instance. + * + * - It should be done in the root of the project only once. + * - Possible place is AppComponent or some root initializer guard. + */ + initialize(): void { + this.routerService.initialize(); + } + + private _navigateWithAction( + replaceValues: { [key: string]: ArrayElement['replaceValue'] }, + activatedRoute: ActivatedRoute | ActivatedRouteSnapshot, + dataKey: 'navigateTo' | 'navigateBack' | 'redirect'): Promise { + + return this.resolveNavigateActionUrl(dataKey, replaceValues, activatedRoute) + .then() + .then(([url, navigationExtras]) => this.navigate(url, navigationExtras)) + .catch((error) => { + console.error(error); + + return false; + }); + } + + private _extractRouteState(activatedRoute: ActivatedRoute | ActivatedRouteSnapshot | RouteState): Promise { + // NOTICE + // this piece of code is needed for Guards cases usage. + // Use params and data from Guard RouteState instead from RouterService because + // RouterService state is populated after all guards are resolved + if (CollectionsUtil.isDefined(activatedRoute)) { + if (activatedRoute instanceof RouteState) { + return Promise.resolve(activatedRoute); + } + + return Promise.resolve( + this._routeStateFactory + .create( + activatedRoute instanceof ActivatedRoute + ? activatedRoute.snapshot + : activatedRoute, + null + ) + ); + } + + return this.routerService + .getState() + .pipe( + take(1) + ) + .toPromise() + .then((state) => state); + } + + // eslint-disable-next-line max-len + private _constructNavigateParameters(state: RouteState, dataKey: 'navigateTo' | 'navigateBack' | 'redirect'): Promise { + const navigateAction = state.getData(dataKey); + const navigationExtras: NavigationExtras = { queryParamsHandling: 'merge' }; + + let url: string; + let replacers: Array> = []; + + if (CollectionsUtil.isDefined(navigateAction)) { + url = this._resolveNavigateUrl(state, dataKey, navigateAction); + + if (CollectionsUtil.isArray(navigateAction.replacers)) { + replacers = navigateAction.replacers; + } + + this._appendNavigateExtras(state, navigationExtras, navigateAction); + } else if (dataKey === 'navigateBack') { + url = state.getParentAbsoluteRoutePath(); + } else { + return Promise.reject('Cannot navigate without NavigationAction'); + } + + return Promise.resolve({ + url, + replacers, + navigationExtras + }); + } + + private _resolveNavigateUrl(state: RouteState, + dataKey: 'navigateTo' | 'navigateBack' | 'redirect', + navigateAction: TaurusNavigateAction): string { + + if (dataKey === 'redirect' || dataKey === 'navigateBack') { + if (navigateAction.path === '$.parent' || navigateAction.path === '$.requested') { + return navigateAction.useConfigPath + ? state.getParentAbsoluteConfigPath() + : state.getParentAbsoluteRoutePath(); + } + + if (navigateAction.path === '$.current') { + return navigateAction.useConfigPath + ? state.getAbsoluteConfigPath() + : state.getAbsoluteRoutePath(); + } + + return navigateAction.path; + } + + if (navigateAction.path === '$.current') { + return navigateAction.useConfigPath + ? state.getAbsoluteConfigPath() + : state.getAbsoluteRoutePath(); + } + + return navigateAction.path; + } + + // eslint-disable-next-line max-len + private _appendNavigateExtras(state: RouteState, navigationExtras: NavigationExtras, navigateAction: TaurusNavigateAction): void { + if (CollectionsUtil.isString(navigateAction.queryParamsHandling)) { + navigationExtras.queryParamsHandling = navigateAction.queryParamsHandling; + + if (navigationExtras.queryParamsHandling === 'merge') { + navigationExtras.queryParams = { + ...state.queryParams, + ...(navigateAction.queryParams ?? {}) + }; + } else if (navigationExtras.queryParamsHandling !== 'preserve') { + navigationExtras.queryParams = navigateAction.queryParams ?? {}; + } + } else if (CollectionsUtil.isNull(navigateAction.queryParamsHandling)) { + delete navigationExtras.queryParamsHandling; + } else { + navigationExtras.queryParams = { + ...state.queryParams, + ...(navigateAction.queryParams ?? {}) + }; + } + } + + private _resolveNavigateValues( + path: string, + replacers: Array>, + replaceValues: { [key: string]: ArrayElement['replaceValue'] }, + navigationExtras: NavigationExtras): Promise<[string, NavigationExtras]> { + + let resolvedPath: string; + + if (CollectionsUtil.isString(path)) { + resolvedPath = path; + } else { + resolvedPath = '/'; + + console.error(`RouteConfig error! NavigationAction config, "path" property is missing.`); + } + + for (const replacer of replacers) { + const replaceValue = replaceValues[replacer.replaceValue] ?? replacer.replaceValue; + + resolvedPath = resolvedPath.replace(replacer.searchValue, replaceValue); + } + + return Promise.resolve([resolvedPath, navigationExtras]); + } +} + +interface NavigationParameters { + url: string; + replacers: Array>; + navigationExtras: NavigationExtras; +} diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/actions/base.actions.spec.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/actions/base.actions.spec.ts new file mode 100644 index 0000000000..32a7c9d502 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/actions/base.actions.spec.ts @@ -0,0 +1,83 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { CollectionsUtil } from '../../../utils'; + +import { BaseAction, BaseActionWithPayload, GenericAction } from './base.actions'; + +describe('BaseActionWithPayload', () => { + describe('Statics::', () => { + describe('Methods::', () => { + describe('|of|', () => { + it('should verify will throw Error', () => { + // Then + expect(() => BaseActionWithPayload.of(null, null)) + .toThrowError('Method have to be overridden in Subclasses.'); + }); + }); + }); + }); +}); + +describe('GenericAction', () => { + it('should verify instance is created', () => { + // When + const instance = new GenericAction(null, null); + + // Then + expect(instance).toBeDefined(); + }); + + describe('Statics::', () => { + describe('Methods::', () => { + describe('|of|', () => { + it('should verify factory method will create instance', () => { + // Given + const type = '[component] Create State'; + const payload = { state: [1, 2, 3, 4] }; + + // When + const instance = GenericAction.of(type, payload); + + // Then + expect(instance).toBeInstanceOf(GenericAction); + expect(instance).toBeInstanceOf(BaseActionWithPayload); + expect(instance).toBeInstanceOf(BaseAction); + + expect(instance.type).toEqual(type); + expect(instance.payload).toBe(payload); + }); + + it('should verify factory method will create instance with Task', () => { + // Given + const type = '[component] Submit Data'; + const payload = { state: [10, 20, 30, 40] }; + const task = 'patch_entity'; + const dateNowISO = new Date().toISOString(); + const taskIdentifier = `${ task } __ ${ dateNowISO }`; + const dateISOSpy = spyOn(CollectionsUtil, 'dateISO').and.returnValue(dateNowISO); + const interpolateStringSpy = spyOn(CollectionsUtil, 'interpolateString').and.returnValue(taskIdentifier); + + // When + const instance = GenericAction.of(type, payload, task); + + // Then + expect(instance).toBeInstanceOf(GenericAction); + expect(instance).toBeInstanceOf(BaseActionWithPayload); + expect(instance).toBeInstanceOf(BaseAction); + + expect(instance.type).toEqual(type); + expect(instance.payload).toBe(payload); + expect(instance.task).toEqual(taskIdentifier); + expect(dateISOSpy).toHaveBeenCalled(); + // @ts-ignore + expect(interpolateStringSpy).toHaveBeenCalledWith('%s __ %s', task, dateNowISO); + }); + }); + }); + }); +}); diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/actions/base.actions.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/actions/base.actions.ts new file mode 100644 index 0000000000..8ff0c5e217 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/actions/base.actions.ts @@ -0,0 +1,91 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Action } from '@ngrx/store'; + +import { createTaskIdentifier } from '../../../common'; + +/** + * ** Base Action for Redux Impl. + * + * + */ +export abstract class BaseAction implements Action { + /** + * ** Type of Action. + */ + readonly type: string; + + /** + * ** Constructor. + */ + protected constructor(type: string) { + this.type = type; + } +} + +/** + * ** Base Action with payload for Redux Impl. + * + * + */ +export abstract class BaseActionWithPayload extends BaseAction { + + /** + * ** Action payload data. + */ + readonly payload: T; + + /** + * ** Action Task. + */ + readonly task: string; + + /** + * ** Constructor. + */ + protected constructor(type: string, + payload: T, + task?: string) { + super(type); + + this.payload = payload; + this.task = createTaskIdentifier(task); + } + + /** + * ** Factory method that have to be overridden in Subclasses. + */ + static of(..._args: unknown[]): BaseActionWithPayload { + throw new Error('Method have to be overridden in Subclasses.'); + } +} + +/** + * ** Generic Action with payload for Redux Impl. + * + * - All parameters type, payload and optional task are provided to Constructor. + * + * + */ +export class GenericAction extends BaseActionWithPayload { + /** + * ** Constructor. + */ + constructor(type: string, + payload: T, + task?: string) { + super(type, payload, task); + } + + /** + * ** Factory method for Generic Action with type and payload in Constructor. + */ + static override of(type: string, payload: K, task?: string): GenericAction { + return new GenericAction(type, payload, task); + } +} diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/actions/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/actions/index.ts new file mode 100644 index 0000000000..6f669d9c61 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/actions/index.ts @@ -0,0 +1,8 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './base.actions'; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/config/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/config/index.ts new file mode 100644 index 0000000000..9dcd8f6725 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/config/index.ts @@ -0,0 +1,8 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './ngrx-config.model'; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/config/ngrx-config.model.spec.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/config/ngrx-config.model.spec.ts new file mode 100644 index 0000000000..fa9dd5d0f5 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/config/ngrx-config.model.spec.ts @@ -0,0 +1,32 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { NGRX_STORE_CONFIG, NGRX_STORE_DEVTOOLS_CONFIG } from './ngrx-config.model'; + +describe('NGRX_STORE_DEVTOOLS_CONFIG', () => { + it('should verify default values are set', () => { + // Then + expect(NGRX_STORE_DEVTOOLS_CONFIG).toEqual({ + maxAge: 100, + serialize: true, + logOnly: false, + name: 'Taurus NgRx Store' + }); + }); +}); + +describe('NGRX_STORE_CONFIG', () => { + it('should verify default values are set', () => { + // Then + expect(NGRX_STORE_CONFIG).toEqual({ + runtimeChecks: { + strictActionImmutability: true, + strictStateImmutability: true + } + }); + }); +}); diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/config/ngrx-config.model.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/config/ngrx-config.model.ts new file mode 100644 index 0000000000..635e4a46c1 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/config/ngrx-config.model.ts @@ -0,0 +1,48 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { RootStoreConfig } from '@ngrx/store'; +import { StoreDevtoolsConfig } from '@ngrx/store-devtools'; + +/** + * ** Configuration for NgRx Store Devtools. + * + * + */ +export const NGRX_STORE_DEVTOOLS_CONFIG: StoreDevtoolsConfig = { + maxAge: 100, + serialize: true, + logOnly: false, + name: 'Taurus NgRx Store' +}; + + +/** + * ** Configuration for NgRx Store. + * + * + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const NGRX_STORE_CONFIG: RootStoreConfig = { + runtimeChecks: { + strictActionImmutability: true, + strictStateImmutability: true + } +}; + +/** + * ** Config for Taurus impl of NgRx. + * + * + */ +export interface TaurusNgRxConfig { + /** + * ** StoreDevTools configuration replica. + *

see {@link https://ngrx.io/guide/store-devtools/config}

+ */ + storeDevToolsConfig?: StoreDevtoolsConfig; +} diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/effects/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/effects/index.ts new file mode 100644 index 0000000000..2fcf8e79a2 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/effects/index.ts @@ -0,0 +1,8 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './root-effects.registry'; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/effects/root-effects.registry.spec.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/effects/root-effects.registry.spec.ts new file mode 100644 index 0000000000..97b0369487 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/effects/root-effects.registry.spec.ts @@ -0,0 +1,17 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { RouterEffects } from '../../router/state/effects'; + +import { SHARED_ROOT_EFFECTS } from './root-effects.registry'; + +describe('SHARED_ROOT_EFFECTS', () => { + it('should verify expected Effects are in this registry', () => { + // Then + expect(SHARED_ROOT_EFFECTS).toEqual([RouterEffects]); + }); +}); diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/effects/root-effects.registry.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/effects/root-effects.registry.ts new file mode 100644 index 0000000000..3c4e25d4db --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/effects/root-effects.registry.ts @@ -0,0 +1,20 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Type } from '@angular/core'; + +import { RouterEffects } from '../../router/state/effects'; + +/** + * ** Registry for Root Effects. + * + * + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const SHARED_ROOT_EFFECTS: Array> = [ + RouterEffects +]; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/helper-modules/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/helper-modules/index.ts new file mode 100644 index 0000000000..8f1199e1f4 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/helper-modules/index.ts @@ -0,0 +1,9 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './taurus-shared-ngrx-dev.module'; +export * from './taurus-shared-ngrx-prod.module'; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/helper-modules/taurus-shared-ngrx-dev.module.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/helper-modules/taurus-shared-ngrx-dev.module.ts new file mode 100644 index 0000000000..f19b4f4670 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/helper-modules/taurus-shared-ngrx-dev.module.ts @@ -0,0 +1,65 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { ModuleWithProviders, NgModule } from '@angular/core'; + +import { StoreModule } from '@ngrx/store'; +import { StoreRouterConnectingModule } from '@ngrx/router-store'; +import { EffectsModule } from '@ngrx/effects'; +import { StoreDevtoolsConfig, StoreDevtoolsModule } from '@ngrx/store-devtools'; + +import { RouterService, RouterServiceImpl } from '../../router'; +import { SharedRouterSerializer } from '../../router/integration/ngrx'; + +import { ComponentService, ComponentServiceImpl } from '../../component'; + +import { SHARED_ROOT_REDUCERS } from '../reducers'; +import { SHARED_ROOT_EFFECTS } from '../effects'; +import { NGRX_STORE_CONFIG, NGRX_STORE_DEVTOOLS_CONFIG } from '../config'; +import { STORE_ROUTER } from '../state'; + +/** + * ** Taurus NgRx Redux module recommended for use in Development builds. + * + * + */ +@NgModule({ + imports: [ + StoreModule.forRoot(SHARED_ROOT_REDUCERS, NGRX_STORE_CONFIG), + EffectsModule.forRoot(SHARED_ROOT_EFFECTS), + StoreDevtoolsModule.instrument(() => TaurusSharedNgrxDevModule.storeDevToolsConfig), + StoreRouterConnectingModule.forRoot({ + stateKey: STORE_ROUTER, + serializer: SharedRouterSerializer + }) + ], + exports: [ + StoreModule, + EffectsModule, + StoreDevtoolsModule, + StoreRouterConnectingModule + ] +}) +export class TaurusSharedNgrxDevModule { + private static storeDevToolsConfig: StoreDevtoolsConfig = NGRX_STORE_DEVTOOLS_CONFIG; + + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-explicit-any + static forRoot(config: StoreDevtoolsConfig = {} as any): ModuleWithProviders { + TaurusSharedNgrxDevModule.storeDevToolsConfig = { + ...NGRX_STORE_DEVTOOLS_CONFIG, + ...config + }; + + return { + ngModule: TaurusSharedNgrxDevModule, + providers: [ + { provide: RouterService, useClass: RouterServiceImpl }, + { provide: ComponentService, useClass: ComponentServiceImpl } + ] + }; + } +} diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/helper-modules/taurus-shared-ngrx-prod.module.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/helper-modules/taurus-shared-ngrx-prod.module.ts new file mode 100644 index 0000000000..5bf0570272 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/helper-modules/taurus-shared-ngrx-prod.module.ts @@ -0,0 +1,54 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { ModuleWithProviders, NgModule } from '@angular/core'; + +import { StoreModule } from '@ngrx/store'; +import { StoreRouterConnectingModule } from '@ngrx/router-store'; +import { EffectsModule } from '@ngrx/effects'; + +import { RouterService, RouterServiceImpl } from '../../router'; +import { SharedRouterSerializer } from '../../router/integration/ngrx'; + +import { ComponentService, ComponentServiceImpl } from '../../component'; + +import { SHARED_ROOT_REDUCERS } from '../reducers'; +import { SHARED_ROOT_EFFECTS } from '../effects'; +import { NGRX_STORE_CONFIG } from '../config'; +import { STORE_ROUTER } from '../state'; + +/** + * ** Taurus NgRx Redux module recommended for use in Production builds. + * + * + */ +@NgModule({ + imports: [ + StoreModule.forRoot(SHARED_ROOT_REDUCERS, NGRX_STORE_CONFIG), + EffectsModule.forRoot(SHARED_ROOT_EFFECTS), + StoreRouterConnectingModule.forRoot({ + stateKey: STORE_ROUTER, + serializer: SharedRouterSerializer + }) + ], + exports: [ + StoreModule, + EffectsModule, + StoreRouterConnectingModule + ] +}) +export class TaurusSharedNgrxProdModule { + static forRoot(): ModuleWithProviders { + return { + ngModule: TaurusSharedNgrxProdModule, + providers: [ + { provide: RouterService, useClass: RouterServiceImpl }, + { provide: ComponentService, useClass: ComponentServiceImpl } + ] + }; + } +} diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/index.ts new file mode 100644 index 0000000000..8acf801157 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/index.ts @@ -0,0 +1,9 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './actions'; +export * from './state'; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/public-api.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/public-api.ts new file mode 100644 index 0000000000..ba142389bd --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/public-api.ts @@ -0,0 +1,8 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './index'; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/reducers/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/reducers/index.ts new file mode 100644 index 0000000000..681b00b6bc --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/reducers/index.ts @@ -0,0 +1,8 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './shared-root-reducers.registry'; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/reducers/shared-root-reducers.registry.spec.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/reducers/shared-root-reducers.registry.spec.ts new file mode 100644 index 0000000000..2cdc95b5b9 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/reducers/shared-root-reducers.registry.spec.ts @@ -0,0 +1,24 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { routerReducer } from '../../router/state/reducers'; + +import { componentReducer } from '../../component'; + +import { STORE_COMPONENTS, STORE_ROUTER } from '../state'; + +import { SHARED_ROOT_REDUCERS } from './shared-root-reducers.registry'; + +describe('SHARED_ROOT_REDUCERS', () => { + it('should verify root reducers are registered', () => { + // Then + expect(SHARED_ROOT_REDUCERS).toEqual({ + [STORE_ROUTER]: routerReducer, + [STORE_COMPONENTS]: componentReducer + }); + }); +}); diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/reducers/shared-root-reducers.registry.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/reducers/shared-root-reducers.registry.ts new file mode 100644 index 0000000000..4e0291d602 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/reducers/shared-root-reducers.registry.ts @@ -0,0 +1,23 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { ActionReducerMap } from '@ngrx/store'; + +import { STORE_COMPONENTS, STORE_ROUTER, StoreState } from '../state'; + +import { routerReducer } from '../../router/state/reducers'; +import { componentReducer } from '../../component/state'; + +/** + * ** Root reducers for Shared. + * + * + */ +export const SHARED_ROOT_REDUCERS: ActionReducerMap = { + [STORE_ROUTER]: routerReducer, + [STORE_COMPONENTS]: componentReducer +}; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/state/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/state/index.ts new file mode 100644 index 0000000000..d83955450f --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/state/index.ts @@ -0,0 +1,8 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './store-state.model'; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/state/store-state.model.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/state/store-state.model.ts new file mode 100644 index 0000000000..1356c3af04 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/state/store-state.model.ts @@ -0,0 +1,29 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { RouterState } from '../../router'; +import { LiteralComponentsState } from '../../component'; + +/** + * ** Constant for Store Router field. + */ +export const STORE_ROUTER = 'router'; + +/** + * ** Constant for Store Components field. + */ +export const STORE_COMPONENTS = 'components'; + +/** + * ** Store State interface. + * + * + */ +export interface StoreState { + [STORE_ROUTER]: RouterState; + [STORE_COMPONENTS]: LiteralComponentsState; +} diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/taurus-shared-ngrx.module.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/taurus-shared-ngrx.module.ts new file mode 100644 index 0000000000..2362ffd93b --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/ngrx/taurus-shared-ngrx.module.ts @@ -0,0 +1,81 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +/* eslint-disable @typescript-eslint/unified-signatures */ + +import { InjectionToken, ModuleWithProviders, NgModule, Type } from '@angular/core'; + +import { Action, ActionReducer, ActionReducerMap, StoreConfig, StoreFeatureModule, StoreModule } from '@ngrx/store'; +import { EffectsFeatureModule, EffectsModule } from '@ngrx/effects'; + +import { TaurusNgRxConfig } from './config'; +import { TaurusSharedNgrxProdModule, TaurusSharedNgrxDevModule } from './helper-modules'; + +/** + * ** Integration Class module for NgRx Redux. + * + * + */ +@NgModule({}) +export class TaurusSharedNgRxModule { + /** + * ** Provides TaurusSharedNgrxProdModule and all Services related to Taurus Redux. + * + * - Recommended for Prod (release) builds. + * - Should be invoke at Root and only once for entire project. + * - In FeaturesModules (lazy loaded Modules) invoke one of the methods forFeatureEffects/forFeatureStore. + */ + static forRoot(): ModuleWithProviders { + return TaurusSharedNgrxProdModule.forRoot(); + } + + /** + * ** Provides TaurusSharedNgrxDevModule including StoreDevTools and all Services related to Taurus Redux. + * + * - Recommended for Dev (local) builds. + * - Should be invoke at Root and only once for entire project. + * - In FeaturesModules (lazy loaded Modules) invoke one of the methods forFeatureEffects/forFeatureStore. + */ + static forRootWithDevtools(config: TaurusNgRxConfig = {}): ModuleWithProviders { + return TaurusSharedNgrxDevModule.forRoot(config.storeDevToolsConfig); + } + + /** + * ** Load Features Effects. + * + * - Should be invoke in FeatureModules (lazy loaded Modules). + * - It will register Effects for that Feature. + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + static forFeatureEffects(effects: Array>): ModuleWithProviders { + return EffectsModule.forFeature(effects); + } + + /** + * ** Load Features Store reducers. + * + * - Should be invoke in FeatureModules (lazy loaded Modules). + * - It will extend Store and add reducers for that Feature. + */ + static forFeatureStore( + featureName: string, + reducers: ActionReducerMap | InjectionToken>, + config?: StoreConfig | InjectionToken>): ModuleWithProviders; + static forFeatureStore( + featureName: string, + reducer: ActionReducer | InjectionToken>, + config?: StoreConfig | InjectionToken>): ModuleWithProviders; + static forFeatureStore( + featureName: string, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + reducer: any, + config?: StoreConfig | InjectionToken>): ModuleWithProviders { + + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + return StoreModule.forFeature(featureName, reducer, config); + } +} diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/public-api.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/public-api.ts new file mode 100644 index 0000000000..d97123f0dd --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/public-api.ts @@ -0,0 +1,13 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './component/public-api'; +export * from './navigation/public-api'; +export * from './ngrx/public-api'; +export * from './router/public-api'; +export * from './system-events/public-api'; +export * from './url-state-manager/public-api'; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/factory/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/factory/index.ts new file mode 100644 index 0000000000..c03c42df7f --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/factory/index.ts @@ -0,0 +1,8 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './route-state.factory'; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/factory/route-state.factory.spec.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/factory/route-state.factory.spec.ts new file mode 100644 index 0000000000..523ac14a70 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/factory/route-state.factory.spec.ts @@ -0,0 +1,54 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { createRouteSnapshot } from '../../../unit-testing'; + +import { RouteSegments, RouteState } from '../model'; + +import { RouteStateFactory } from './route-state.factory'; + +describe('RouteStateFactory', () => { + it('should verify instance is created', () => { + // When + const instance = new RouteStateFactory(); + + // Then + expect(instance).toBeDefined(); + }); + + describe('Methods::', () => { + describe('|create|', () => { + it('should verify will return RouteState from provided ActivatedRouteSnapshot', () => { + // Given + const routeSnapshot = createRouteSnapshot({}); + const factory = new RouteStateFactory(); + const assertion = RouteState.of( + new RouteSegments( + 'entity/23', + { paramKey: 'prime' }, + { entityId: 23 }, + { search: 'test-team' }, + new RouteSegments( + 'domain/context', + {}, + {}, + {}, + undefined, + 'domain/context'), + 'entity/23'), + 'domain/context/entity/23' + ); + + // When + const routeState = factory.create(routeSnapshot, 'domain/context/entity/23'); + + // Then + expect(routeState).toEqual(assertion); + }); + }); + }); +}); diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/factory/route-state.factory.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/factory/route-state.factory.ts new file mode 100644 index 0000000000..233ab1866d --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/factory/route-state.factory.ts @@ -0,0 +1,54 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { ActivatedRouteSnapshot, UrlSegment } from '@angular/router'; + +import { CollectionsUtil } from '../../../utils'; + +import { RouteSegments, RouteState } from '../model'; + +/** + * ** Route State Factory. + * + * + */ +export class RouteStateFactory { + /** + * ** Creates Router State from provided Route snapshot. + */ + create(routeSnapshot: ActivatedRouteSnapshot, url: string): RouteState { + return RouteState.of( + this._getRouteSegments(routeSnapshot), + url + ); + } + + private _getRouteSegments(routeSnapshot: ActivatedRouteSnapshot): RouteSegments { + if (CollectionsUtil.isNil(routeSnapshot)) { + return null; + } + + const routePathSegments: string[] = []; + routeSnapshot.url.forEach((segment: UrlSegment) => { + routePathSegments.push(segment.path); + }); + + const routePath = routePathSegments.join('/'); + const data = CollectionsUtil.cloneDeep(routeSnapshot.data); + const params = CollectionsUtil.cloneDeep(routeSnapshot.params); + const queryParams = CollectionsUtil.cloneDeep(routeSnapshot.queryParams); + const configPath = routeSnapshot.routeConfig?.path; + + let parentNavSegments: RouteSegments; + + if (routeSnapshot.parent) { + parentNavSegments = this._getRouteSegments(routeSnapshot.parent); + } + + return RouteSegments.of(routePath, data, params, queryParams, parentNavSegments, configPath); + } +} diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/index.ts new file mode 100644 index 0000000000..6ad912db02 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/index.ts @@ -0,0 +1,10 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './model'; +export * from './state/actions'; +export * from './services'; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/integration/ngrx/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/integration/ngrx/index.ts new file mode 100644 index 0000000000..497a8d8bcf --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/integration/ngrx/index.ts @@ -0,0 +1,8 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './router-serializer.service'; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/integration/ngrx/router-serializer.service.spec.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/integration/ngrx/router-serializer.service.spec.ts new file mode 100644 index 0000000000..00d6ff1b24 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/integration/ngrx/router-serializer.service.spec.ts @@ -0,0 +1,107 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { UrlSegment } from '@angular/router'; +import { TestBed } from '@angular/core/testing'; + +import { ComponentStub, createRouteSnapshot, RouteConfigStub } from '../../../../unit-testing'; + +import { RouteSegments, RouteState } from '../../model'; +import { RouteStateFactory } from '../../factory'; + +import { SharedRouterSerializer } from './router-serializer.service'; + +describe('SharedRouterSerializer', () => { + let service: SharedRouterSerializer; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + SharedRouterSerializer + ] + }); + + service = TestBed.inject(SharedRouterSerializer); + }); + + it('should verify instance is created', () => { + // Then + expect(service).toBeDefined(); + }); + + describe('Methods::', () => { + describe('|serialize|', () => { + it('should verify will invoke correct methods', () => { + // Given + const routeState = RouteState.of( + RouteSegments.of( + 'delivery', + { paramKey: 'prime' }, + { entityId: 25 }, + { search: 'test-team' }, + RouteSegments.of( + 'entity/25', + { deferredLoading: true }, + { entityId: 25 }, + { search: 'test-team' }, + RouteSegments.of( + 'domain/context', + { entityKey: 'second' }, + {}, + { search: 'test-team' }, + null + ) + ) + ), + 'domain/context/entity/25/delivery' + ); + const factoryCreateSpy = spyOn(RouteStateFactory.prototype, 'create').and.returnValue(routeState); + + const routeSnapshotChild2 = createRouteSnapshot({ + url: [new UrlSegment('domain/context', {}), new UrlSegment('entity/25', {}), new UrlSegment('delivery', {})], + data: { entityKey: 'second' }, + params: { entityId: 25 }, + queryParams: { search: 'team-test' }, + outlet: 'router-outlet', + component: ComponentStub, + routeConfig: new RouteConfigStub('delivery', ComponentStub, 'router-outlet') + }); + const routeSnapshotChild1 = createRouteSnapshot({ + url: [new UrlSegment('domain/context', {}), new UrlSegment('entity/25', {})], + data: { deferredLoading: true }, + params: { entityId: 25 }, + queryParams: { search: 'team-test' }, + outlet: 'router-outlet', + component: ComponentStub, + routeConfig: new RouteConfigStub('entity/25', ComponentStub, 'router-outlet'), + firstChild: routeSnapshotChild2 + }); + const rootRouteSnapshot = createRouteSnapshot({ + url: [new UrlSegment('domain/context', {})], + data: { paramKey: 'prime' }, + params: {}, + queryParams: { search: 'team-test' }, + outlet: 'router-outlet', + component: ComponentStub, + routeConfig: new RouteConfigStub('domain/context', ComponentStub, 'router-outlet'), + firstChild: routeSnapshotChild1 + }); + + // When + const result = service.serialize({ + url: 'domain/context/entity/25/delivery', + root: rootRouteSnapshot, + toString: () => '' + }); + + // Then + expect(factoryCreateSpy).toHaveBeenCalledWith(routeSnapshotChild2, 'domain/context/entity/25/delivery'); + expect(result).toBe(routeState); + }); + }); + }); +}); diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/integration/ngrx/router-serializer.service.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/integration/ngrx/router-serializer.service.ts new file mode 100644 index 0000000000..5006876b44 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/integration/ngrx/router-serializer.service.ts @@ -0,0 +1,44 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Injectable } from '@angular/core'; +import { RouterStateSnapshot } from '@angular/router'; + +import { RouterStateSerializer } from '@ngrx/router-store'; + +import { RouteState } from '../../model'; +import { RouteStateFactory } from '../../factory'; + +/** + * ** Shared Router Serializer implements NgRx RouterStateSerializer. + * + * + */ +@Injectable() +export class SharedRouterSerializer implements RouterStateSerializer { + private readonly _routeStateFactory: RouteStateFactory; + + /** + * ** Constructor. + */ + constructor() { + this._routeStateFactory = new RouteStateFactory(); + } + + /** + * @inheritDoc + */ + serialize(routerState: RouterStateSnapshot): RouteState { + let routeSnapshot = routerState.root; + + while (routeSnapshot.firstChild) { + routeSnapshot = routeSnapshot.firstChild; + } + + return this._routeStateFactory.create(routeSnapshot, routerState.url); + } +} diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/model/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/model/index.ts new file mode 100644 index 0000000000..db0bb6637c --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/model/index.ts @@ -0,0 +1,8 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './route.model'; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/model/route.model.spec.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/model/route.model.spec.ts new file mode 100644 index 0000000000..ac74027172 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/model/route.model.spec.ts @@ -0,0 +1,792 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { RouterState, RouteSegments, RouteState } from './route.model'; + +describe('Route Classes', () => { + describe('RouteSegments', () => { + it('should verify instance is created', () => { + // When + const instance = new RouteSegments( + 'domain/context', + {}, + {}, + {}, + null, + null); + + // Then + expect(instance).toBeDefined(); + }); + + describe('Statics::', () => { + describe('Methods::', () => { + describe('|of|', () => { + it('should verify factory method will create instance', () => { + // When + const instance = RouteSegments.of('domain/context', + {}, + {}, + null, + null); + + // Then + expect(instance).toBeInstanceOf(RouteSegments); + }); + }); + + describe('|empty|', () => { + it('should verify will create empty instance with default values', () => { + // When + const instance = RouteSegments.empty(); + + // Then + expect(instance).toBeInstanceOf(RouteSegments); + expect(instance.params).toEqual({}); + expect(instance.queryParams).toEqual({}); + expect(instance.routePath).toEqual(''); + }); + }); + }); + }); + + describe('Methods::', () => { + describe('|getData|', () => { + it('should verify will return param', () => { + // Given + const routeSegments = RouteSegments.of( + 'bill/105/view', + { paramKey: 'prime' }, + { billId: '105' }, + {}, + RouteSegments.of( + 'entity/17/explore', + { deferredLoad: true }, + { entityId: '17' }, + null, + RouteSegments.of( + 'domain/context', + null, + null, + null, + null, + 'domain/context' + ), + 'entity/:entityId/explore' + ), + 'bill/:billId/view' + ); + + // When + const param1 = routeSegments.getData('randomParam'); + const param2 = routeSegments.getData('paramKey'); + const param3 = routeSegments.getData('deferredLoad'); + + // Then + expect(param1).toBeUndefined(); + expect(param2).toEqual('prime'); + expect(param3).toEqual(true); + }); + }); + + describe('|getParam|', () => { + it('should verify will return param', () => { + // Given + const routeSegments = RouteSegments.of( + 'bill/105', + {}, + { billId: '105' }, + {}, + RouteSegments.of( + 'entity/17', + {}, + { entityId: '17' }, + {}, + null, + 'entity/:entityId' + ), + 'bill/:billId' + ); + + // When + const param1 = routeSegments.getParam('randomParam'); + const param2 = routeSegments.getParam('billId'); + const param3 = routeSegments.getParam('entityId'); + + // Then + expect(param1).toBeUndefined(); + expect(param2).toEqual('105'); + expect(param3).toEqual('17'); + }); + }); + + describe('|getQueryParam|', () => { + it('should verify will return queryParam', () => { + // Given + const routeSegments = RouteSegments.of( + 'bill/105', + {}, + { billId: '105' }, + { search: 'test-team', activeUser: 'aUser' }, + RouteSegments.of( + 'entity/17', + {}, + { entityId: '17' }, + { search: 'test-team', activeUser: 'aUser' }, + RouteSegments.of( + 'domain/context', + {}, + {}, + { search: 'test-team', activeUser: 'aUser' }, + null, + 'domain/context' + ), + 'entity/:entityId' + ), + 'bill/:billId' + ); + + // When + const queryParam1 = routeSegments.getQueryParam('entityId'); + const queryParam2 = routeSegments.getQueryParam('search'); + const queryParam3 = routeSegments.getQueryParam('activeUser'); + + // Then + expect(queryParam1).toBeUndefined(); + expect(queryParam2).toEqual('test-team'); + expect(queryParam3).toEqual('aUser'); + }); + }); + }); + + describe('Getters/Setters::', () => { + describe('|GET -> routePathSegments|', () => { + it('should verify will return routePathSegments', () => { + // Given + const routeSegments = RouteSegments.of( + 'bill/105/view', + {}, + null, + null, + RouteSegments.of( + 'entity/17/explore', + {}, + null, + null, + RouteSegments.of( + 'domain/context', + null, + null, + null, + null, + 'domain/context' + ), + 'entity/:entityId/explore' + ), + 'bill/:billId/view' + ); + + // When + const routePathSegments = routeSegments.routePathSegments; + + // Then + expect(routePathSegments).toEqual(['domain/context', 'entity/17/explore', 'bill/105/view']); + }); + }); + + describe('|GET -> configPathSegments|', () => { + it('should verify will return configPathSegments', () => { + // Given + const routeSegments = RouteSegments.of( + 'bill/105/view', + {}, + null, + null, + RouteSegments.of( + 'entity/17/explore', + {}, + null, + null, + RouteSegments.of( + 'domain/context', + null, + null, + null, + null, + 'domain/context' + ), + 'entity/:entityId/explore' + ), + 'bill/:billId/view' + ); + + // When + const configPathSegments = routeSegments.configPathSegments; + + // Then + expect(configPathSegments).toEqual(['domain/context', 'entity/:entityId/explore', 'bill/:billId/view']); + }); + }); + }); + }); + + describe('RouteState', () => { + let routeState: RouteState; + let routeSegments: RouteSegments; + + beforeEach(() => { + routeSegments = RouteSegments.of( + 'bill/105/view', + {}, + null, + { 'test_search*': '^test-team$', 'test_@active-user': '%aUser%' }, + RouteSegments.of( + 'entity/17/explore', + {}, + null, + null, + RouteSegments.of( + 'domain/context', + {}, + null, + null, + null, + 'domain/context' + ), + 'entity/:entityId/explore' + ), + 'bill/:billId/view' + ); + + routeState = RouteState.of( + routeSegments, + 'domain/context/entity/17/explore/bill/105/view' + ); + }); + + it('should verify instance is created', () => { + // When + const instance = new RouteState( + new RouteSegments( + 'domain/context', + {}, + {}, + null, + null), + 'domain/context' + ); + + // Then + expect(instance).toBeDefined(); + }); + + describe('Statics::', () => { + describe('Methods::', () => { + describe('|of|', () => { + it('should verify factory method will create instance', () => { + // When + const instance = RouteState.of( + new RouteSegments( + 'domain/context', + {}, + {}, + null, + null), + 'domain/context' + ); + + // Then + expect(instance).toBeInstanceOf(RouteState); + }); + }); + + describe('|empty|', () => { + it('should verify will create empty instance with default values', () => { + // When + const instance = RouteState.empty(); + + // Then + expect(instance).toBeInstanceOf(RouteState); + expect(instance.routeSegments).toEqual(RouteSegments.empty()); + expect(instance.url).toEqual(''); + }); + }); + }); + }); + + describe('Getters/Setters::', () => { + describe('|GET -> absoluteRoutePath|', () => { + it('should verify will return location', () => { + // When + const location = routeState.absoluteRoutePath; + + // Then + expect(location).toEqual('/domain/context/entity/17/explore/bill/105/view'); + }); + }); + + describe('|GET -> routePath|', () => { + it('should verify will return routePath', () => { + // When + const routePath = routeState.routePath; + + // Then + expect(routePath).toEqual('bill/105/view'); + }); + }); + + describe('|GET -> routePathSegments|', () => { + it('should verify will return routePathSegments', () => { + // When + const routePathSegments = routeState.routePathSegments; + + // Then + expect(routePathSegments).toEqual(['domain/context', 'entity/17/explore', 'bill/105/view']); + }); + }); + + describe('|GET -> configPath|', () => { + it('should verify will return configPath', () => { + // When + const configPath = routeState.configPath; + + // Then + expect(configPath).toEqual('bill/:billId/view'); + }); + }); + + describe('|GET -> absoluteConfigPath|', () => { + it('should verify will return absoluteConfigPath', () => { + // When + const absoluteConfigPath = routeState.absoluteConfigPath; + + // Then + expect(absoluteConfigPath).toEqual('/domain/context/entity/:entityId/explore/bill/:billId/view'); + }); + }); + + describe('|GET -> configPathSegments|', () => { + it('should verify will return configPathSegments', () => { + // When + const configPathSegments = routeState.configPathSegments; + + // Then + expect(configPathSegments).toEqual(['domain/context', 'entity/:entityId/explore', 'bill/:billId/view']); + }); + }); + + describe('|GET -> queryParams|', () => { + it('should verify will return queryParams', () => { + // When + const queryParams = routeState.queryParams; + + // Then + expect(queryParams).toBe(routeState.routeSegments.queryParams); + }); + }); + }); + + describe('Methods::', () => { + describe('|serializeQueryParams|', () => { + it('should verify will serialize queryParams in string', () => { + // When + const query = routeState.serializeQueryParams(); + + // Then + expect(query).toEqual('test_search*=%5Etest-team%24&test_%40active-user=%25aUser%25'); + }); + + it('should verify will return empty string when no queryParams', () => { + // Given + routeState = RouteState.of( + RouteSegments.of( + routeState.routeSegments.routePath, + routeState.routeSegments.data, + routeState.routeSegments.params, + {}, + routeState.routeSegments.parent, + routeState.routeSegments.configPath + ), + routeState.url + ); + + // When + const query = routeState.serializeQueryParams(); + + // Then + expect(query).toEqual(''); + }); + }); + + describe('|getUrl|', () => { + it('should verify will return correct value', () => { + // When + const url = routeState.getUrl(); + + // Then + expect(url).toEqual( + '/domain/context/entity/17/explore/bill/105/view?test_search*=%5Etest-team%24&test_%40active-user=%25aUser%25' + ); + }); + }); + + describe('|getData|', () => { + it('should verify will invoke correct methods', () => { + // Given + const getDataSpy = spyOn(RouteSegments.prototype, 'getData').and.returnValue('loading'); + + // When + const data = routeState.getData('primeKey'); + + // Then + expect(getDataSpy).toHaveBeenCalledWith('primeKey'); + expect(data).toEqual('loading'); + }); + }); + + describe('|getParam|', () => { + it('should verify will invoke correct methods', () => { + // Given + const getParamSpy = spyOn(RouteSegments.prototype, 'getParam').and.returnValue('test-value'); + + // When + const param = routeState.getParam('random-key'); + + // Then + expect(getParamSpy).toHaveBeenCalledWith('random-key'); + expect(param).toEqual('test-value'); + }); + }); + + describe('|getQueryParam|', () => { + it('should verify will invoke correct methods', () => { + // Given + const getQueryParamSpy = spyOn(RouteSegments.prototype, 'getQueryParam').and.returnValue('test-value'); + + // When + const queryParam = routeState.getQueryParam('random-key'); + + // Then + expect(getQueryParamSpy).toHaveBeenCalledWith('random-key'); + expect(queryParam).toEqual('test-value'); + }); + }); + + describe('|getParentAbsoluteRoutePath|', () => { + it('should verify will return correct value', () => { + // When + const parentAbsoluteRoutePath = routeState.getParentAbsoluteRoutePath(); + + // Then + expect(parentAbsoluteRoutePath).toEqual('/domain/context/entity/17/explore'); + }); + }); + + describe('|toJSON|', () => { + it('should verify will return correct object for serialization', () => { + // When + const objForSerialize = routeState.toJSON(); + + // Then + expect(objForSerialize).toEqual({ + routeSegments, + url: 'domain/context/entity/17/explore/bill/105/view', + routePath: 'bill/105/view', + absoluteRoutePath: '/domain/context/entity/17/explore/bill/105/view', + routePathSegments: ['domain/context', 'entity/17/explore', 'bill/105/view'], + configPath: 'bill/:billId/view', + absoluteConfigPath: '/domain/context/entity/:entityId/explore/bill/:billId/view', + configPathSegments: ['domain/context', 'entity/:entityId/explore', 'bill/:billId/view'], + queryParams: { 'test_search*': '^test-team$', 'test_@active-user': '%aUser%' } + }); + }); + }); + }); + }); + + describe('RouterState', () => { + let routeSegments: RouteSegments; + let routeState: RouteState; + let routerState: RouterState; + let previousRouterState: RouterState; + + beforeEach(() => { + routeSegments = RouteSegments.of( + 'bill/105/view', + {}, + null, + { 'test_search*': '^test-team$', 'test_@active-user': '%aUser%' }, + RouteSegments.of( + 'entity/17/explore', + {}, + null, + null, + RouteSegments.of( + 'domain/context', + {}, + null, + null, + null, + 'domain/context' + ), + 'entity/:entityId/explore' + ), + 'bill/:billId/view' + ); + + routeState = RouteState.of( + routeSegments, + 'domain/context/entity/17/explore/bill/105/view' + ); + + routerState = RouterState.of( + routeState, + 15 + ); + + previousRouterState = RouterState.of( + RouteState.empty(), + 14 + ); + previousRouterState.previousStates.push( + RouterState.of( + RouteState.empty(), + 13 + ), + RouterState.of( + RouteState.empty(), + 12 + ), + RouterState.of( + RouteState.empty(), + 11 + ) + ); + }); + + it('should verify instance is created', () => { + // When + const instance = new RouterState( + new RouteState( + new RouteSegments( + 'domain/context', + {}, + {}, + null, + null), + 'domain/context' + ), + 8 + ); + + // Then + expect(instance).toBeDefined(); + }); + + describe('Statics::', () => { + describe('Methods::', () => { + describe('|of|', () => { + it('should verify factory method will create instance', () => { + // When + const instance = RouterState.of( + new RouteState( + new RouteSegments( + 'domain/context', + {}, + {}, + null, + null), + 'domain/context' + ), + 11 + ); + + // Then + expect(instance).toBeInstanceOf(RouterState); + }); + }); + + describe('|empty|', () => { + it('should verify will create empty instance with default values', () => { + // When + const instance = RouterState.empty(); + + // Then + expect(instance).toBeInstanceOf(RouterState); + expect(instance.state).toEqual(RouteState.empty()); + expect(instance.navigationId).toEqual(null); + expect(instance.previousStates).toEqual([]); + }); + }); + }); + }); + + describe('Methods::', () => { + describe('|getPrevious|', () => { + beforeEach(() => { + routerState.previousStates.push( + ...previousRouterState.previousStates + ); + previousRouterState.previousStates.length = 0; + routerState.previousStates.unshift(previousRouterState); + }); + + it('should verify will return the first before current as default', () => { + // When + const previous = routerState.getPrevious(); + + // Then + expect(previous).toBe(previousRouterState); + }); + + it('should verify will return the first before current for index 0', () => { + // When + const previous = routerState.getPrevious(0); + + // Then + expect(previous).toBe(previousRouterState); + }); + + it('should verify will return the third before current for index 2', () => { + // When + const previous = routerState.getPrevious(2); + + // Then + expect(previous).toBe(routerState.previousStates[2]); + }); + + it('should verify will return null for number less than 0', () => { + // When + const previous = routerState.getPrevious(-1); + + // Then + expect(previous).toBe(null); + }); + + it('should verify will return the first before current for index not a number', () => { + // When + const previous = routerState.getPrevious(null); + + // Then + expect(previous).toBe(previousRouterState); + }); + + it('should verify will return null for index that is out of bound of stored States', () => { + // When + const previous = routerState.getPrevious(4); + + // Then + expect(previous).toBe(null); + }); + }); + + describe('|appendPrevious|', () => { + it('should verify will add previousStates to current one', () => { + // Given + const firstElement = RouterState.of(RouteState.empty(), 14); + const forthElement = RouterState.of(RouteState.empty(), 11); + + // When + routerState.appendPrevious(previousRouterState); + + // Then + expect(routerState.previousStates.length).toEqual(4); + expect(routerState.previousStates[0]).toEqual(firstElement); + expect(routerState.previousStates[3]).toEqual(forthElement); + }); + + it('should verify will pop the oldest one and will unshift the new one when buffer has 10 elements', () => { + // Given + previousRouterState.previousStates.push( + RouterState.of( + RouteState.empty(), + 10 + ), + RouterState.of( + RouteState.empty(), + 9 + ), + RouterState.of( + RouteState.empty(), + 8 + ), + RouterState.of( + RouteState.empty(), + 7 + ), + RouterState.of( + RouteState.empty(), + 6 + ), + RouterState.of( + RouteState.empty(), + 5 + ), + RouterState.of( + RouteState.empty(), + 4 + ) + ); + + // Then 1 + expect(routerState.previousStates.length).toEqual(0); + expect(routerState.previousStates[0]).toBeUndefined(); + expect(previousRouterState.navigationId).toEqual(14); + expect(previousRouterState.previousStates[0]).toEqual( + RouterState.of(RouteState.empty(), 13) + ); + expect(previousRouterState.previousStates[9]).toEqual( + RouterState.of(RouteState.empty(), 4) + ); + + // When + routerState.appendPrevious(previousRouterState); + + // Then 2 + expect(routerState.previousStates.length).toEqual(10); + expect(routerState.previousStates[0]).toEqual( + RouterState.of(RouteState.empty(), 14) + ); + expect(routerState.previousStates[9]).toEqual( + RouterState.of(RouteState.empty(), 5) + ); + }); + + it(`should verify won't add new state when previousState has same navigationId like current`, () => { + // Given + routerState = RouterState.of( + RouteState.empty(), + 14 + ); + + // Then 1 + expect(routerState.previousStates.length).toEqual(0); + expect(previousRouterState.previousStates.length).toEqual(3); + expect(previousRouterState.previousStates[0]).toEqual( + RouterState.of(RouteState.empty(), 13) + ); + expect(previousRouterState.previousStates[2]).toEqual( + RouterState.of(RouteState.empty(), 11) + ); + + // When + routerState.appendPrevious(previousRouterState); + + // Then 2 + expect(routerState.previousStates.length).toEqual(3); + expect(routerState.navigationId).toEqual(14); + expect(routerState.previousStates[0]).toEqual( + RouterState.of(RouteState.empty(), 13) + ); + expect(routerState.previousStates[2]).toEqual( + RouterState.of(RouteState.empty(), 11) + ); + }); + }); + }); + }); +}); diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/model/route.model.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/model/route.model.ts new file mode 100644 index 0000000000..6166a4b410 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/model/route.model.ts @@ -0,0 +1,443 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +/* eslint-disable @typescript-eslint/member-ordering */ + +import { Params } from '@angular/router'; + +import { BaseRouterStoreState, RouterReducerState } from '@ngrx/router-store'; + +import { CollectionsUtil, PrimitivesNil } from '../../../utils'; + +import { Serializable, TaurusRouteData } from '../../../common'; + +/** + * ** Route Segments Class. + * + * + */ +export class RouteSegments { + public readonly routePath: string; + public readonly data: TaurusRouteData; + public readonly params: Params; + public readonly queryParams: Params; + public readonly parent?: RouteSegments; + public readonly configPath?: string; + + /** + * ** Constructor. + */ + constructor( + routePath: string, + data: TaurusRouteData, + params: Params, + queryParams: Params, + parent?: RouteSegments, + configPath?: string) { + + this.routePath = routePath ?? ''; + this.data = data || {}; + this.params = params || {}; + this.queryParams = queryParams || {}; + this.parent = parent; + this.configPath = configPath; + } + + /** + * ** Factory method. + */ + static of( + routePath?: string, + data?: TaurusRouteData, + params?: Params, + queryParams?: Params, + parent?: RouteSegments, + configPath?: string): RouteSegments { + + return new RouteSegments(routePath, data, params, queryParams, parent, configPath); + } + + /** + * ** Factory method for empty RouteSegments. + */ + static empty(): RouteSegments { + return RouteSegments.of(null, null, null, null, null, null); + } + + /** + * ** Get RoutePath Segments. + */ + get routePathSegments(): string[] { + if (this.parent) { + return ([] as string[]) + .concat(this.parent.routePathSegments, this.routePath) + .filter((path) => path); + } + + return [this.routePath]; + } + + /** + * ** Get ConfigPath Segments. + */ + get configPathSegments(): string[] { + if (this.parent) { + return ([] as string[]) + .concat(this.parent.configPathSegments, this.configPath) + .filter((path) => path); + } + + return [this.configPath]; + } + + /** + * ** Get Data from Route configuration by key. + * + * - Return first (closest) found key starting from the current one. + */ + getData(key: string): T { + if (this.data[key]) { + return this.data[key] as T; + } + + if (this.parent) { + return this.parent.getData(key); + } + + return undefined; + } + + /** + * ** Get url param by key. + * + * - Return first (closest) found key starting from the current one. + */ + getParam(key: string): string { + if (this.params[key]) { + return this.params[key] as string; + } + + if (this.parent) { + return this.parent.getParam(key); + } + + return undefined; + } + + /** + * ** Get query param by key. + */ + getQueryParam(key: string): string { + if (this.queryParams[key]) { + return this.queryParams[key] as string; + } + + if (this.parent) { + return this.parent.getQueryParam(key); + } + + return undefined; + } +} + +/** + * ** Route State Class. + * + * + */ +export class RouteState implements BaseRouterStoreState, Serializable { + public readonly routeSegments: RouteSegments; + public readonly url: string; + + /** + * ** Constructor. + */ + constructor(routeSegments: RouteSegments, + url: string) { + this.routeSegments = routeSegments ?? RouteSegments.empty(); + this.url = url ?? ''; + } + + /** + * ** Factory method. + */ + static of(routeSegments: RouteSegments, url: string): RouteState { + return new RouteState(routeSegments, url); + } + + /** + * ** Factory method for empty State. + */ + static empty(): RouteState { + return RouteState.of(null, null); + } + + /** + * ** Get serialized queryString. + */ + static serializeQueryParams(queryParams: unknown): string { + const paramsKeys = Object.keys(queryParams); + + if (!paramsKeys.length) { + return ''; + } + + return paramsKeys + .map((key) => `${ encodeURIComponent(key) }=${ encodeURIComponent(queryParams[key] as string) }`) + .join('&'); + } + + /** + * ** Returns current RoutePath. + */ + get routePath(): string { + return this.routeSegments.routePath; + } + + /** + * ** Returns current Absolute RoutePath. + */ + get absoluteRoutePath(): string { + return RouteState._resolveAbsolutePath( + this.routePathSegments + ); + } + + /** + * ** Returns the route paths for each route segment starting from the root. + */ + get routePathSegments(): string[] { + return this.routeSegments.routePathSegments; + } + + /** + * ** Returns current ConfigPath. + */ + get configPath(): string { + return this.routeSegments.configPath; + } + + /** + * ** Returns current Absolute ConfigPath. + */ + get absoluteConfigPath(): string { + return RouteState._resolveAbsolutePath( + this.configPathSegments + ); + } + + /** + * ** Returns the config paths for each route segment starting from the root. + */ + get configPathSegments(): string[] { + return this.routeSegments.configPathSegments; + } + + /** + * ** Get all query params. + */ + get queryParams(): Params { + return this.routeSegments.queryParams; + } + + /** + * ** Get serialized queryString. + */ + serializeQueryParams(): string { + return RouteState.serializeQueryParams(this.queryParams); + } + + /** + * ** Get url including QueryParams. + */ + getUrl(): string { + return `${ this.absoluteRoutePath }?${ this.serializeQueryParams() }`; + } + + /** + * ** Get Data from Route configuration by key. + * + * - Return first (closest) found key starting from first RouteSegment. + */ + getData(key: string): T { + return this.routeSegments.getData(key); + } + + /** + * ** Get url param by key. + * + * - Return first (closest) found key starting from first RouteSegment. + */ + getParam(key: string): string { + return this.routeSegments.getParam(key); + } + + /** + * ** Get query param by key. + */ + getQueryParam(key: string): string { + return this.routeSegments.getQueryParam(key); + } + + /** + * ** Get Absolute ConfigPath. + */ + getAbsoluteConfigPath(): string { + return this.absoluteConfigPath; + } + + /** + * ** Get parent of current Absolute ConfigPath. + */ + getParentAbsoluteConfigPath(): string { + const configPathSegments = this.configPathSegments; + configPathSegments.pop(); + + return RouteState._resolveAbsolutePath(configPathSegments); + } + + /** + * ** Get Absolute RoutePath. + */ + getAbsoluteRoutePath(): string { + return RouteState._resolveAbsolutePath( + this.routePathSegments + ); + } + + /** + * ** Get parent of current Absolute RoutePath. + */ + getParentAbsoluteRoutePath(): string { + const routePathSegments = this.routePathSegments; + routePathSegments.pop(); + + return RouteState._resolveAbsolutePath(routePathSegments); + } + + /** + * @inheritDoc + */ + toJSON(): SerializedRouteState { + return { + url: this.url, + routePath: this.routePath, + absoluteRoutePath: this.absoluteRoutePath, + routePathSegments: this.routePathSegments, + configPath: this.configPath, + absoluteConfigPath: this.absoluteConfigPath, + configPathSegments: this.configPathSegments, + queryParams: this.queryParams, + routeSegments: this.routeSegments + }; + } + + /** + * ** Resolve Absolute RoutePath from given routePathSegments. + */ + private static _resolveAbsolutePath(routePathSegments: string[]): string { + const path = routePathSegments + .join('/') + .replace(/^\/+/, ''); + + if (path === '') { + return '/'; + } + + return `/${ path }`; + } +} + +/** + * ** Router state. + */ +export class RouterState implements RouterReducerState { + readonly state: RouteState; + readonly navigationId: number; + readonly previousStates: RouterState[]; + + /** + * ** Constructor. + */ + constructor(state: RouteState, + navigationId: number) { + this.state = state ?? RouteState.empty(); + this.navigationId = navigationId ?? null; + this.previousStates = []; + } + + /** + * ** Factory method. + */ + static of(state: RouteState, navigationId: number): RouterState { + return new RouterState(state, navigationId); + } + + /** + * ** Factory method for empty State. + */ + static empty(): RouterState { + return RouterState.of(null, null); + } + + /** + * ** Returns previous RouterState if exist otherwise null. + * + * - Optional parameter could be provided to instruct which previous RouterState to return, default one is 0. + * - 0 means the first before current. + * - 1 means the second before current. + * - 2 means the third before current. + * - 3 ... etc... + */ + getPrevious(index = 0): RouterState | null { + const lookupIndex = CollectionsUtil.isNumber(index) + ? index + : 0; + + if (lookupIndex >= 0 && lookupIndex < this.previousStates.length) { + return this.previousStates[lookupIndex]; + } + + return null; + } + + /** + * ** Append previous RouterState[] to current One. + * + * - Internal API used in reducer, not for public use. + */ + appendPrevious(routerState: RouterState): void { + const previousStoredStates: RouterState[] = [...routerState.previousStates]; + const cleanedPreviousState = RouterState.of(routerState.state, routerState.navigationId); + + if (this.navigationId !== cleanedPreviousState.navigationId) { + if (previousStoredStates.length >= 10) { + previousStoredStates.pop(); + } + + previousStoredStates.unshift(cleanedPreviousState); + } + + this.previousStates.length = 0; + this.previousStates.push(...previousStoredStates); + } +} + +/** + * ** Route state serialized. + */ +interface SerializedRouteState { + url: string; + routePath: string; + absoluteRoutePath: string; + routePathSegments: string[]; + configPath: string; + absoluteConfigPath: string; + configPathSegments: string[]; + queryParams: { [key: string]: PrimitivesNil }; + routeSegments: RouteSegments; +} diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/public-api.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/public-api.ts new file mode 100644 index 0000000000..a99e71c6d1 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/public-api.ts @@ -0,0 +1,9 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './index'; +export * from './factory'; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/services/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/services/index.ts new file mode 100644 index 0000000000..adc9a3f64f --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/services/index.ts @@ -0,0 +1,8 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './router.service'; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/services/router.service.spec.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/services/router.service.spec.ts new file mode 100644 index 0000000000..fde2ad54e6 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/services/router.service.spec.ts @@ -0,0 +1,193 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +/* eslint-disable @typescript-eslint/dot-notation */ + +import { TestBed } from '@angular/core/testing'; + +import { of } from 'rxjs'; +import { delay } from 'rxjs/operators'; + +import { marbles } from 'rxjs-marbles/jasmine'; + +import { Store } from '@ngrx/store'; + +import { STORE_ROUTER, StoreState } from '../../ngrx'; + +import { RouterState, RouteState } from '../model'; + +import { RouterService, RouterServiceImpl } from './router.service'; + +describe('RouterService -> RouterServiceImpl', () => { + let storeStub$: jasmine.SpyObj>; + let service: RouterService; + + beforeEach(() => { + storeStub$ = jasmine.createSpyObj>('store', ['select']); + + TestBed.configureTestingModule({ + providers: [ + { provide: Store, useValue: storeStub$ }, + { provide: RouterService, useClass: RouterServiceImpl } + ] + }); + + service = TestBed.inject(RouterService); + }); + + it('should verify instance is created', () => { + // Then + expect(service).toBeDefined(); + }); + + describe('Statics::', () => { + describe('Methods::', () => { + describe('|get|', () => { + it('should verify will return RouterState', () => { + // When + const routerState = RouterService.get(); + + // Then + expect(routerState.state).toEqual(RouteState.empty()); + }); + }); + + describe('|getState|', () => { + it('should verify will return RouteState', () => { + // When + const routeState = RouterService.getState(); + + // Then + expect(routeState).toEqual(RouteState.empty()); + }); + }); + }); + }); + + describe('Methods::', () => { + describe('|get|', () => { + it('should verify will select RouterState from Store', () => { + // Given + storeStub$.select.and.returnValue(of({ state: {}, navigationId: 5 } as RouterState)); + + // When + service.get(); + + // Then + expect(storeStub$.select.calls.mostRecent().args).toEqual([STORE_ROUTER]); + }); + + it('should verify will return correct Observable state', marbles((m) => { + // Given + const storeStream$ = m.cold('----a---', { + a: { + state: RouteState.empty(), + navigationId: 7 + } as RouterState + }); + const expectedOutputStream$ = m.cold('----a---', { + a: { + state: RouteState.empty(), + navigationId: 7 + } as RouterState + }); + storeStub$.select.and.returnValue(storeStream$); + + // When + const response$ = service.get(); + + // Then + m.expect(response$).toBeObservable(expectedOutputStream$); + })); + }); + + describe('|getState|', () => { + it('should verify will select RouterState from Store', () => { + // Given + storeStub$.select.and.returnValue(of({ state: {}, navigationId: 5 } as RouterState)); + + // When + service.getState(); + + // Then + expect(storeStub$.select.calls.mostRecent().args).toEqual([STORE_ROUTER]); + }); + + it('should verify will return correct Observable state', marbles((m) => { + // Given + const storeStream$ = m.cold('----a---', { + a: { + state: RouteState.empty(), + navigationId: 7 + } as RouterState + }); + const expectedOutputStream$ = m.cold('----a---', { + a: RouteState.empty() + }); + storeStub$.select.and.returnValue(storeStream$); + + // When + const response$ = service.getState(); + + // Then + m.expect(response$).toBeObservable(expectedOutputStream$); + })); + }); + + describe('|initialize|', () => { + it('should verify will push created subscriptions to buffer', () => { + // Given + const storeStream$ = of({ + state: RouteState.empty(), + navigationId: 7 + } as RouterState); + storeStub$.select.and.returnValue(storeStream$); + // @ts-ignore + const cleanSubSpy = spyOn(service, 'cleanSubscriptions').and.callThrough(); + + // Then 1 + expect(service['subscriptions'].length).toEqual(0); + + // When + service.initialize(); + + // Then 2 + expect(service['subscriptions'].length).toEqual(1); + expect(cleanSubSpy).toHaveBeenCalled(); + }); + + it('should verify will create subscriptions and will assign value to local state', marbles((m) => { + // Given + const routerState = RouterState.of( + RouteState.empty(), + 7 + ); + const storeStream$ = m.hot('-^--a---', { + a: routerState + }); + storeStub$.select.and.returnValue(storeStream$); + + // When 1 + service.initialize(); + + // delay of 4 frames + of(true).pipe( + delay(m.time('----|')) + ).subscribe(() => { + // Then 1 + expect(RouterService['_routerState']).toBe(routerState); + + // When 2 // destroy Service + service.ngOnDestroy(); + + // Then + m.expect(storeStream$).toHaveSubscriptions('^---!'); + }); + })); + }); + }); +}); diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/services/router.service.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/services/router.service.ts new file mode 100644 index 0000000000..2b4af4d298 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/services/router.service.ts @@ -0,0 +1,99 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Directive, Injectable } from '@angular/core'; + +import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; + +import { Store } from '@ngrx/store'; + +import { TaurusObject } from '../../../common'; + +import { STORE_ROUTER, StoreState } from '../../ngrx'; + +import { RouterState, RouteState } from '../model'; + +/** + * ** Router Service. + * + * + */ +@Directive() +// eslint-disable-next-line @angular-eslint/directive-class-suffix +export abstract class RouterService extends TaurusObject { + protected static _routerState: RouterState = RouterState.empty(); + + /** + * ** Will return current Router. + */ + static get(): RouterState { + return RouterService._routerState; + } + + /** + * ** Will return current Route State. + */ + static getState(): RouteState { + return RouterService._routerState.state; + } + + /** + * ** Will return Observable with NgRx Route State. + */ + abstract get(): Observable; + + /** + * ** Will return Observable with Route State. + */ + abstract getState(): Observable; + + /** + * ** Will initialize service. + */ + abstract initialize(): void; +} + +/** + * @inheritDoc + */ +@Injectable() +export class RouterServiceImpl extends RouterService { + constructor(private readonly store$: Store) { + super(); + } + + /** + * @inheritDoc + */ + get(): Observable { + return this.store$.select(STORE_ROUTER); + } + + /** + * @inheritDoc + */ + getState(): Observable { + return this.get().pipe( + map((data) => data.state) + ); + } + + /** + * @inheritDoc + */ + initialize(): void { + this.cleanSubscriptions(); + + this.subscriptions.push( + this.get() + .subscribe((state) => { + RouterService._routerState = state; + }) + ); + } +} diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/state/actions/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/state/actions/index.ts new file mode 100644 index 0000000000..258264b723 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/state/actions/index.ts @@ -0,0 +1,8 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './router.actions'; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/state/actions/router.actions.spec.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/state/actions/router.actions.spec.ts new file mode 100644 index 0000000000..626c0f6ba0 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/state/actions/router.actions.spec.ts @@ -0,0 +1,171 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { BaseAction, BaseActionWithPayload } from '../../../ngrx'; + +import { + LOCATION_BACK, + LOCATION_FORWARD, + LOCATION_GO, + LocationBack, + LocationForward, + LocationGo, + ROUTER_NAVIGATE, + RouterNavigate +} from './router.actions'; + +describe('NavigationActions', () => { + describe('RouterNavigate', () => { + it('should verify instance is created', () => { + // Then + expect(() => new RouterNavigate({ commands: [], extras: {} })).toBeDefined(); + }); + + it('should verify correct type is assigned', () => { + // When + const instance = new RouterNavigate({ commands: [], extras: {} }); + + // Then + expect(instance.type).toEqual(ROUTER_NAVIGATE); + }); + + it('should verify prototype chaining', () => { + // When + const instance = new RouterNavigate({ commands: [], extras: {} }); + + // Then + expect(instance).toBeInstanceOf(BaseActionWithPayload); + expect(instance).toBeInstanceOf(BaseAction); + }); + + describe('Statics::', () => { + describe('Methods::', () => { + describe('|of|', () => { + it('should verify factory method will create instance', () => { + // When + const instance = RouterNavigate.of({ commands: [], extras: {} }); + + // Then + expect(instance).toBeInstanceOf(RouterNavigate); + }); + }); + }); + }); + }); + + describe('LocationGo', () => { + it('should verify instance is created', () => { + // Then + expect(() => new LocationGo({ path: 'entity/15', query: 'search=test-team', state: {} })).toBeDefined(); + }); + + it('should verify correct type is assigned', () => { + // When + const instance = new LocationGo({ path: 'entity/15', query: 'search=test-team', state: {} }); + + // Then + expect(instance.type).toEqual(LOCATION_GO); + }); + + it('should verify prototype chaining', () => { + // When + const instance = new LocationGo({ path: 'entity/15', query: 'search=test-team', state: {} }); + + // Then + expect(instance).toBeInstanceOf(BaseActionWithPayload); + expect(instance).toBeInstanceOf(BaseAction); + }); + + describe('Statics::', () => { + describe('Methods::', () => { + describe('|of|', () => { + it('should verify factory method will create instance', () => { + // When + const instance = LocationGo.of({ path: 'entity/15', query: 'search=test-team', state: {} }); + + // Then + expect(instance).toBeInstanceOf(LocationGo); + }); + }); + }); + }); + }); + + describe('LocationBack', () => { + it('should verify instance is created', () => { + // Then + expect(() => new LocationBack()).toBeDefined(); + }); + + it('should verify correct type is assigned', () => { + // When + const instance = new LocationBack(); + + // Then + expect(instance.type).toEqual(LOCATION_BACK); + }); + + it('should verify prototype chaining', () => { + // When + const instance = new LocationBack(); + + // Then + expect(instance).toBeInstanceOf(BaseAction); + }); + + describe('Statics::', () => { + describe('Methods::', () => { + describe('|of|', () => { + it('should verify factory method will create instance', () => { + // When + const instance = LocationBack.of(); + + // Then + expect(instance).toBeInstanceOf(LocationBack); + }); + }); + }); + }); + }); + + describe('LocationForward', () => { + it('should verify instance is created', () => { + // Then + expect(() => new LocationForward()).toBeDefined(); + }); + + it('should verify correct type is assigned', () => { + // When + const instance = new LocationForward(); + + // Then + expect(instance.type).toEqual(LOCATION_FORWARD); + }); + + it('should verify prototype chaining', () => { + // When + const instance = new LocationForward(); + + // Then + expect(instance).toBeInstanceOf(BaseAction); + }); + + describe('Statics::', () => { + describe('Methods::', () => { + describe('|of|', () => { + it('should verify factory method will create instance', () => { + // When + const instance = LocationForward.of(); + + // Then + expect(instance).toBeInstanceOf(LocationForward); + }); + }); + }); + }); + }); +}); diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/state/actions/router.actions.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/state/actions/router.actions.ts new file mode 100644 index 0000000000..299d347a31 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/state/actions/router.actions.ts @@ -0,0 +1,125 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { NavigationExtras } from '@angular/router'; + +import { BaseAction, BaseActionWithPayload } from '../../../ngrx/actions'; + +/** + * ** Action Identifier for Router Navigate. + * + * + */ +export const ROUTER_NAVIGATE = '[router] Navigate'; + +/** + * ** Action Identifier for Location Go. + * + * + */ +export const LOCATION_GO = '[location] Go'; + +/** + * ** Action Identifier for Location Back. + * + * + */ +export const LOCATION_BACK = '[location] Back'; + +/** + * ** Action Identifier for Location Forward. + * + * + */ +export const LOCATION_FORWARD = '[location] Forward'; + +export interface NavigatePayload { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + commands: any[]; + extras?: NavigationExtras; +} + +/** + * ** Navigate Action instruct subscribers that they should navigate to given path. + * + * + */ +export class RouterNavigate extends BaseActionWithPayload { + constructor(payload: NavigatePayload) { + super(ROUTER_NAVIGATE, payload); + } + + /** + * ** Factory method. + */ + static override of(payload: NavigatePayload) { + return new RouterNavigate(payload); + } +} + +export interface GoPayload { + path: string; + query?: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + state?: any; +} + +/** + * ** Location Go Action instruct subscribers that they should navigate using Location. + * + * + */ +export class LocationGo extends BaseActionWithPayload { + constructor(payload: GoPayload) { + super(LOCATION_GO, payload); + } + + /** + * ** Factory method. + */ + static override of(payload: GoPayload) { + return new LocationGo(payload); + } +} + +/** + * ** Back Action instruct subscribers to pop history Backward. + * + * + */ +export class LocationBack extends BaseAction { + constructor() { + super(LOCATION_BACK); + } + + /** + * ** Factory method. + */ + static of() { + return new LocationBack(); + } +} + +/** + * ** Forward Action instruct subscribers to go Forward. + * + * + */ +export class LocationForward extends BaseAction { + constructor() { + super(LOCATION_FORWARD); + } + + /** + * ** Factory method. + */ + static of() { + return new LocationForward(); + } +} + +export type NavigationActions = RouterNavigate | LocationGo | LocationBack | LocationForward; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/state/effects/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/state/effects/index.ts new file mode 100644 index 0000000000..49fe38a24f --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/state/effects/index.ts @@ -0,0 +1,8 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './router.effects'; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/state/effects/router.effects.spec.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/state/effects/router.effects.spec.ts new file mode 100644 index 0000000000..e3d34b49a5 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/state/effects/router.effects.spec.ts @@ -0,0 +1,129 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Location } from '@angular/common'; +import { Router } from '@angular/router'; +import { TestBed, waitForAsync } from '@angular/core/testing'; + +import { provideMockActions } from '@ngrx/effects/testing'; + +import { Observable } from 'rxjs'; +import { marbles } from 'rxjs-marbles/jasmine'; + +import { RouterEffects } from './router.effects'; +import { LocationBack, LocationForward, LocationGo, RouterNavigate } from '../actions'; + +describe('RouterEffects', () => { + let effects: RouterEffects; + + let routeAction$: Observable; + + let routerStub: jasmine.SpyObj; + let locationStub: jasmine.SpyObj; + + beforeEach(waitForAsync(() => { + routerStub = jasmine.createSpyObj('router', ['navigate']); + locationStub = jasmine.createSpyObj('location', ['go', 'back', 'forward']); + + TestBed.configureTestingModule({ + providers: [ + { + provide: Router, + useValue: routerStub + }, + { + provide: Location, + useValue: locationStub + }, + provideMockActions(() => routeAction$), + RouterEffects + ] + }); + + routerStub.navigate.and.returnValue(Promise.resolve(true)); + + effects = TestBed.inject(RouterEffects); + })); + + it('should use Location.go()', marbles((m) => { + routeAction$ = m.hot('-a', { a: new LocationGo({ path: '/test', query: 'search=random', state: {} }) }); + effects.locationGo$.subscribe(() => { + expect(locationStub.go).toHaveBeenCalled(); + }); + })); + + it(`should verify will handle error and won't invoke Location.go()`, marbles((m) => { + routeAction$ = m.hot('-#'); + + effects.locationGo$.subscribe((v) => { + expect(v).toBeTrue(); + expect(locationStub.go).not.toHaveBeenCalled(); + }); + })); + + it('should navigate back', marbles((m) => { + routeAction$ = m.hot('-a', { a: new LocationBack() }); + effects.locationBack$.subscribe(() => { + expect(locationStub.back).toHaveBeenCalled(); + }); + })); + + it(`should verify will handle error and won't invoke navigate back`, marbles((m) => { + routeAction$ = m.hot('-#'); + + effects.locationBack$.subscribe((v) => { + expect(v).toBeTrue(); + expect(locationStub.back).not.toHaveBeenCalled(); + }); + })); + + it('should navigate forward', marbles((m) => { + routeAction$ = m.hot('-a', { a: new LocationForward() }); + effects.locationForward$.subscribe(() => { + expect(locationStub.forward).toHaveBeenCalled(); + }); + })); + + it(`should verify will handle error and won't invoke Location.forward()`, marbles((m) => { + routeAction$ = m.hot('-#'); + + effects.locationForward$.subscribe((v) => { + expect(v).toBeTrue(); + expect(locationStub.forward).not.toHaveBeenCalled(); + }); + })); + + it('should navigate to path', marbles((m) => { + const goAction = new RouterNavigate({ + commands: ['/domain', 'context', 'entity', 10, 'sub-entity', 7], + extras: { + queryParams: { value: 1 } + } + }); + + routeAction$ = m.hot('a', { a: goAction }); + effects.routerNavigate$.subscribe(() => { + expect(routerStub.navigate).toHaveBeenCalledWith([ + '/domain', + 'context', + 'entity', + 10, + 'sub-entity', + 7 + ], goAction.payload.extras); + }); + })); + + it(`should verify will handle error and won't invoke router navigate`, marbles((m) => { + routeAction$ = m.hot('-#'); + + effects.routerNavigate$.subscribe((v) => { + expect(v).toBeTrue(); + expect(routerStub.navigate).not.toHaveBeenCalled(); + }); + })); +}); diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/state/effects/router.effects.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/state/effects/router.effects.ts new file mode 100644 index 0000000000..a04d69b227 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/state/effects/router.effects.ts @@ -0,0 +1,99 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Injectable } from '@angular/core'; +import { Location } from '@angular/common'; +import { Router } from '@angular/router'; + +import { Observable, of } from 'rxjs'; +import { catchError, map, tap } from 'rxjs/operators'; + +import { Actions, createEffect, ofType } from '@ngrx/effects'; + +import { GoPayload, LOCATION_BACK, LOCATION_FORWARD, LOCATION_GO, NavigatePayload, ROUTER_NAVIGATE, RouterNavigate } from '../actions'; + +/** + * ** Router Effects Service. + * + * + */ +@Injectable() +export class RouterEffects { + /** + * ** Effect for Router navigation. + */ + routerNavigate$ = createEffect( + () => this.actions$.pipe( + ofType(ROUTER_NAVIGATE), + map((action: RouterNavigate) => action.payload), + tap((payload) => this._navigate(payload)), + catchError((error: unknown) => RouterEffects._handleError(error)) + ), + { dispatch: false } + ); + + /** + * ** Effect for Location go (navigate). + */ + locationGo$ = createEffect( + () => this.actions$.pipe( + ofType(LOCATION_GO), + tap((payload: GoPayload) => this.location.go(payload.path, payload.query, payload.state)), + catchError((error: unknown) => RouterEffects._handleError(error)) + ), + { dispatch: false } + ); + + /** + * ** Effect for pop Backward Browser state. + */ + locationBack$ = createEffect( + () => this.actions$.pipe( + ofType(LOCATION_BACK), + tap(() => this.location.back()), + catchError((error: unknown) => RouterEffects._handleError(error)) + ), + { dispatch: false } + ); + + /** + * ** Effect for push Forward Browser state. + */ + locationForward$ = createEffect( + () => this.actions$.pipe( + ofType(LOCATION_FORWARD), + tap(() => this.location.forward()), + catchError((error: unknown) => RouterEffects._handleError(error)) + ), + { dispatch: false } + ); + + /** + * ** Constructor. + */ + constructor(private readonly actions$: Actions, + private readonly router: Router, + private readonly location: Location) { + } + + private static _handleError(error: unknown): Observable { + console.error(error); + + return of(true); + } + + private _navigate(payload: NavigatePayload): void { + const extras = payload.extras ?? {}; + + // eslint-disable-next-line @typescript-eslint/no-floating-promises + this.router + .navigate(payload.commands, extras) + .then(() => { + // No-op. + }); + } +} diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/state/reducers/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/state/reducers/index.ts new file mode 100644 index 0000000000..3bbc2f5e1e --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/state/reducers/index.ts @@ -0,0 +1,8 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './router.reducer'; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/state/reducers/router.reducer.spec.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/state/reducers/router.reducer.spec.ts new file mode 100644 index 0000000000..018109ec64 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/state/reducers/router.reducer.spec.ts @@ -0,0 +1,38 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { RouterStateSnapshot, RoutesRecognized } from '@angular/router'; + +import { ROUTER_NAVIGATION, RouterNavigationAction } from '@ngrx/router-store'; + +import { RouterState, RouteState } from '../../model'; + +import { routerReducer } from './router.reducer'; + +describe('routerReducer', () => { + it('should verify will invoke correct methods', () => { + // Given + const navigateAction: RouterNavigationAction = { + type: ROUTER_NAVIGATION, + payload: { + routerState: RouteState.empty(), + event: new RoutesRecognized(5, 'domain/context', 'domain/context', {} as RouterStateSnapshot) + } + }; + const storedRouterStateStub = jasmine.createSpyObj('routerState', ['appendPrevious']); + const newRouterStateStub = jasmine.createSpyObj('routerState', ['appendPrevious']); + const factoryMethodSpy = spyOn(RouterState, 'of').and.returnValue(newRouterStateStub); + + // When + const value = routerReducer(storedRouterStateStub, navigateAction); + + // Then + expect(value).toBe(newRouterStateStub); + expect(factoryMethodSpy).toHaveBeenCalledWith(navigateAction.payload.routerState, navigateAction.payload.event.id); + expect(newRouterStateStub.appendPrevious).toHaveBeenCalledWith(storedRouterStateStub); + }); +}); diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/state/reducers/router.reducer.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/state/reducers/router.reducer.ts new file mode 100644 index 0000000000..4376692c54 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/router/state/reducers/router.reducer.ts @@ -0,0 +1,47 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +/* eslint-disable arrow-body-style,prefer-arrow/prefer-arrow-functions */ + +import { + ROUTER_CANCEL, + ROUTER_ERROR, + ROUTER_NAVIGATION, + RouterCancelAction, + RouterErrorAction, + RouterNavigationAction +} from '@ngrx/router-store'; + +import { RouterState, RouteState } from '../../model'; + +type AcceptedActions = RouterNavigationAction | + RouterErrorAction | + RouterCancelAction; + +/** + * ** Reducer for Router Actions. + * + * + */ +export function routerReducer( + state = RouterState.empty(), + action: AcceptedActions = { type: null, payload: null }) { + + const actionPayload = action.payload; + + switch (action.type) { + case ROUTER_NAVIGATION: + case ROUTER_ERROR: + case ROUTER_CANCEL: + const newState = RouterState.of(actionPayload.routerState, actionPayload.event.id); + newState.appendPrevious(state); + + return newState; + default: + return state; + } +} diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/decorator/event-handler-class.decorator.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/decorator/event-handler-class.decorator.ts new file mode 100644 index 0000000000..cd33dd1f32 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/decorator/event-handler-class.decorator.ts @@ -0,0 +1,129 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +/* eslint-disable max-len, + @typescript-eslint/naming-convention, + prefer-arrow/prefer-arrow-functions, + prefer-arrow/prefer-arrow-functions, + prefer-arrow/prefer-arrow-functions, + space-before-function-paren, + @typescript-eslint/no-unsafe-member-access, + @typescript-eslint/no-unsafe-argument, + @typescript-eslint/ban-ts-comment, + @typescript-eslint/no-explicit-any, + @typescript-eslint/no-unsafe-assignment +*/ + +import 'reflect-metadata'; + +import { CollectionsUtil } from '../../../utils'; + +import { SystemEventMetadataRecords } from '../dispatcher/models'; +import { SystemEventHandlerRegistry } from '../dispatcher/registry'; + +import { SYSTEM_EVENTS_METADATA_KEY } from './models'; + +/** + * ** Decorator to annotate Handler source Class as SystemEvent Handler source Class. + * + * - Annotation should be add on top, before Class declaration starts, + * there should be no other annotations between this one and the Class declaration. + * - Annotation brings context from Object instance and make connection between Handler and the instance. + * - Take care for auto unsubscribe from HandlersRegistry for all Handlers from the Class on Object destroy, + * leveraging Angular ngOnDestroy lifecycle hook. + * - Annotation could be added only on Angular objects (like Services) to avoid memory leaks. + * - Decorator is evaluated on Class declaration and inside code is evaluated on Object/Instance creation. + * - For Dispatcher and Handler decorator read JSDoc {@link SystemEventDispatcher} {@link SystemEventHandler} + * + * @example + * + * `@Injectable()` + * `@SystemEventHandlerClass()` // immediately before Class declaration! + * export class DataJobService { + * // some properties, methods and handlers + * } + * + * + */ +export function SystemEventHandlerClass(): ClassDecorator { + // @ts-ignore + return InstanceType>(OriginConstructor: T): + (...args: ConstructorParameters) => InstanceType => { + + const originMetadataKeys = Reflect.getMetadataKeys(OriginConstructor); + const originMetadataRecords: SystemEventMetadataRecords = Reflect.getMetadata(SYSTEM_EVENTS_METADATA_KEY, OriginConstructor) ?? []; + const originOnDestroyRef: () => void = getOriginOnDestroyRef(OriginConstructor); + + let instance: InstanceType; + + const OverriddenConstructor = function(...args: ConstructorParameters): InstanceType { + instance = new OriginConstructor(...args); + + originMetadataRecords.forEach((r) => { + SystemEventHandlerRegistry.register>( + r.events, + r.handler, + instance, + r.filterExpression + ); + }); + + return instance; + }; + + OriginConstructor.prototype.ngOnDestroy = () => { + originMetadataRecords.forEach((r) => { + SystemEventHandlerRegistry.unregister( + r.events, + r.handler + ); + + if (originOnDestroyRef) { + originOnDestroyRef.call(instance); + } + }); + }; + + originMetadataKeys.forEach((key) => { + Reflect.defineMetadata(key, Reflect.getMetadata(key, OriginConstructor), OverriddenConstructor); + }); + + copyFromOriginToOverride(OriginConstructor, OverriddenConstructor); + + return OverriddenConstructor; + }; +} + +const getOriginOnDestroyRef = InstanceType>(classRef: T): () => void | null => { + if (!classRef || !classRef.prototype) { + return null; + } + + if (classRef.prototype.ngOnDestroy) { + return classRef.prototype.ngOnDestroy as (() => void); + } + + return getOriginOnDestroyRef(classRef.prototype); +}; + +const copyFromOriginToOverride = InstanceType>( + OriginConstructor: T, + OverriddenConstructor: (...args: ConstructorParameters) => InstanceType +) => { + + CollectionsUtil.iterateClassStatics(OriginConstructor, (descriptor, key) => { + const overriddenPropertyDescriptor = CollectionsUtil.getObjectPropertyDescriptor(OverriddenConstructor, key); + if (overriddenPropertyDescriptor && !overriddenPropertyDescriptor.writable) { + return; + } + + OverriddenConstructor[key] = descriptor.value; + }); + + OverriddenConstructor.prototype = Object.create(OriginConstructor.prototype); + OverriddenConstructor.prototype.constructor = OverriddenConstructor; +}; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/decorator/event-handler.decorator.spec.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/decorator/event-handler.decorator.spec.ts new file mode 100644 index 0000000000..7225ff7797 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/decorator/event-handler.decorator.spec.ts @@ -0,0 +1,68 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { SystemEventHandler } from './event-handler.decorator'; +import { SYSTEM_EVENTS_METADATA_KEY } from './models'; + +describe('SystemEventHandler', () => { + let event: string; + let event2: string; + // eslint-disable-next-line @typescript-eslint/ban-types + let constructor: Function; + // eslint-disable-next-line @typescript-eslint/ban-types + let target: { constructor: Function }; + let propertyKey: string; + let propertyKey2: string; + let descriptor: PropertyDescriptor; + let descriptor2: PropertyDescriptor; + + beforeEach(() => { + class TestClazz { + } + + event = 'SE_Register_User'; + event2 = 'SE_Unregister_User'; + constructor = TestClazz.prototype.constructor; + target = { constructor }; + propertyKey = '_eventHandlerRegister'; + propertyKey2 = 'eventHandleUnregister'; + descriptor = { value: () => Promise.resolve(true) }; + descriptor2 = { value: () => Promise.resolve(true) }; + }); + + it('should verify will record metadata with Reflect to Constructor', () => { + // Then 1 + expect(Reflect.hasMetadata(SYSTEM_EVENTS_METADATA_KEY, constructor)).toBeFalse(); + + // When + SystemEventHandler(event, null)(target, propertyKey, descriptor); + + // Then 2 + expect(Reflect.hasMetadata(SYSTEM_EVENTS_METADATA_KEY, constructor)).toBeTrue(); + expect(Reflect.getMetadata(SYSTEM_EVENTS_METADATA_KEY, constructor)).toEqual([{ + handler: descriptor.value, + events: event, + filterExpression: null + }]); + }); + + it('should verify will add metadata on existing with Reflect to Constructor', () => { + // Then 1 + expect(Reflect.hasMetadata(SYSTEM_EVENTS_METADATA_KEY, constructor)).toBeFalse(); + + // When + SystemEventHandler(event, null)(target, propertyKey, descriptor); + SystemEventHandler(event2, null)(target, propertyKey2, descriptor2); + + // Then 2 + expect(Reflect.hasMetadata(SYSTEM_EVENTS_METADATA_KEY, constructor)).toBeTrue(); + expect(Reflect.getMetadata(SYSTEM_EVENTS_METADATA_KEY, constructor)).toEqual([ + { handler: descriptor.value, events: event, filterExpression: null }, + { handler: descriptor2.value, events: event2, filterExpression: null } + ]); + }); +}); diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/decorator/event-handler.decorator.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/decorator/event-handler.decorator.ts new file mode 100644 index 0000000000..236b4a39fa --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/decorator/event-handler.decorator.ts @@ -0,0 +1,137 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +/* eslint-disable prefer-arrow/prefer-arrow-functions, + @typescript-eslint/naming-convention, + max-len +*/ + +import 'reflect-metadata'; + +import { Expression } from '../../../common'; + +import { SystemEvent } from '../event'; +import { SystemEventHandlerRef, SystemEventMetadataRecords } from '../dispatcher/models'; + +import { SYSTEM_EVENTS_METADATA_KEY } from './models'; + +/** + * ** Decorator to register method as SystemEvent Handler. + * + * - Annotation should be added before method declaration. + * - It will annotate method as Handler for provided SystemEvent or SystemEvents. + * - Optionally filter expression could be provide to filter Handler execution only when expression requirements are met. + * - Depends of the SystemEvent Handler should return Promise or not. + * - Blocking SystemEvents require Promise, Non-Blocking doesn't. + * - Recommended is every Handler to return Promise to avoid Runtime exception in signature change on Invoker side. + * - For Dispatcher and Handler Class decorator read JSDoc {@link SystemEventDispatcher} {@link SystemEventHandlerClass} + * + * @example + * + * `@Injectable()` + * `@SystemEventHandlerClass()` + * export class LoggerService { + * + * // some properties, methods + * + * `@SystemEventHandler('SE_Log')` // NON-BLOCKING SystemEvent + * public _logHandler(payload: { message: string }, eventId?: SystemEvent): void { + * // Do something + * } + * } + * + * `@Injectable` + * `@SystemEventHandlerClass()` + * export class DataJobService { + * constructor(private readonly httpClient: HttpClient) {} + * + * `@SystemEventHandler('SE_Create_Job')` // BLOCKING SystemEvent + * public _createJobHandler(payload: { baseUrl: string; jobName: string; jobCreator: string }): Promise { + * + * // Do something + * + * // Possibility to make asynchronous calls, + * // while Invoker is waiting until every Handler is executed, + * // and execution flow is returned + * return this + * .httpClient + * .post(`${baseUrl}/create`, {jobName, jobCreator}) + * .pipe( + * map((v) => !!v) // do some transformation and return boolean + * ) + * .toPromise(); // cast to Promise + * } + * } + * + * `@Injectable()` + * `@SystemEventHandlerClass()` + * export class DataJobManageService { + * constructor(private readonly httpClient: HttpClient) {} + * + * `@SystemEventHandler(['SE_Create_Job', 'SE_Update_Job'])` // BLOCKING SystemEvents + * public _createOrUpdateJobHandler(payload: { baseUrl: string; jobName: string; jobCreator?: string; data?: any }, eventId: SystemEvent): Promise { + * + * // Do something + * + * let url: string; + * let payloadData: { jobName: string; jobCreator?: string; data?: any } + * + * if (eventId === 'SE_Create_Job') { + * url = `${baseUrl}/create`; + * payloadData = { jobName, jobCreator }; + * } else { + * url = `${baseUrl}/update` + * payloadData = { jobName, data }; + * } + * + * // Possibility to make asynchronous calls, + * // while Invoker is waiting until every Handler is executed, + * // and execution flow is returned + * return this + * .httpClient + * .post(url, payloadData) + * .pipe( + * map((v) => !!v) // do some transformation and return boolean + * ) + * .toPromise(); // cast to Promise + * } + * } + * + * + */ +export function SystemEventHandler(knownEvents: SystemEvent | SystemEvent[], + filterExpression?: Expression): MethodDecorator { + return (target, propertyKey, descriptor) => { + if (!descriptor || !knownEvents) { + return; + } + + let metadataRecords: SystemEventMetadataRecords = []; + + if (Reflect.hasMetadata(SYSTEM_EVENTS_METADATA_KEY, target.constructor)) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + metadataRecords = Reflect.getMetadata(SYSTEM_EVENTS_METADATA_KEY, target.constructor); + metadataRecords.push({ + handler: (descriptor.value as unknown) as SystemEventHandlerRef, + events: knownEvents, + filterExpression + }); + + Reflect.defineMetadata(SYSTEM_EVENTS_METADATA_KEY, metadataRecords, target.constructor, propertyKey); + + return; + } + + metadataRecords.push({ + handler: (descriptor.value as unknown) as SystemEventHandlerRef, + events: knownEvents, + filterExpression + }); + + Reflect.defineMetadata(SYSTEM_EVENTS_METADATA_KEY, metadataRecords, target.constructor); + }; +} diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/decorator/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/decorator/index.ts new file mode 100644 index 0000000000..bbb4bbda79 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/decorator/index.ts @@ -0,0 +1,9 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './event-handler-class.decorator'; +export * from './event-handler.decorator'; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/decorator/models/event-decorator-helper.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/decorator/models/event-decorator-helper.ts new file mode 100644 index 0000000000..c9e64490e4 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/decorator/models/event-decorator-helper.ts @@ -0,0 +1,13 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * ** Key identifier for Taurus System Events Metadata on Class and Methods. + * + * + */ +export const SYSTEM_EVENTS_METADATA_KEY = '__taurus::shared::system::events::metadata__'; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/decorator/models/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/decorator/models/index.ts new file mode 100644 index 0000000000..ee7097320d --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/decorator/models/index.ts @@ -0,0 +1,8 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './event-decorator-helper'; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/dispatcher/event.dispatcher.spec.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/dispatcher/event.dispatcher.spec.ts new file mode 100644 index 0000000000..732e7729fd --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/dispatcher/event.dispatcher.spec.ts @@ -0,0 +1,140 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { fakeAsync, tick } from '@angular/core/testing'; + +import { SE_NAVIGATE } from '../event'; + +import { SystemEventHandlerRegistry } from './registry'; +import { SystemEventDispatcher } from './event.dispatcher'; +import { SystemEventHandlerRef } from './models'; + +describe('SystemEventDispatcher', () => { + describe('Statics::', () => { + describe('Methods::()', () => { + describe('|post|', () => { + it('should verify will post NON-BLOCKING System Event to 2 Handlers', fakeAsync(() => { + // Given + const spyHandler1: jasmine.SpyObj<{ evaluate: SystemEventHandlerRef }> = jasmine.createSpyObj('handler1', ['evaluate']); + const spyHandler2: jasmine.SpyObj<{ evaluate: SystemEventHandlerRef }> = jasmine.createSpyObj('handler2', ['evaluate']); + const spyRegistryPrepared = spyOn(SystemEventHandlerRegistry, 'getPreparedArrayOfHandlers').and.returnValue([ + { handlerRef: spyHandler1.evaluate, handlerClassInstance: spyHandler1 }, + { handlerRef: spyHandler2.evaluate, handlerClassInstance: spyHandler2 } + ]); + const spyRegistryFind = spyOn(SystemEventHandlerRegistry, 'findHandlerByReference').and.returnValue({ + handlerRef: () => Promise.resolve(true), + handlerRecord: { + handlerRef: () => Promise.resolve(true), + handlerClassInstance: null, + handlerFilterExpression: null + }, + active: true + }); + const payload = {}; + + // When + SystemEventDispatcher.post(SE_NAVIGATE, payload); + + // Then + tick(100); + expect(spyRegistryPrepared).toHaveBeenCalled(); + expect(spyHandler1.evaluate).toHaveBeenCalledWith(payload, SE_NAVIGATE); + expect(spyHandler2.evaluate).toHaveBeenCalledWith(payload, SE_NAVIGATE); + expect(spyRegistryFind).toHaveBeenCalledTimes(2); + })); + + it('should verify will post NON-BLOCKING System Event to 1 Handler and will skip the second one', fakeAsync(() => { + // Given + const spyHandler1: jasmine.SpyObj<{ evaluate: SystemEventHandlerRef }> = jasmine.createSpyObj('handler1', ['evaluate']); + const spyHandler2: jasmine.SpyObj<{ evaluate: SystemEventHandlerRef }> = jasmine.createSpyObj('handler2', ['evaluate']); + const spyRegistryPrepared = spyOn(SystemEventHandlerRegistry, 'getPreparedArrayOfHandlers').and.returnValue([ + { handlerRef: spyHandler1.evaluate, handlerClassInstance: spyHandler1 }, + { handlerRef: spyHandler2.evaluate, handlerClassInstance: spyHandler2 } + ]); + const spyRegistryFind = spyOn(SystemEventHandlerRegistry, 'findHandlerByReference').and.returnValue({ + handlerRef: () => Promise.resolve(true), + handlerRecord: { + handlerRef: () => Promise.resolve(true), + handlerClassInstance: null, + handlerFilterExpression: null + }, + active: true + }); + const payload = {}; + + // When + SystemEventDispatcher.post(SE_NAVIGATE, payload, 1); + + // Then + tick(100); + expect(spyRegistryPrepared).toHaveBeenCalled(); + expect(spyHandler1.evaluate).toHaveBeenCalledWith(payload, SE_NAVIGATE); + expect(spyHandler2.evaluate).not.toHaveBeenCalled(); + expect(spyRegistryFind).toHaveBeenCalledTimes(1); + })); + }); + + describe('|send|', () => { + it('should verify will send BLOCKING System Event to 2 Handlers', fakeAsync(() => { + // Given + const spyHandler1: jasmine.SpyObj<{ evaluate: SystemEventHandlerRef }> = jasmine.createSpyObj('handler1', ['evaluate']); + const spyHandler2: jasmine.SpyObj<{ evaluate: SystemEventHandlerRef }> = jasmine.createSpyObj('handler2', ['evaluate']); + spyHandler1.evaluate.and.returnValue(Promise.resolve(true)); + spyHandler2.evaluate.and.returnValue(Promise.resolve(true)); + + const spyRegistryPrepared = spyOn(SystemEventHandlerRegistry, 'getPreparedArrayOfHandlers').and.returnValue([ + { handlerRef: spyHandler1.evaluate, handlerClassInstance: spyHandler1 }, + { handlerRef: spyHandler2.evaluate, handlerClassInstance: spyHandler2 } + ]); + const payload = {}; + + // When + SystemEventDispatcher + .send(SE_NAVIGATE, payload) + .then(() => { + // No-op. + }); + + tick(100); + + // Then + expect(spyRegistryPrepared).toHaveBeenCalled(); + expect(spyHandler1.evaluate).toHaveBeenCalledWith(payload, SE_NAVIGATE); + expect(spyHandler2.evaluate).toHaveBeenCalledWith(payload, SE_NAVIGATE); + })); + + it('should verify will send BLOCKING System Event to 1 Handler and will skip the second one', fakeAsync(() => { + // Given + const spyHandler1: jasmine.SpyObj<{ evaluate: SystemEventHandlerRef }> = jasmine.createSpyObj('handler1', ['evaluate']); + const spyHandler2: jasmine.SpyObj<{ evaluate: SystemEventHandlerRef }> = jasmine.createSpyObj('handler2', ['evaluate']); + spyHandler1.evaluate.and.returnValue(Promise.resolve(true)); + spyHandler2.evaluate.and.returnValue(Promise.resolve(true)); + + const spyRegistryPrepared = spyOn(SystemEventHandlerRegistry, 'getPreparedArrayOfHandlers').and.returnValue([ + { handlerRef: spyHandler1.evaluate, handlerClassInstance: spyHandler1 }, + { handlerRef: spyHandler2.evaluate, handlerClassInstance: spyHandler2 } + ]); + const payload = {}; + + // When + SystemEventDispatcher + .send(SE_NAVIGATE, payload, 1) + .then(() => { + // No-op. + }); + + tick(100); + + // Then + expect(spyRegistryPrepared).toHaveBeenCalled(); + expect(spyHandler1.evaluate).not.toHaveBeenCalled(); + expect(spyHandler2.evaluate).toHaveBeenCalledWith(payload, SE_NAVIGATE); + })); + }); + }); + }); +}); diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/dispatcher/event.dispatcher.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/dispatcher/event.dispatcher.ts new file mode 100644 index 0000000000..37d10651d4 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/dispatcher/event.dispatcher.ts @@ -0,0 +1,157 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import { CollectionsUtil } from '../../../utils'; + +import { SE_ALL_EVENTS, SystemEvent, SystemEventComparable } from '../event'; +import { SystemEventHandlerRegistry } from './registry'; +import { SystemEventHandlerRecord } from './models'; + +/** + * ** System Event Dispatcher. + * + * - This Class is start point to trigger SystemEvent. + * - SystemEvent identifier is in string format, and would be nice those string to be assigned to constant for easier usage. + * - Payload could be any format but preferable is Literal Object. + * - SystemEvents could be dispatched in two ways using implemented behavior: + * + * 1) POST - this is NON-BLOCKING execution that leverages `setTimeout(handler, 0)` to avoid blocking main JavaScript thread. + * + * 2) SEND - this is BLOCKING execution that leverages `Promise` mechanism to chain Handlers execution. At the end returns + * flow of execution to Invoker. + * + * - Handlers and their usage read at Decorators JSDoc {@link SystemEventHandlerClass} {@link SystemEventHandler}. + * + * @see SystemEventDispatcher.post + * @see SystemEventDispatcher.send + * + * @example + * // void + * SystemEventDispatcher + * .post('SE_Send_Email', { email: 'join-versatiledatakit@groups.vmware.com }); + * + * // returns Promise + * SystemEventDispatcher + * .send('SE_Create_Job', { jobName: 'Test', jobCreator: 'System' }) + * .then((v) => { + * // Do something + * }); + * + * + */ +export class SystemEventDispatcher { + /** + * ** Method to post System Event. + * + * - NON-BLOCKING execution. + * - Execution is non-blocking, e.g. it is executed in queue using setTimeout of 0. + * - If third parameters is provided it will execute as many handlers as it is request. + * - e.g. 1 or 2 or N and will skip the others. + */ + static post(eventId: SystemEvent, payload: any, handlersToExecute: number = null) { + const preparedHandlers = SystemEventHandlerRegistry.getPreparedArrayOfHandlers(eventId); + + let executedHandlers = 0; + + for (const handlerRecord of preparedHandlers) { + if (!SystemEventDispatcher.executeExpressionFilter(handlerRecord, eventId, payload)) { + return; + } + + executedHandlers++; + + const isHandlerActive = (e: SystemEvent) => SystemEventHandlerRegistry.findHandlerByReference( + e, + handlerRecord.handlerRef, + handlerRecord.handlerClassInstance + ).active; + + setTimeout(() => { + if (!isHandlerActive(eventId) && !isHandlerActive(SE_ALL_EVENTS)) { + return; + } + + handlerRecord.handlerRef.call(handlerRecord.handlerClassInstance, payload, eventId); + }, 0); + + if (CollectionsUtil.isNumber(handlersToExecute) && executedHandlers >= handlersToExecute) { + break; + } + } + } + + /** + * ** Method to send System Event. + * + * - BLOCKING execution. + * - Execution is blocking, e.g. it is achieved using Promises, and every handler must return Promise. + * - If third parameters is provided it will execute as many handlers as it is request. + * - e.g. 1 or 2 or N and will return the flow to the invoker. + */ + static send(eventId: SystemEvent, payload: any, handlersToExecute: number = null): Promise { + const preparedHandlers = SystemEventHandlerRegistry.getPreparedArrayOfHandlers(eventId, true); + + return SystemEventDispatcher.executeSendCommand( + preparedHandlers, + eventId, + payload, + handlersToExecute + ); + } + + /** + * ** Execute send command and handle Promises from handlers. + */ + private static executeSendCommand(handlers: SystemEventHandlerRecord[], + eventId: SystemEvent, + payload: any, + handlersToExecute: number = null, + executedHandlers = 0): Promise { + + if (!handlers.length) { + return Promise.resolve(true); + } + + if (CollectionsUtil.isNumber(handlersToExecute) && handlersToExecute === executedHandlers) { + return Promise.resolve(true); + } + + const handlerForExecution = handlers.pop(); + + if (!SystemEventDispatcher.executeExpressionFilter(handlerForExecution, eventId, payload)) { + return SystemEventDispatcher.executeSendCommand(handlers, eventId, payload, handlersToExecute, executedHandlers); + } + + // eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-return + return handlerForExecution.handlerRef + .call(handlerForExecution.handlerClassInstance, payload, eventId) + .then(() => SystemEventDispatcher + .executeSendCommand(handlers, eventId, payload, handlersToExecute, executedHandlers + 1)) + .catch(() => Promise.reject(false)); + } + + /** + * ** Execute Expression filter for Handler before execution. + */ + private static executeExpressionFilter(handlerRecord: SystemEventHandlerRecord, + eventId: SystemEvent, + payload: any): boolean { + + const filterExpression = handlerRecord.handlerFilterExpression; + + if (CollectionsUtil.isNil(filterExpression)) { + return true; + } + + return handlerRecord.handlerFilterExpression.evaluate( + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + SystemEventComparable.of({ eventId, payload }) + ); + } +} diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/dispatcher/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/dispatcher/index.ts new file mode 100644 index 0000000000..58f53223ee --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/dispatcher/index.ts @@ -0,0 +1,8 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './event.dispatcher'; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/dispatcher/models/event-dispatcher.model.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/dispatcher/models/event-dispatcher.model.ts new file mode 100644 index 0000000000..71de77cc95 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/dispatcher/models/event-dispatcher.model.ts @@ -0,0 +1,45 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import { Expression } from '../../../../common'; + +import { SystemEvent } from '../../event'; + +export type SystemEventMetadataRecord = { + handler: SystemEventHandlerRef; + events: SystemEvent | SystemEvent[]; + filterExpression: Expression; +}; + +/** + * ** Type for one Handler interface. + */ +export type SystemEventHandlerRef = (payload: any, eventId?: SystemEvent) => Promise; + +/** + * ** Event Handler record in registry. + */ +export type SystemEventHandlerRecord = { + /** + * ** Reference to Handler instance. + */ + handlerRef: SystemEventHandlerRef; + + /** + * ** Reference to the object where Handler belongs, for context purpose. + */ + handlerClassInstance?: Record; + + /** + * ** Expression that filters if some Event should be dispatch to the Handler reference or not. + */ + handlerFilterExpression?: Expression; +}; +export type SystemEventHandlerFindByRef = { active: boolean; handlerRecord: SystemEventHandlerRecord; handlerRef: SystemEventHandlerRef }; +export type SystemEventMetadataRecords = SystemEventMetadataRecord[]; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/dispatcher/models/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/dispatcher/models/index.ts new file mode 100644 index 0000000000..ebacdda6a0 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/dispatcher/models/index.ts @@ -0,0 +1,8 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './event-dispatcher.model'; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/dispatcher/registry/event-handler.registry.spec.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/dispatcher/registry/event-handler.registry.spec.ts new file mode 100644 index 0000000000..3dc86ff736 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/dispatcher/registry/event-handler.registry.spec.ts @@ -0,0 +1,251 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { SystemEventHandlerRegistry } from './event-handler.registry'; + +class TestClazz { +} + +describe('SystemEventHandlerRegistry', () => { + describe('Statics::', () => { + describe('Getters::()', () => { + describe('|instance|', () => { + it('should verify will return always same Repository instance', () => { + // When + const r1 = SystemEventHandlerRegistry.instance; + const r2 = SystemEventHandlerRegistry.instance; + const r3 = SystemEventHandlerRegistry.instance; + + // Then + expect(r2).toBe(r1); + expect(r3).toBe(r2); + }); + }); + }); + + describe('Methods::()', () => { + describe('|register|', () => { + it('should verify will execute register and execute methods in Registry instance', () => { + // Given + // @ts-ignore + const spyRegister = spyOn(SystemEventHandlerRegistry.instance, 'register').and.callThrough(); + // @ts-ignore + const spyExecute = spyOn(SystemEventHandlerRegistry.instance, 'execute').and.callThrough(); + const handler = () => Promise.resolve(true); + const handlerClassInstance = {}; + + // When + SystemEventHandlerRegistry.register( + 'SE_Send_Email', + handler, + handlerClassInstance + ); + + // Then + // @ts-ignore + expect(spyRegister).toHaveBeenCalledWith('SE_Send_Email', handler, handlerClassInstance, null); + // @ts-ignore + expect(spyExecute).toHaveBeenCalledWith('SE_Send_Email', handler, jasmine.any(Function), handlerClassInstance, null); + }); + + it('should verify will register handler in Registry', () => { + // Given + const event = 'SE_Create_Job'; + const handlerRef = () => Promise.resolve(false); + const clazzInstance = new TestClazz(); + + // When + SystemEventHandlerRegistry.register( + event, + handlerRef, + clazzInstance + ); + + // Then + // eslint-disable-next-line @typescript-eslint/dot-notation + const registry = SystemEventHandlerRegistry.instance['handlers']; + const handlers = registry.get(event); + expect(Array.from(handlers.values())).toContain({ + handlerRef, + handlerFilterExpression: null, + handlerClassInstance: clazzInstance + }); + }); + }); + + describe('|unregister|', () => { + it('should verify will execute unregister and execute methods in Registry instance', () => { + // Given + // @ts-ignore + const spyUnregister = spyOn(SystemEventHandlerRegistry.instance, 'unregister').and.callThrough(); + // @ts-ignore + const spyExecute = spyOn(SystemEventHandlerRegistry.instance, 'execute').and.callThrough(); + const event = 'SE_Delete_Job'; + const handler = () => Promise.resolve(true); + + // When + SystemEventHandlerRegistry.unregister( + event, + handler + ); + + // Then + // @ts-ignore + expect(spyUnregister).toHaveBeenCalledWith(event, handler); + // @ts-ignore + expect(spyExecute).toHaveBeenCalledWith(event, handler, jasmine.any(Function)); + }); + + it('should verify will remove handler in Registry after it is added', () => { + // Given + // eslint-disable-next-line @typescript-eslint/dot-notation + const registry = SystemEventHandlerRegistry.instance['handlers']; + const event = 'SE_Update_Job'; + const handlerRef = () => Promise.resolve(true); + const clazzInstance = new TestClazz(); + + // When 1 + SystemEventHandlerRegistry.register( + event, + handlerRef, + clazzInstance + ); + + // Then 1 + const handlers = registry.get(event); + expect(Array.from(handlers.values())).toContain({ + handlerRef, + handlerFilterExpression: null, + handlerClassInstance: clazzInstance + }); + + // When 2 + SystemEventHandlerRegistry.unregister(event, handlerRef); + + // Then 2 + // eslint-disable-next-line @typescript-eslint/dot-notation + expect(Array.from(handlers.values())).not.toContain({ + handlerRef, + handlerFilterExpression: null, + handlerClassInstance: clazzInstance + }); + }); + }); + + describe('|findHandlerByReference|', () => { + it('should verify will find Handler by its reference', () => { + // Given + const event = 'SE_Notify_Users'; + const handlerRef = () => Promise.resolve(true); + const clazzInstance = new TestClazz(); + + // When + SystemEventHandlerRegistry.register( + event, + handlerRef, + clazzInstance + ); + const record = SystemEventHandlerRegistry.findHandlerByReference(event, handlerRef, clazzInstance); + + // Then + expect(record).toEqual({ + active: true, + handlerRef, + handlerRecord: { + handlerRef, + handlerFilterExpression: null, + handlerClassInstance: clazzInstance + } + }); + }); + }); + + describe('|getPreparedArrayOfHandlers|', () => { + it('should verify will return empty Array when there is no Handlers for Event', () => { + // Given + const event = 'SE_Clean_Jobs'; + + // When + const handlers = SystemEventHandlerRegistry.getPreparedArrayOfHandlers(event); + + // Then + expect(handlers).toEqual([]); + }); + + it('should verify will return 3 handlers for given Event', () => { + // Given + const event = 'SE_Log_Incident'; + const handlerRef1 = () => Promise.resolve(true); + const handlerRef2 = () => Promise.resolve(true); + const handlerRef3 = () => Promise.resolve(true); + const clazzInstance = new TestClazz(); + + SystemEventHandlerRegistry.register( + event, + handlerRef1, + clazzInstance + ); + SystemEventHandlerRegistry.register( + event, + handlerRef2, + clazzInstance + ); + SystemEventHandlerRegistry.register( + event, + handlerRef3, + clazzInstance + ); + + // When + const handlers = SystemEventHandlerRegistry.getPreparedArrayOfHandlers(event); + + // Then + expect(handlers).toEqual([ + { handlerRef: handlerRef1, handlerClassInstance: clazzInstance, handlerFilterExpression: null }, + { handlerRef: handlerRef2, handlerClassInstance: clazzInstance, handlerFilterExpression: null }, + { handlerRef: handlerRef3, handlerClassInstance: clazzInstance, handlerFilterExpression: null } + ]); + }); + + it('should verify will return 3 handlers in reverse order for given Event', () => { + // Given + const event = 'SE_Clean_Session'; + const handlerRef11 = () => Promise.resolve(true); + const handlerRef22 = () => Promise.resolve(true); + const handlerRef33 = () => Promise.resolve(true); + const clazzInstance = new TestClazz(); + + SystemEventHandlerRegistry.register( + event, + handlerRef11, + clazzInstance + ); + SystemEventHandlerRegistry.register( + event, + handlerRef22, + clazzInstance + ); + SystemEventHandlerRegistry.register( + event, + handlerRef33, + clazzInstance + ); + + // When + const handlers = SystemEventHandlerRegistry.getPreparedArrayOfHandlers(event, true); + + // Then + expect(handlers).toEqual([ + { handlerRef: handlerRef33, handlerClassInstance: clazzInstance, handlerFilterExpression: null }, + { handlerRef: handlerRef22, handlerClassInstance: clazzInstance, handlerFilterExpression: null }, + { handlerRef: handlerRef11, handlerClassInstance: clazzInstance, handlerFilterExpression: null } + ]); + }); + }); + }); + }); +}); diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/dispatcher/registry/event-handler.registry.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/dispatcher/registry/event-handler.registry.ts new file mode 100644 index 0000000000..5c0273bb04 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/dispatcher/registry/event-handler.registry.ts @@ -0,0 +1,258 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import { CollectionsUtil } from '../../../../utils'; + +import { Expression } from '../../../../common'; + +import { SE_ALL_EVENTS, SystemEvent } from '../../event'; +import { SystemEventHandlerFindByRef, SystemEventHandlerRecord, SystemEventHandlerRef } from '../models'; + +/** + * ** Registry for Events Handlers. + * + * + */ +export class SystemEventHandlerRegistry { + /** + * ** Returns Singleton instance. + */ + static get instance(): SystemEventHandlerRegistry { + if (CollectionsUtil.isNil(SystemEventHandlerRegistry._instance)) { + SystemEventHandlerRegistry._instance = new SystemEventHandlerRegistry(); + } + + return SystemEventHandlerRegistry._instance; + } + + private static _instance: SystemEventHandlerRegistry; + + private readonly handlers: Map> = new Map>(); + + /** + * ** Constructor. + * + * - Private constructor to make it Singleton. + */ + private constructor() { + // No-op. + } + + /** + * ** Register Handler for SystemEvent/s. + */ + static register(knownEvents: SystemEvent | SystemEvent[], + handlerRef: SystemEventHandlerRef, + handlerClassInstance: T, + handlerFilterExpression: Expression = null): boolean { + return SystemEventHandlerRegistry + .instance + .register( + knownEvents, + handlerRef, + handlerClassInstance, + handlerFilterExpression + ); + } + + /** + * ** Unregister Handler from registry. + * + * - It should be done in ngOnDestroy() method in Services/Components to avoid potential memory leaks. + */ + static unregister(knownEvents: SystemEvent | SystemEvent[], handlerRef: SystemEventHandlerRef): boolean { + return SystemEventHandlerRegistry + .instance + .unregister(knownEvents, handlerRef); + } + + /** + * ** Find Handler by reference looking through the Registry. + */ + static findHandlerByReference(eventId: SystemEvent, + handlerRef: SystemEventHandlerRef, + handlerClassInstance?: any): SystemEventHandlerFindByRef { + return SystemEventHandlerRegistry + .instance + .findHandlerByReference(eventId, handlerRef, handlerClassInstance); + } + + /** + * ** Prepare array of handlers for execution of post or send. + */ + static getPreparedArrayOfHandlers(eventId: SystemEvent, reversed = false): SystemEventHandlerRecord[] { + return SystemEventHandlerRegistry + .instance + .getPreparedArrayOfHandlers(eventId, reversed); + } + + private register(knownEvents: SystemEvent | SystemEvent[], + handlerRef: SystemEventHandlerRef, + handlerClassInstance: T, + handlerFilterExpression: Expression = null): boolean { + + return this.execute( + knownEvents, + handlerRef, + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + this.executeRegister.bind(this), + handlerClassInstance, + handlerFilterExpression + ); + } + + private unregister(knownEvents: SystemEvent | SystemEvent[], handlerRef: SystemEventHandlerRef): boolean { + return this.execute( + knownEvents, + handlerRef, + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + this.executeUnregister.bind(this) + ); + } + + private findHandlerByReference(eventId: SystemEvent, + handlerRef: SystemEventHandlerRef, + handlerClassInstance?: any): SystemEventHandlerFindByRef { + + const handlers = this.handlers.has(eventId) + ? this.handlers.get(eventId) + : new Set(); + + const handlerRecord = Array.from(handlers.values()).find((r) => { + let isEqual = r.handlerRef === handlerRef; + + if (handlerClassInstance) { + isEqual = isEqual && (r.handlerClassInstance === handlerClassInstance); + } + + return isEqual; + }); + + return { + active: CollectionsUtil.isDefined(handlerRecord), + handlerRecord, + handlerRef: CollectionsUtil.isDefined(handlerRecord) + ? handlerRecord.handlerRef + : undefined + }; + } + + private getPreparedArrayOfHandlers(eventId: SystemEvent, reversed = false): SystemEventHandlerRecord[] { + const specialHandlers: SystemEventHandlerRecord[] = this.getSpecialHandlers(); + + if (!this.handlers.has(eventId)) { + return specialHandlers; + } + + return reversed + ? Array.from(this.handlers.get(eventId).values()).concat(specialHandlers).reverse() + : Array.from(this.handlers.get(eventId).values()).concat(specialHandlers); + } + + /** + * ** Generic abstraction for Register and Unregister handlers. + */ + private execute(knownEvents: SystemEvent | SystemEvent[], + handlerRef: SystemEventHandlerRef, + executeMethodRef: (...arg: any[]) => any, + handlerClassInstance?: any, + handlerFilterExpression?: Expression): boolean { + + const preparedData = this.prepareEventNames(knownEvents); + + if (!preparedData.status || !CollectionsUtil.isFunction(handlerRef)) { + return false; + } + + try { + preparedData.eventNames.forEach((eventId) => { + executeMethodRef( + eventId, + handlerRef, + handlerClassInstance, + handlerFilterExpression + ); + }); + + return true; + } catch (_e) { + return false; + } + } + + /** + * ** Evaluate Handler register. + */ + private executeRegister(eventId: SystemEvent, + handlerRef: SystemEventHandlerRef, + handlerClassInstance: any, + handlerFilterExpression: Expression): boolean { + + if (!this.handlers.has(eventId)) { + this.handlers.set(eventId, new Set()); + } + + if (this.findHandlerByReference(eventId, handlerRef, handlerClassInstance).active) { + return false; + } + + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + this.handlers.get(eventId).add({ handlerRef, handlerClassInstance, handlerFilterExpression }); + + return true; + } + + /** + * ** Evaluate Handler unregister. + */ + private executeUnregister(eventId: SystemEvent, handlerRef: SystemEventHandlerRef): boolean { + if (!this.handlers.has(eventId)) { + return false; + } + + const findHandlerResponse = this.findHandlerByReference(eventId, handlerRef); + + if (!findHandlerResponse.active) { + return false; + } + + this.handlers.get(eventId).delete(findHandlerResponse.handlerRecord); + + return true; + } + + /** + * ** Prepares Event names in Array of strings format. + */ + private prepareEventNames(knownEvents: SystemEvent | SystemEvent[]): { status: boolean; eventNames?: SystemEvent[] } { + if (!CollectionsUtil.isString(knownEvents) && !CollectionsUtil.isArray(knownEvents)) { + return { + status: false + }; + } + + return { + status: true, + eventNames: CollectionsUtil.isString(knownEvents) + ? [knownEvents] + : knownEvents + }; + } + + /** + * ** Returns special Handlers. + */ + private getSpecialHandlers(): SystemEventHandlerRecord[] { + if (!this.handlers.has(SE_ALL_EVENTS)) { + return []; + } + + return Array.from(this.handlers.get(SE_ALL_EVENTS).values()); + } +} diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/dispatcher/registry/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/dispatcher/registry/index.ts new file mode 100644 index 0000000000..a7bd46eb94 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/dispatcher/registry/index.ts @@ -0,0 +1,8 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './event-handler.registry'; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/event/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/event/index.ts new file mode 100644 index 0000000000..af30aa746b --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/event/index.ts @@ -0,0 +1,8 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './models'; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/event/models/event-filter.expression.spec.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/event/models/event-filter.expression.spec.ts new file mode 100644 index 0000000000..796b81c9a7 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/event/models/event-filter.expression.spec.ts @@ -0,0 +1,147 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Comparable, ComparableImpl, Equal, Predicate } from '../../../../common'; + +import { SystemEventFilterExpression } from './event-filter.expression'; + +describe('SystemEventFilterExpression', () => { + let v1: any; + let v2: any; + let v3: any; + let v4: any; + + let c1: Comparable; + let c2: Comparable; + let c3: Comparable; + let c4: Comparable; + + let p1: Predicate; + let p2: Predicate; + let p3: Predicate; + let p4: Predicate; + + beforeEach(() => { + v1 = 'Taurus'; + v2 = 'Taurus'; + v3 = 'VDK'; + v4 = 'Taurus'; + + c1 = ComparableImpl.of(v1); + c2 = ComparableImpl.of(v2); + c3 = ComparableImpl.of(v3); + c4 = ComparableImpl.of(v4); + + p1 = Equal.of(c1); + p2 = Equal.of(c2); + p3 = Equal.of(c3); + p4 = Equal.of(c4); + }); + + it('should verify instance is created', () => { + // When + const r = new SystemEventFilterExpression(p1); + + // Then + expect(r).toBeDefined(); + }); + + it('should verify Predicates are correctly assigned', () => { + // When + const r = new SystemEventFilterExpression(p1, p2, p3); + + // Then + expect(r.predicates).toEqual([p1, p2, p3]); + }); + + it('should verify is no Predicates given will create empty Expression', () => { + // When + const r1 = SystemEventFilterExpression.of(); + const r2 = new SystemEventFilterExpression(); + + // Then + expect(r1).toBeDefined(); + expect(r1.predicates).toEqual([]); + expect(r2).toBeDefined(); + expect(r2.predicates).toEqual([]); + }); + + describe('Statics::', () => { + describe('Methods::()', () => { + describe('|of|', () => { + it('should verify factory method will create instance', () => { + // When + const r = SystemEventFilterExpression.of(p1, p2, p3, p4); + + // Then + expect(r).toBeDefined(); + expect(r).toBeInstanceOf(SystemEventFilterExpression); + expect(r.predicates).toEqual([p1, p2, p3, p4]); + }); + }); + }); + }); + + describe('Methods::()', () => { + describe('|addPredicate|', () => { + it('should verify will add new Predicates to existing one', () => { + // Given + const e = SystemEventFilterExpression.of(p1); + + // Then 1 + expect(e.predicates).toEqual([p1]); + + // When + e.addPredicate(p3, p4); + e.addPredicate(p2); + + // Then 2 + expect(e.predicates).toEqual([p1, p3, p4, p2]); + }); + }); + + describe('|hasPredicates|', () => { + it('should verify will return true if there is Predicates in Expression, otherwise false', () => { + // Given + const e1 = SystemEventFilterExpression.of(); + const e2 = SystemEventFilterExpression.of(p1, p2); + + // When + const r1 = e1.hasPredicates(); + const r2 = e2.hasPredicates(); + + // Then + expect(r1).toBeFalse(); + expect(r2).toBeTrue(); + }); + }); + + describe('|evaluate|', () => { + it('should verify will return true only when all predicates return true', () => { + // Given + const e = SystemEventFilterExpression.of(p1, p2); + + // When + const r = e.evaluate(c4); + + // Then + expect(r).toBeTrue(); + }); + + it('should verify will return false if one of the Predicates return false', () => { + // Given + const e = SystemEventFilterExpression.of(p1, p2, p3); + + // When + const r = e.evaluate(c4); + + // Then + expect(r).toBeFalse(); + }); + }); + }); +}); diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/event/models/event-filter.expression.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/event/models/event-filter.expression.ts new file mode 100644 index 0000000000..4e3c27f65d --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/event/models/event-filter.expression.ts @@ -0,0 +1,56 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Comparable, Expression, Predicate } from '../../../../common'; + +/** + * ** System Event Filter Expression that evaluates if some Handler should be executed or no. + * + * + */ +export class SystemEventFilterExpression implements Expression { + /** + * @inheritDoc + */ + public readonly predicates: Predicate[]; + + /** + * ** Constructor. + */ + constructor(...predicates: Predicate[]) { + this.predicates = predicates ?? []; + } + + /** + * ** Factory method. + */ + static of(...predicates: Predicate[]): SystemEventFilterExpression { + return new SystemEventFilterExpression(...predicates); + } + + /** + * ** Add Predicates to Expression. + */ + addPredicate(...predicates: Predicate[]): void { + this.predicates.push(...predicates); + } + + /** + * ** Returns value that reflects if there is any Predicate inside the Expression (SystemEventFilter). + */ + hasPredicates(): boolean { + return this.predicates.length > 0; + } + + /** + * @inheritDoc + */ + evaluate(comparable?: Comparable): boolean { + return this.predicates + .every((p) => p.evaluate(comparable)); + } +} diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/event/models/event-helper.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/event/models/event-helper.ts new file mode 100644 index 0000000000..b7c1d54424 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/event/models/event-helper.ts @@ -0,0 +1,8 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +export type SystemEvent = string; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/event/models/event.codes.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/event/models/event.codes.ts new file mode 100644 index 0000000000..f57106b0fa --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/event/models/event.codes.ts @@ -0,0 +1,28 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * ** System Event ID for navigation trigger. + * + * - Send event [BLOCKING] + * - Every Handler should return Promise. + * + * - Payload {url: string | string[], extras?: NavigationExtras} + */ +export const SE_NAVIGATE = 'SE_Navigate'; + +/** + * ** System Event that could be consumed by Handlers. + *

+ * - Must return a Promise! + * + * - Handlers will listen for all Events in the System. + * - They could be BLOCKING and NON-BLOCKING. + * + * - Payload {any} + */ +export const SE_ALL_EVENTS = '*'; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/event/models/event.comparable.spec.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/event/models/event.comparable.spec.ts new file mode 100644 index 0000000000..3be2b233f4 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/event/models/event.comparable.spec.ts @@ -0,0 +1,166 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Comparable } from '../../../../common/interfaces'; + +import { SystemEventComparable } from './event.comparable'; + +class ComparableStub implements Comparable { + public readonly value: any; + + constructor(value: any) { + this.value = value; + } + + compare(_comparable: Comparable): number { + return 0; + } + + equal(_comparable: Comparable): boolean { + return true; + } + + like(_comparable: Comparable): boolean { + return true; + } + + notEqual(_comparable: Comparable): boolean { + return false; + } + + isNil(): boolean { + return false; + } + + notNil(): boolean { + return true; + } + + greaterThan(_comparable: Comparable): boolean { + return false; + } + + greaterThanInclusive(_comparable: Comparable): boolean { + return false; + } + + lessThan(_comparable: Comparable): boolean { + return false; + } + + lessThanInclusive(_comparable: Comparable): boolean { + return false; + } +} + +describe('SystemEventComparable', () => { + it('should verify instance is created', () => { + // Given + const eventId = 'VDK'; + const payload = {}; + + // When + const instance = new SystemEventComparable({ eventId, payload }); + + // Then + expect(instance).toBeDefined(); + }); + + it('should verify value is correctly assigned', () => { + // Given + const eventId = 'VDK'; + const payload = {}; + const value = { eventId, payload }; + + // When + const instance = new SystemEventComparable(value); + + // Then + expect(instance.value).toBe(value); + }); + + describe('Statics::', () => { + describe('Methods::()', () => { + describe('|of|', () => { + it('should verify factory method will create instance', () => { + // Given + const eventId = 'VDK'; + const payload = {}; + const value = { eventId, payload }; + + // When + const instance = SystemEventComparable.of(value); + + // Then + expect(instance).toBeDefined(); + expect(instance).toBeInstanceOf(SystemEventComparable); + }); + }); + }); + }); + + describe('Methods::()', () => { + let v1: { eventId: string; payload: unknown }; + let v2: { eventId: string; payload: unknown }; + let v3: { eventId: string; payload: unknown }; + let v4: { eventId: string; payload: unknown }; + + let c1: SystemEventComparable; + let c2: SystemEventComparable; + let c3: SystemEventComparable; + let c4: SystemEventComparable; + + beforeEach(() => { + v1 = { eventId: 'Taurus', payload: { t: 10 } }; + v2 = { eventId: 'VDK', payload: { t: 100 } }; + v3 = { eventId: 'Test', payload: { t: 30 } }; + v4 = { eventId: 'Taurus', payload: { t: 10 } }; + + c1 = SystemEventComparable.of(v1); + c2 = SystemEventComparable.of(v2); + c3 = SystemEventComparable.of(v3); + c4 = SystemEventComparable.of(v4); + }); + + describe('|compare|', () => { + it('should verify will return 0 for equal', () => { + // When + const comparison = c1.compare(c4); + + // Then + expect(comparison).toEqual(0); + }); + + it('should verify will return -1 for greaterThan', () => { + // When + const comparison = c2.compare(c1); + + // Then + expect(comparison).toEqual(-1); + }); + + it('should verify will return -1 for lessThan', () => { + // When + const comparison = c2.compare(c3); + + // Then + expect(comparison).toEqual(-1); + }); + + it('should verify will return -1 given comparable is not instance of the current Constructor', () => { + // Given + const c10 = new ComparableStub(v2); + + // When + const comparison = c1.compare(c10); + + // Then + expect(comparison).toEqual(-1); + }); + }); + }); +}); diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/event/models/event.comparable.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/event/models/event.comparable.ts new file mode 100644 index 0000000000..8d33d31100 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/event/models/event.comparable.ts @@ -0,0 +1,50 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Comparable, ComparableImpl } from '../../../../common'; + +import { CollectionsUtil } from '../../../../utils'; + +import { SystemEvent } from './event-helper'; + +/** + * @inheritDoc + * + * + */ +export class SystemEventComparable extends ComparableImpl<{ eventId: SystemEvent; payload: unknown }> { + /** + * ** Constructor. + */ + constructor(value: { eventId: string; payload: unknown }) { + super(value); + } + + /** + * ** Factory method. + */ + static override of(value: { eventId: string; payload: unknown }): SystemEventComparable { + return new SystemEventComparable(value); + } + + /** + * @inheritDoc + */ + override compare(comparable: Comparable): number { + if (comparable instanceof SystemEventComparable) { + const evaluateSecondStatement = () => this.value.payload > comparable.value.payload ? + 1 : + -1; + + return CollectionsUtil.isEqual(this.value.payload, comparable.value.payload) ? + 0 : + evaluateSecondStatement(); + } else { + return -1; + } + } +} diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/event/models/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/event/models/index.ts new file mode 100644 index 0000000000..600c82d32a --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/event/models/index.ts @@ -0,0 +1,11 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './event.codes'; +export * from './event-helper'; +export * from './event-filter.expression'; +export * from './event.comparable'; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/index.ts new file mode 100644 index 0000000000..fa95660c4d --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/index.ts @@ -0,0 +1,10 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './event'; +export * from './decorator'; +export * from './dispatcher'; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/public-api.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/public-api.ts new file mode 100644 index 0000000000..a959d2330b --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/system-events/public-api.ts @@ -0,0 +1,10 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +export { SystemEventHandler, SystemEventHandlerClass } from './decorator'; +export { SystemEventDispatcher } from './dispatcher'; +export { SE_NAVIGATE, SE_ALL_EVENTS, SystemEvent, SystemEventFilterExpression, SystemEventComparable } from './event'; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/taurus-shared-core.module.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/taurus-shared-core.module.ts new file mode 100644 index 0000000000..e211cb8466 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/taurus-shared-core.module.ts @@ -0,0 +1,39 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { ModuleWithProviders, NgModule, Optional, SkipSelf } from '@angular/core'; + +import { CookieService } from 'ngx-cookie-service'; + +import { NavigationService } from './navigation'; + +@NgModule({}) +export class TaurusSharedCoreModule { + /** + * ** Constructor. + */ + constructor(@Optional() @SkipSelf() readonly taurusSharedCoreModule: TaurusSharedCoreModule) { + if (taurusSharedCoreModule) { + throw new Error('TaurusSharedCoreModule is already loaded. Import only once in root Module'); + } + } + + /** + * ** Provides TaurusSharedCore and all Services related to Taurus Core. + * + * - Should be execute once for entire project. + */ + static forRoot(): ModuleWithProviders { + return { + ngModule: TaurusSharedCoreModule, + providers: [ + CookieService, + NavigationService + ] + }; + } +} diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/url-state-manager/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/url-state-manager/index.ts new file mode 100644 index 0000000000..7d58315c43 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/url-state-manager/index.ts @@ -0,0 +1,8 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './url-state.manager'; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/url-state-manager/public-api.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/url-state-manager/public-api.ts new file mode 100644 index 0000000000..5908b4978b --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/url-state-manager/public-api.ts @@ -0,0 +1,8 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +export { URLStateManager } from './index'; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/core/url-state-manager/url-state.manager.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/url-state-manager/url-state.manager.ts new file mode 100644 index 0000000000..1e3bf01d4a --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/core/url-state-manager/url-state.manager.ts @@ -0,0 +1,250 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +/* eslint-disable @typescript-eslint/naming-convention */ + +import { Location } from '@angular/common'; + +import { SE_NAVIGATE, SystemEventDispatcher } from '../system-events'; + +export interface StateManagerParamValue { + key: string; + value: string; + position: number; +} + +/** + * ** State Manager for Browser URL. + * + * - Provides methods for easy appending/retrieving/removing of params and query params to the URL state. + * - Provides ability to serialize current state as URL href. + */ +export class URLStateManager { + /** + * ** Store value if URL Params State mutated since previous navigation. + */ + public isParamsStateMutated = false; + + /** + * ** Store value if URL QueryParams State mutated since previous navigation. + */ + public isQueryParamsStateMutated = false; + + private readonly params: Map; + private readonly queryParams: Map; + private readonly locationHref: string; + + /** + * ** Constructor. + */ + constructor(public baseURL: string, + public urlLocation: Location) { + + this.params = new Map(); + this.queryParams = new Map(); + + this.locationHref = this.urlLocation.path(); + } + + /** + * ** Returns current Browser URL href. + */ + get URL(): string { + if (this.baseURL) { + return `${ this.baseURL }${ this.getParamsToString() }${ this.getQueryParamsToString() }`; + } + + return null; + } + + /** + * ** Apply current URL state to Browser URl. + */ + locationToURL(): void { + const browserCurrUrl = window.location.href; + + if (browserCurrUrl.endsWith(this.URL)) { + return; + } + + this.isParamsStateMutated = false; + + this.urlLocation.go(this.URL); + } + + /** + * ** Navigate through Angular Router with set URL state. + */ + navigateToUrl(): Promise { + const browserCurrUrl = window.location.href; + + if (browserCurrUrl.endsWith(this.URL)) { + return Promise.resolve(false); + } + + this.isQueryParamsStateMutated = false; + + return SystemEventDispatcher.send( + SE_NAVIGATE, + { + url: this.buildUrlWithParams(), + extras: { + queryParams: this.getQueryParamsAsMap() + } + }, + 1 + ); + } + + /** + * ** Set query param to URL state. + */ + setQueryParam(key: string, value: string, position = 1): void { + this.isQueryParamsStateMutated = true; + + if (value) { + this.queryParams.set(key, { key, value, position }); + } else { + this.removeQueryParam(key); + } + } + + /** + * ** Returns query param value for given key. + */ + getQueryParam(key: string): string { + if (this.queryParams.has(key)) { + return this.queryParams.get(key).value; + } + + return null; + } + + /** + * ** Removes query param from URL state. + */ + removeQueryParam(key: string): void { + if (this.queryParams.has(key)) { + this.isQueryParamsStateMutated = true; + + this.queryParams.delete(key); + } + } + + /** + * ** Clear stored queryParams. + */ + clearQueryParams(): void { + this.queryParams.clear(); + } + + /** + * ** Set param to URL state. + */ + setParam(key: string, value: string, position = 1): void { + this.isParamsStateMutated = true; + + if (value) { + this.params.set(key, { key, value, position }); + } else { + this.removeParam(key); + } + } + + /** + * ** Returns param value for given key. + */ + getParam(key: string): string { + if (this.params.has(key)) { + return this.params.get(key).value; + } + + return null; + } + + /** + * ** Removes query param from URL state. + */ + removeParam(key: string): void { + if (this.params.has(key)) { + this.isParamsStateMutated = true; + + this.params.delete(key); + } + } + + /** + * ** Clear stored params. + */ + clearParams(): void { + this.params.clear(); + } + + /** + * ** Returns serialized params in string. + */ + getParamsToString(): string { + let paramString = ''; + + this.getSortedByPosition(this.params).forEach((p) => { + paramString += `/${ p.value }`; + }); + + return paramString; + } + + /** + * ** Returns serialized queryParams in string. + */ + getQueryParamsToString(): string { + const sortedParams = this.getSortedByPosition(this.queryParams); + + let paramString = ''; + + if (sortedParams.length > 0) { + paramString = `?${ sortedParams[0].key }=${ sortedParams[0].value }`; + + for (let i = 1; i < sortedParams.length; i++) { + const p = sortedParams[i]; + paramString += `&${ p.key }=${ p.value }`; + } + } + + return paramString; + } + + /** + * ** Returns query params in Map format. + */ + getQueryParamsAsMap(): { [key: string]: string } { + const sortedParams = this.getSortedByPosition(this.queryParams); + const paramsMap = {}; + + for (const paramsPair of sortedParams) { + paramsMap[paramsPair.key] = paramsPair.value; + } + + return paramsMap; + } + + /** + * ** Build url from base and provided params. + */ + buildUrlWithParams(): string { + if (this.baseURL) { + return `${ this.baseURL }${ this.getParamsToString() }`; + } + + return null; + } + + private getSortedByPosition(values: Map): StateManagerParamValue[] { + return Array.from(values.entries()) + .sort((p1, p2) => p1[1].position - p2[1].position) + .map((e) => e[1]); + } +} diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/error-handler/public-api.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/error-handler/public-api.ts new file mode 100644 index 0000000000..ab31b844b5 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/error-handler/public-api.ts @@ -0,0 +1,8 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './service'; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/error-handler/service/error-handler.service.spec.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/error-handler/service/error-handler.service.spec.ts new file mode 100644 index 0000000000..53e58cf51d --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/error-handler/service/error-handler.service.spec.ts @@ -0,0 +1,493 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +/* eslint-disable max-len */ + +import { HttpErrorResponse, HttpHeaders } from '@angular/common/http'; +import { fakeAsync, TestBed, tick } from '@angular/core/testing'; + +import { Observable } from 'rxjs'; + +import { VmwToastType } from '../../../commons'; + +import { CallFake } from '../../../unit-testing'; + +import { ToastService } from '../../toasts/service'; +import { FormattedError } from '../../toasts/model'; + +import { ErrorHandlerService } from './error-handler.service'; + +describe('ErrorHandlerService', () => { + let toastServiceStub: jasmine.SpyObj; + let service: ErrorHandlerService; + + beforeEach(() => { + toastServiceStub = jasmine.createSpyObj('toastService', ['show']); + + TestBed.configureTestingModule({ + providers: [ + { provide: ToastService, useValue: toastServiceStub }, + ErrorHandlerService + ] + }); + + service = TestBed.inject(ErrorHandlerService); + }); + + it('should verify instance is created', () => { + // Then + expect(service).toBeDefined(); + }); + + describe('Methods::', () => { + describe('|handleError|', () => { + it('should verify will return Observable in error state', fakeAsync(() => { + // Given + const rootError = new Error('some error'); + const processErrorSpy: jasmine.Spy<(error: Error) => void> = spyOn(service, 'processError'); + + // When + const observable$ = service.handleError(rootError); + + // Then + expect(processErrorSpy).toHaveBeenCalledWith(rootError); + expect(observable$).toBeInstanceOf(Observable); + + observable$.subscribe( + CallFake, + (error: unknown) => { + expect(error).toEqual(new Error('Something unexpected happened')); + }); + + tick(100); + })); + }); + + describe('|processError|', () => { + let logErrorSpy: jasmine.Spy; + let url: string; + + beforeEach(() => { + logErrorSpy = spyOn(console, 'error').and.callFake(CallFake); + url = '/shared/resources/statistics/15'; + }); + + describe('should verify will invoke ToastService with expected Toast:', () => { + it('Error is Nil', () => { + // Given + const rootError: Error = null; + + // When + service.processError(rootError); + + // Then + expect(logErrorSpy).toHaveBeenCalledWith(rootError); + expect(toastServiceStub.show).toHaveBeenCalledWith({ + type: VmwToastType.FAILURE, + title: `An error occurred: undefined`, + description: 'We are sorry for the inconvenience.' + + 'Please try again or come back later, and if the issue persists – please copy the details and report the error.', + error: rootError, + responseStatus: null + }); + }); + + it('Error of type SyntaxError', () => { + // Given + const rootError = new SyntaxError('Uncaught SyntaxError: Function statements require a function name'); + + // When + service.processError(rootError); + + // Then + expect(logErrorSpy).toHaveBeenCalledWith(rootError); + expect(toastServiceStub.show).toHaveBeenCalledWith({ + type: VmwToastType.FAILURE, + title: `An error occurred: ${ rootError.message }`, + description: 'We are sorry for the inconvenience.' + + 'Please try again or come back later, and if the issue persists – please copy the details and report the error.', + error: rootError, + responseStatus: null + }); + }); + + it('Error of type SyntaxError and provided additional ErrorHandlerConfig', () => { + // Given + const rootError = new SyntaxError('Uncaught SyntaxError: Function statements require a function name'); + + // When + service.processError( + rootError, + { + title: '---> Unknown error happened', + description: '---> Please try again later, or contact you administrator.', + type: VmwToastType.INFO + } + ); + + // Then + expect(logErrorSpy).toHaveBeenCalledWith(rootError); + expect(toastServiceStub.show).toHaveBeenCalledWith({ + title: '---> Unknown error happened', + description: '---> Please try again later, or contact you administrator.', + type: VmwToastType.INFO, + error: rootError, + responseStatus: null, + extendedData: { + title: `An error occurred: ${ rootError.message }`, + description: 'We are sorry for the inconvenience.' + + 'Please try again or come back later, and if the issue persists – please copy the details and report the error.' + } + }); + }); + + it('Error of type HttpErrorResponse when sub-error is of type ErrorEvent', () => { + // Given + const topRootError = new ErrorEvent('Connection lost'); + const rootError = new HttpErrorResponse({ + error: topRootError, + status: 0, + url, + headers: new HttpHeaders() + }); + + // When + service.processError(rootError); + + // Then + expect(logErrorSpy).toHaveBeenCalledWith(`An error occurred: ${ (rootError.error as Error).message }`); + expect(toastServiceStub.show).toHaveBeenCalledWith({ + type: VmwToastType.FAILURE, + title: `An error occurred: ${ rootError.message }`, + description: 'We are sorry for the inconvenience.' + + 'Please try again or come back later, and if the issue persists – please copy the details and report the error.', + error: rootError, + responseStatus: null + }); + }); + + it('Error of type HttpErrorResponse when status 403 and sub-error is server side error', () => { + // Given + const topRootError = { message: 'Access Denied' } as Error; + const rootError = new HttpErrorResponse({ + error: topRootError, + status: 403, + url, + headers: new HttpHeaders() + }); + + // When + service.processError(rootError); + + // Then + expect(logErrorSpy).toHaveBeenCalledWith(topRootError); + expect(toastServiceStub.show).toHaveBeenCalledWith({ + type: VmwToastType.FAILURE, + title: 'ACCESS DENIED', + description: 'You are not authorized for this content!\ ' + + 'If you think it is a mistake please contact the data owners and request them to grant you access.', + error: topRootError, + responseStatus: 403 + }); + }); + + it('Error of type HttpErrorResponse when status 500 and sub-error is Nil', () => { + // Given + const topRootError = null; + const rootError = new HttpErrorResponse({ + error: topRootError, + status: 500, + url, + headers: new HttpHeaders() + }); + + // When + service.processError(rootError); + + // Then + expect(logErrorSpy).toHaveBeenCalledWith(rootError); + expect(toastServiceStub.show).toHaveBeenCalledWith({ + type: VmwToastType.FAILURE, + title: 'Internal Server Error', + description: 'We are sorry for the inconvenience.' + + 'Please try again or come back later, and if the issue persists – please copy the details and report the error.', + error: topRootError, + responseStatus: 500 + }); + }); + + it('Error of type HttpErrorResponse when status 500 and sub-error is server side error with what and why', () => { + // Given + const topRootError = { + message: 'Internal Server Error', + what: 'Server is down, internal server error', + why: 'Unknown error happened on server side' + }; + const rootError = new HttpErrorResponse({ + error: topRootError, + status: 500, + url, + headers: new HttpHeaders() + }); + + // When + service.processError(rootError); + + // Then + expect(logErrorSpy).toHaveBeenCalledWith(topRootError); + expect(toastServiceStub.show).toHaveBeenCalledWith({ + type: VmwToastType.FAILURE, + title: topRootError.what, + description: topRootError.why, + error: topRootError, + responseStatus: 500 + }); + }); + + it('Error of type HttpErrorResponse when status 500 and sub-error is server side error', () => { + // Given + const topRootError = { + message: 'Internal Server Error' + } as Error; + const rootError = new HttpErrorResponse({ + error: topRootError, + status: 500, + url, + headers: new HttpHeaders() + }); + + // When + service.processError(rootError); + + // Then + expect(logErrorSpy).toHaveBeenCalledWith(topRootError); + expect(toastServiceStub.show).toHaveBeenCalledWith({ + type: VmwToastType.FAILURE, + title: 'Internal Server Error', + description: 'We are sorry for the inconvenience.' + + 'Please try again or come back later, and if the issue persists – please copy the details and report the error.', + error: topRootError, + responseStatus: 500 + }); + }); + + it('Error of type HttpErrorResponse when status 400 and sub-error is Nil', () => { + // Given + const rootError = new HttpErrorResponse({ + error: null, + status: 400, + url, + headers: new HttpHeaders() + }); + + // When + service.processError(rootError); + + // Then + expect(logErrorSpy).toHaveBeenCalledWith(rootError); + expect(toastServiceStub.show).toHaveBeenCalledWith({ + type: VmwToastType.FAILURE, + title: 'Invalid param', + description: 'Operation failed', + error: rootError, + responseStatus: 400 + }); + }); + + it('Error of type HttpErrorResponse when status 401 and sub-error is Nil', () => { + // Given + const rootError = new HttpErrorResponse({ + error: null, + status: 401, + url, + headers: new HttpHeaders() + }); + + // When + service.processError(rootError); + + // Then + expect(logErrorSpy).toHaveBeenCalledWith(rootError); + expect(toastServiceStub.show).toHaveBeenCalledWith({ + type: VmwToastType.FAILURE, + title: 'Unauthorized', + description: 'Operation failed', + error: rootError, + responseStatus: 401 + }); + }); + + it('Error of type HttpErrorResponse when status 404 and sub-error is Nil', () => { + // Given + const rootError = new HttpErrorResponse({ + error: null, + status: 404, + url, + headers: new HttpHeaders() + }); + + // When + service.processError(rootError); + + // Then + expect(logErrorSpy).toHaveBeenCalledWith(rootError); + expect(toastServiceStub.show).toHaveBeenCalledWith({ + type: VmwToastType.FAILURE, + title: 'Not Found', + description: 'Operation failed', + error: rootError, + responseStatus: 404 + }); + }); + + it('Error of type HttpErrorResponse when status 405 and sub-error is Nil', () => { + // Given + const rootError = new HttpErrorResponse({ + error: null, + status: 405, + url, + headers: new HttpHeaders() + }); + + // When + service.processError(rootError); + + // Then + expect(logErrorSpy).toHaveBeenCalledWith(rootError); + expect(toastServiceStub.show).toHaveBeenCalledWith({ + type: VmwToastType.FAILURE, + title: 'Not Allowed', + description: 'Operation failed', + error: rootError, + responseStatus: 405 + }); + }); + + it('Error of type HttpErrorResponse when status 422 and sub-error is Nil', () => { + // Given + const rootError = new HttpErrorResponse({ + error: null, + status: 422, + url, + headers: new HttpHeaders() + }); + + // When + service.processError(rootError); + + // Then + expect(logErrorSpy).toHaveBeenCalledWith(rootError); + expect(toastServiceStub.show).toHaveBeenCalledWith({ + type: VmwToastType.FAILURE, + title: 'Invalid operation', + description: 'Operation failed', + error: rootError, + responseStatus: 422 + }); + }); + + it('Error of type HttpErrorResponse when status 501 and sub-error is Nil', () => { + // Given + const rootError = new HttpErrorResponse({ + error: null, + status: 501, + url, + headers: new HttpHeaders() + }); + + // When + service.processError(rootError); + + // Then + expect(logErrorSpy).toHaveBeenCalledWith(rootError); + expect(toastServiceStub.show).toHaveBeenCalledWith({ + type: VmwToastType.FAILURE, + title: 'Unknown Error', + description: 'Operation failed', + error: rootError, + responseStatus: 501 + }); + }); + + it('Error of type HttpErrorResponse when status 507 and sub-error is server side error with what and why', () => { + // Given + const rootError = new HttpErrorResponse({ + error: { + message: 'Insufficient Storage', + what: 'Insufficient Storage on server', + why: 'Cloud storage id down' + }, + status: 507, + url, + headers: new HttpHeaders() + }); + + // When + service.processError(rootError); + + // Then + expect(logErrorSpy).toHaveBeenCalledWith(rootError.error); + expect(toastServiceStub.show).toHaveBeenCalledWith({ + type: VmwToastType.FAILURE, + title: (rootError.error as FormattedError).what, + description: (rootError.error as FormattedError).why, + error: rootError.error, + responseStatus: 507 + }); + }); + + it('Error of type HttpErrorResponse when status 423 and sub-error is of type string', () => { + // Given + const rootError = new HttpErrorResponse({ + error: 'Entity is locked', + status: 423, + url, + headers: new HttpHeaders() + }); + + // When + service.processError(rootError); + + // Then + expect(logErrorSpy).toHaveBeenCalledWith(rootError.error); + expect(toastServiceStub.show).toHaveBeenCalledWith({ + type: VmwToastType.FAILURE, + title: rootError.error, + description: rootError.message, + error: rootError.error, + responseStatus: 423 + }); + }); + + it('Error of type HttpErrorResponse when status 417 and sub-error is server side error', () => { + // Given + const rootError = new HttpErrorResponse({ + error: { + message: 'Expectation Failed' + }, + status: 417, + url, + headers: new HttpHeaders() + }); + + // When + service.processError(rootError); + + // Then + expect(logErrorSpy).toHaveBeenCalledWith(rootError.error); + expect(toastServiceStub.show).toHaveBeenCalledWith({ + type: VmwToastType.FAILURE, + title: 'Unknown Error', + description: rootError.message, + error: rootError.error, + responseStatus: 417 + }); + }); + }); + }); + }); +}); diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/error-handler/service/error-handler.service.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/error-handler/service/error-handler.service.ts new file mode 100644 index 0000000000..256f6099d2 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/error-handler/service/error-handler.service.ts @@ -0,0 +1,188 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +/* eslint-disable @typescript-eslint/unified-signatures */ + +import { Injectable } from '@angular/core'; +import { HttpErrorResponse } from '@angular/common/http'; + +import { Observable, throwError } from 'rxjs'; + +import { VmwToastType } from '../../../commons'; + +import { CollectionsUtil } from '../../../utils'; + +import { FormattedError, Toast } from '../../toasts/model'; +import { ToastService } from '../../toasts/service'; + +export interface ErrorHandlerConfig { + title?: Toast['title']; + description?: Toast['description']; + type?: Toast['type']; +} + +/** + * ** Error handler service. + * + * + */ +@Injectable() +export class ErrorHandlerService { + /** + * ** Constructor. + */ + constructor(private readonly toastService: ToastService) { + } + + /** + * ** Handle Error in rxjs stream. + * + * - Show Toast message + * - Log it to console + * - Re-throw new Error('Something unexpected happened') + */ + handleError = (error: Error): Observable => { + this.processError(error); + + const newError = new Error('Something unexpected happened'); + + return throwError(() => newError); + }; + + /** + * ** Process Error. + * + * - Show Toast message + * - Log it to console + */ + processError(error: Error): void; + processError(error: Error, overriddenConfig: ErrorHandlerConfig): void; + processError(error: Error, overriddenConfig?: ErrorHandlerConfig): void { + if (error instanceof HttpErrorResponse) { + if (error.error instanceof ErrorEvent) { // A client-side or network error occurred. + const toast = ErrorHandlerService._createToastConfigForError(error, overriddenConfig); + + console.error(`An error occurred: ${ error.error.message }`); + + this.toastService.show(toast); + } else { // Server side error occurred. + const toast = ErrorHandlerService._createToastConfigForHttpErrorResponse(error, overriddenConfig); + + console.error(error.error ?? error); + + this.toastService.show(toast); + } + } else { // Runtime error occurred, potential bug. + const toast = ErrorHandlerService._createToastConfigForError(error, overriddenConfig); + + console.error(error); + + this.toastService.show(toast); + } + } + + /* eslint-disable @typescript-eslint/member-ordering */ + + private static _createToastConfigForHttpErrorResponse(error: HttpErrorResponse, overriddenConfig: ErrorHandlerConfig): Toast { + let title: string; + let description: string; + let rootError: Error = error.error as Error; + const responseStatus = error.status; + + if (error.status === 403) { + title = 'ACCESS DENIED'; + description = 'You are not authorized for this content!\ ' + + 'If you think it is a mistake please contact the data owners and request them to grant you access.'; + } else if (error.status === 500) { + title = (error.error as FormattedError)?.what + ? (error.error as FormattedError).what + : 'Internal Server Error'; + description = (error.error as FormattedError)?.why + ? (error.error as FormattedError).why + : 'We are sorry for the inconvenience.' + + 'Please try again or come back later, and if the issue persists – please copy the details and report the error.'; + } else if (CollectionsUtil.isNil(error.error)) { + title = ErrorHandlerService._getErrorTitle(error.status); + description = 'Operation failed'; + rootError = error; + } else if (error.error && (error.error as FormattedError).what && (error.error as FormattedError).why) { + title = (error.error as FormattedError).what; + description = (error.error as FormattedError).why; + } else if (typeof error.error === 'string') { + title = error.error; + description = error.message; + } else { + title = ErrorHandlerService._getErrorTitle(error.status); + description = error.message; + } + + return ErrorHandlerService._createToastConfig( + title, + description, + rootError, + responseStatus, + overriddenConfig + ); + } + + private static _createToastConfigForError(error: Error, overriddenConfig: ErrorHandlerConfig): Toast { + return ErrorHandlerService._createToastConfig( + `An error occurred: ${ error?.message }`, + 'We are sorry for the inconvenience.' + + 'Please try again or come back later, and if the issue persists – please copy the details and report the error.', + error, + undefined, + overriddenConfig + ); + } + + private static _getErrorTitle(status: number): string { + switch (status) { + case 400: + return 'Invalid param'; + case 401: + return 'Unauthorized'; + case 404: + return 'Not Found'; + case 405: + return 'Not Allowed'; + case 422: + return 'Invalid operation'; + default: + return 'Unknown Error'; + } + } + + private static _createToastConfig( + title: string, + description: string, + error: Error, + responseStatus: number = null, + overriddenConfig: ErrorHandlerConfig = null): Toast { + + let toastConfig: Toast = { + title, + description, + type: VmwToastType.FAILURE, + error, + responseStatus + }; + + if (CollectionsUtil.isDefined(overriddenConfig)) { + toastConfig = { + ...toastConfig, + ...overriddenConfig, + extendedData: { + title, + description + } + }; + } + + return toastConfig; + } +} diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/error-handler/service/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/error-handler/service/index.ts new file mode 100644 index 0000000000..e1e7d2b202 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/error-handler/service/index.ts @@ -0,0 +1,8 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './error-handler.service'; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/public-api.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/public-api.ts new file mode 100644 index 0000000000..c70260de59 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/public-api.ts @@ -0,0 +1,9 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './error-handler/public-api'; +export * from './toasts/public-api'; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/taurus-shared-features.module.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/taurus-shared-features.module.ts new file mode 100644 index 0000000000..029941ae07 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/taurus-shared-features.module.ts @@ -0,0 +1,62 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { ModuleWithProviders, NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule } from '@angular/router'; + +import { ClarityModule } from '@clr/angular'; + +import { VdkComponentsModule } from '../commons'; + +import { ToastsModule } from './toasts/toasts.module'; + +import { ErrorHandlerService } from './error-handler/service'; + +import { ToastService } from './toasts/service'; + +@NgModule({ + imports: [ + CommonModule, + ClarityModule, + VdkComponentsModule.forChild(), + RouterModule, + ToastsModule + ], + exports: [ + ToastsModule + ] +}) +export class TaurusSharedFeaturesModule { + /** + * ** Provides TaurusSharedFeaturesModule and all Services related to Shared Module features. + * + * - Should be invoke only once for entire project. + * - Not inside FeatureModule (lazy loaded Module). + * - In other modules import only taurus-shared-features.module.tTaurusSharedFeaturesModule. + */ + static forRoot(): ModuleWithProviders { + return { + ngModule: TaurusSharedFeaturesModule, + providers: [ + ErrorHandlerService, + ToastService + ] + }; + } + + /** + * ** Provides TaurusSharedFeaturesModule. + * + * - Should be invoke in FeatureModules (lazy loaded Modules). + */ + static forChild(): ModuleWithProviders { + return { + ngModule: TaurusSharedFeaturesModule + }; + } +} diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/toasts/model/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/toasts/model/index.ts new file mode 100644 index 0000000000..21595c91af --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/toasts/model/index.ts @@ -0,0 +1,8 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './toast.interface'; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/toasts/model/toast.interface.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/toasts/model/toast.interface.ts new file mode 100644 index 0000000000..b54401093b --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/toasts/model/toast.interface.ts @@ -0,0 +1,28 @@ +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { HttpErrorResponse } from '@angular/common/http'; + +import { VmwToastType } from '../../../commons'; + +export interface Toast { + title: string; + description: string; + type: VmwToastType; + error?: Error | FormattedError | HttpErrorResponse; + expanded?: boolean; + responseStatus?: number; + extendedData?: { + title: string; + description: string; + }; +} + +export interface FormattedError { + consequences?: string; + countermeasures?: string; + what: string; + why: string; +} diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/toasts/public-api.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/toasts/public-api.ts new file mode 100644 index 0000000000..b54203acde --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/toasts/public-api.ts @@ -0,0 +1,11 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +export { Toast, FormattedError } from './model'; +export { ToastService } from './service'; +export { ToastsComponent } from './widget'; +export { ToastsModule } from './toasts.module'; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/toasts/service/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/toasts/service/index.ts new file mode 100644 index 0000000000..5237c38345 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/toasts/service/index.ts @@ -0,0 +1,8 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './toast.service'; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/toasts/service/toast.service.spec.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/toasts/service/toast.service.spec.ts new file mode 100644 index 0000000000..c718797b04 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/toasts/service/toast.service.spec.ts @@ -0,0 +1,40 @@ +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { TestBed } from '@angular/core/testing'; +import { VmwToastType } from '../../../commons'; + +import { ToastService } from './toast.service'; + +describe('ToastServiceComponent', () => { + let service: ToastService; + + const TEST_TOAST = { + title: 'title001', + description: 'descr001', + type: VmwToastType.FAILURE + }; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + ToastService + ] + }); + service = TestBed.inject(ToastService); + }); + + it('should create', () => { + expect(service).toBeTruthy(); + }); + + describe('show', () => { + it('makes expected calls', () => { + spyOn(service.notificationsSubject, 'next').and.callThrough(); + service.show(TEST_TOAST); + expect(service.notificationsSubject.next).toHaveBeenCalledWith(TEST_TOAST); + }); + }); +}); diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/toasts/service/toast.service.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/toasts/service/toast.service.ts new file mode 100644 index 0000000000..478c751a64 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/toasts/service/toast.service.ts @@ -0,0 +1,30 @@ +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Injectable } from '@angular/core'; + +import { Subject } from 'rxjs'; + +import { Toast } from '../model'; + +@Injectable() +export class ToastService { + notificationsSubject = new Subject(); + private notification$ = this.notificationsSubject.asObservable(); + + /** + * ** Get subscribable stream, that raise new Events when new Toast should be shown. + */ + getNotifications() { + return this.notification$; + } + + /** + * ** Show Toast message. + */ + show(toast: Toast) { + this.notificationsSubject.next(toast); + } +} diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/toasts/toasts.module.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/toasts/toasts.module.ts new file mode 100644 index 0000000000..628c32533d --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/toasts/toasts.module.ts @@ -0,0 +1,32 @@ +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +import { ClipboardModule } from 'ngx-clipboard'; + +import { ClarityModule } from '@clr/angular'; + +import { VdkComponentsModule } from '../../commons'; + +import { ToastsComponent } from './widget'; + +@NgModule({ + imports: [ + CommonModule, + ClarityModule, + VdkComponentsModule, + ClipboardModule + ], + declarations: [ + ToastsComponent + ], + exports: [ + ToastsComponent + ] +}) +export class ToastsModule { +} diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/toasts/widget/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/toasts/widget/index.ts new file mode 100644 index 0000000000..a65b5a6aa9 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/toasts/widget/index.ts @@ -0,0 +1,8 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './toasts.component'; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/toasts/widget/toasts.component.html b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/toasts/widget/toasts.component.html new file mode 100644 index 0000000000..69dab4f082 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/toasts/widget/toasts.component.html @@ -0,0 +1,27 @@ + + + + +

{{toast.title}}
+ +

{{toast.description}}

+

consequences: {{toast.error?.consequences}}

+

countermeasures: {{toast.error?.countermeasures}}

+

Please copy the details and report the error.

+ + + diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/toasts/widget/toasts.component.scss b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/toasts/widget/toasts.component.scss new file mode 100644 index 0000000000..34245805c0 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/toasts/widget/toasts.component.scss @@ -0,0 +1,17 @@ +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +.toast-container { + z-index: 1051; + + .toast-title { + max-width: 300px; + } + + .toast-description { + margin-top: 5px; + max-width: 330px; + } +} diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/toasts/widget/toasts.component.spec.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/toasts/widget/toasts.component.spec.ts new file mode 100644 index 0000000000..da82baa729 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/toasts/widget/toasts.component.spec.ts @@ -0,0 +1,57 @@ +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { VmwToastType } from '../../../commons'; + +import { ToastsComponent } from './toasts.component'; +import { ToastService } from '../service'; + +describe('ToastsComponent', () => { + let component: ToastsComponent; + let fixture: ComponentFixture; + + const TEST_TOAST = { + title: 'title001', + description: 'description001', + type: VmwToastType.FAILURE, + id: 10, + time: new Date() + }; + + const FIRST_ELEMENT_INDEX = 0; + const EMPTY_LIST_LENGTH = 0; + + beforeEach( + async () => { + await TestBed.configureTestingModule({ + providers: [ + ToastService + ], + schemas: [NO_ERRORS_SCHEMA], + declarations: [ToastsComponent] + }).compileComponents(); + } + ); + + beforeEach(() => { + fixture = TestBed.createComponent(ToastsComponent); + component = fixture.componentInstance; + component.toasts = [TEST_TOAST]; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + describe('remove', () => { + it('removes an element from the toasts list ', () => { + component.removeToast(FIRST_ELEMENT_INDEX); + expect(component.toasts.length).toEqual(EMPTY_LIST_LENGTH); + }); + }); +}); diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/features/toasts/widget/toasts.component.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/toasts/widget/toasts.component.ts new file mode 100644 index 0000000000..b55151ca8a --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/features/toasts/widget/toasts.component.ts @@ -0,0 +1,167 @@ +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Component, OnInit } from '@angular/core'; + +import { ClipboardService } from 'ngx-clipboard'; + +import { VmwToastType } from '../../../commons'; + +import { TaurusObject } from '../../../common'; + +import { Toast } from '../model'; +import { ToastService } from '../service'; + +interface ToastInternal extends Toast { + id: number; + time: Date; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + error?: any; +} + +@Component({ + selector: 'shared-toasts', + templateUrl: './toasts.component.html', + styleUrls: ['./toasts.component.scss'] +}) +export class ToastsComponent extends TaurusObject implements OnInit { + private static toastMessageCounter = 0; + + toasts: ToastInternal[]; + + constructor(private readonly toastService: ToastService, + private readonly clipboardService: ClipboardService) { + super(); + this.toasts = []; + } + + /** + * ** Optimize Toast rendering using tracking with auto incremented ID per Toast. + */ + trackByRendering(_index: number, toast: ToastInternal): number { + return toast.id; + } + + /** + * ** Returns if Toast with given index is expanded. + */ + isToastExpanded(index: number): boolean { + return this.toasts[index].expanded; + } + + /** + * ** Evaluate if recommendation text for Copy and Report is visible. + */ + isReportRecommendationVisible(toast: ToastInternal, index: number): boolean { + return this.isToastExpanded(index) && + this._isTypeError(toast) && + toast.responseStatus !== 500; + } + + /** + * ** Remove Toast message. + */ + removeToast(index: number): void { + this.toasts.splice(index, 1); + } + + /** + * ** Toggle Toast expand details (expand/collapse). + */ + toggleToastExpandDetails(index: number): void { + this.toasts[index].expanded = !this.toasts[index].expanded; + } + + /** + * ** Copy to clipboard provided object. + */ + copyToClipboard(copy: ToastInternal): void { + try { + this.clipboardService.copy(JSON.stringify(copy)); + } catch (e) { + console.error(e); + + this._handleCopyActionError(); + } + } + + /** + * ** Returns Toast timeout in unit seconds. + */ + getTimeout(toast: ToastInternal): number { + return this._isTypeError(toast) + ? 30 + : 5; + } + + /** + * ** Returns text for Btn CopyToClipboard. + */ + getCopyToClipboardBtnText(toast: ToastInternal): string { + return this._isTypeError(toast) + ? 'Copy to clipboard' + : ''; + } + + /** + * ** Returns text for Btn Expand/Collapse. + */ + getExpandBtnText(toast: ToastInternal, index: number): string { + if (this._isTypeError(toast)) { + return this.isToastExpanded(index) + ? 'less' + : 'more'; + } + + return ''; + } + + /** + * @inheritDoc + */ + ngOnInit() { + this.subscriptions.push( + this.toastService + .getNotifications() + .subscribe((toast: Toast) => { + this.toasts.push({ + ...toast, + id: ToastsComponent.generateID(), + time: ToastsComponent.getDateTimeNow() + }); + }), + this.clipboardService + .copyResponse$ + .subscribe((result) => { + if (!result.isSuccess) { + this._handleCopyActionError(); + } + }) + ); + } + + private _isTypeError(toast: Toast): boolean { + return toast.type === VmwToastType.FAILURE && !!toast.error; + } + + private _handleCopyActionError(): void { + this.toasts.push({ + type: VmwToastType.FAILURE, + title: `Copy to clipboard`, + description: `The view definition failed to copy to the clipboard`, + id: ToastsComponent.generateID(), + time: ToastsComponent.getDateTimeNow() + }); + } + + /* eslint-disable @typescript-eslint/member-ordering */ + private static generateID(): number { + return this.toastMessageCounter++; + } + + private static getDateTimeNow(): Date { + return new Date(); + } +} diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/unit-testing/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/unit-testing/index.ts new file mode 100644 index 0000000000..937c1ff272 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/unit-testing/index.ts @@ -0,0 +1,8 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './utils'; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/unit-testing/public-api.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/unit-testing/public-api.ts new file mode 100644 index 0000000000..ba142389bd --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/unit-testing/public-api.ts @@ -0,0 +1,8 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './index'; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/unit-testing/utils/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/unit-testing/utils/index.ts new file mode 100644 index 0000000000..c9509c2a3c --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/unit-testing/utils/index.ts @@ -0,0 +1,9 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './unit-test-utils'; +export * from './router-utils'; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/unit-testing/utils/router-utils.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/unit-testing/utils/router-utils.ts new file mode 100644 index 0000000000..28effb9a76 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/unit-testing/utils/router-utils.ts @@ -0,0 +1,135 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +/* eslint-disable arrow-body-style, + prefer-arrow/prefer-arrow-functions, + @angular-eslint/component-class-suffix, + @typescript-eslint/no-explicit-any */ + +import { Type } from '@angular/core'; + +import { ActivatedRouteSnapshot, Data, Params, Route, UrlSegment } from '@angular/router'; + +import { CollectionsUtil } from '../../utils'; + +type ComponentType = Type | string | null; + +export const createRouteSnapshot = ({ + url = [new UrlSegment('entity/23', {})] as UrlSegment[], + data = { paramKey: 'prime' } as Data, + params = { entityId: 23 } as Params, + queryParams = { search: 'test-team' } as Params, + outlet = 'router-outlet' as string, + component = ComponentStub as ComponentType, + routeConfig = new RouteConfigStub('entity/23', ComponentStub, 'router-outlet'), + firstChild = null as ActivatedRouteSnapshot, + parent = undefined as ActivatedRouteSnapshot + }): ActivatedRouteSnapshot => { + + // @ts-ignore + return new ActivatedRouteSnapshotStub({ + url, + data, + params, + queryParams, + outlet, + component, + routeConfig, + firstChild, + parent: CollectionsUtil.isUndefined(parent) + ? createRouteSnapshot({ + url: [new UrlSegment('domain/context', {})], + data: {}, + params: {}, + queryParams: {}, + outlet: 'router-outlet', + component: null, + routeConfig: new RouteConfigStub('domain/context', null, 'router-outlet'), + firstChild: null, + parent: null + }) + : null + }); +}; + +export class ComponentStub { +} + +interface RouteSnapshotStub { + url: UrlSegment[]; + data: Data; + params: Params; + queryParams: Params; + outlet: string; + component: Type | string | null; + routeConfig: Route | null; + firstChild: ActivatedRouteSnapshot; + parent: ActivatedRouteSnapshot; +} + +export class ActivatedRouteSnapshotStub extends ActivatedRouteSnapshot { + override url: UrlSegment[]; + override data: Data; + override params: Params; + override queryParams: Params; + override outlet: string; + // @ts-ignore + override component: Type | string | null; + override readonly routeConfig: Route | null; + + override get parent(): ActivatedRouteSnapshot | null { + return this._parent; + } + + override set parent(parent: ActivatedRouteSnapshot) { + this._parent = parent; + } + + override get firstChild(): ActivatedRouteSnapshot | null { + return this._firstChild; + } + + protected _parent: ActivatedRouteSnapshot; + protected _firstChild: ActivatedRouteSnapshot; + + constructor(snapshot: RouteSnapshotStub) { + super(); + + this.url = snapshot.url; + this.data = snapshot.data; + this.params = snapshot.params; + this.queryParams = snapshot.queryParams; + this.outlet = snapshot.outlet; + this.component = snapshot.component; + this.routeConfig = snapshot.routeConfig; + this._firstChild = snapshot.firstChild; + this._parent = snapshot.parent; + + if (snapshot.firstChild instanceof ActivatedRouteSnapshotStub) { + // @ts-ignore + snapshot.firstChild.parent = this; + } + } + + override toString(): string { + return super.toString(); + } +} + +export class RouteConfigStub implements Route { + path: string; + component: Type; + outlet: string; + + constructor(path: string, + component: Type, + outlet: string) { + this.path = path; + this.component = component; + this.outlet = outlet; + } +} diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/unit-testing/utils/unit-test-utils.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/unit-testing/utils/unit-test-utils.ts new file mode 100644 index 0000000000..f85b0bbb62 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/unit-testing/utils/unit-test-utils.ts @@ -0,0 +1,33 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +/* eslint-disable @typescript-eslint/naming-convention,@typescript-eslint/no-explicit-any */ + +/** + * ** Fake method to mock methods. + * + * e.g. + * .and.callFake(CallFake) + * + * + */ +export const CallFake = (..._args: any[]): any => { + // No-op. +}; + +/** + * ** Utility to trigger keyboard event from some HTMLElement. + */ +export const triggerKeyboardEvent = (el: HTMLElement, type: string, keyCode: string) => { + const e = new KeyboardEvent(type, { + code: keyCode, + bubbles: true, + cancelable: true + }); + + el.dispatchEvent(e); +}; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/utils/collections/collections-util.spec.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/utils/collections/collections-util.spec.ts new file mode 100644 index 0000000000..5b18c07bb1 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/utils/collections/collections-util.spec.ts @@ -0,0 +1,959 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +/* eslint-disable @typescript-eslint/naming-convention,prefer-arrow/prefer-arrow-functions,space-before-function-paren */ + +import { CollectionsUtil } from './collections-util'; + +const testObj: { + num: number; + str: string; + arr: any[]; + arrEmpty: any[]; + obj: Record; + objNoProp: Record; + undef: any; + nullVal: null; + bool: boolean; + nan: any; + map: Map; + weakMap: WeakMap; + set: Set; + func: () => void; + date: Date; +} = { + num: 1, + str: 'string', + arr: [1, 2, 3, 4, 5], + arrEmpty: [], + obj: { prop: 1 }, + objNoProp: {}, + undef: undefined, + nullVal: null, + bool: true, + nan: NaN, + map: new Map(), + weakMap: new WeakMap(), + set: new Set(), + func: () => { + // do nothing; + }, + date: new Date() +}; + +describe('CollectionsUtil::', () => { + describe('Statics', () => { + describe('Methods::()', () => { + describe('|isUndefined|', () => { + it('should verify if undefined', () => { + expect(CollectionsUtil.isUndefined(testObj.undef)).toBeTruthy(); + + expect(CollectionsUtil.isUndefined(testObj.num)).toBeFalsy(); + expect(CollectionsUtil.isUndefined(testObj.str)).toBeFalsy(); + expect(CollectionsUtil.isUndefined(testObj.arr)).toBeFalsy(); + expect(CollectionsUtil.isUndefined(testObj.arrEmpty)).toBeFalsy(); + expect(CollectionsUtil.isUndefined(testObj.obj)).toBeFalsy(); + expect(CollectionsUtil.isUndefined(testObj.objNoProp)).toBeFalsy(); + expect(CollectionsUtil.isUndefined(testObj.nullVal)).toBeFalsy(); + expect(CollectionsUtil.isUndefined(testObj.bool)).toBeFalsy(); + expect(CollectionsUtil.isUndefined(testObj.nan)).toBeFalsy(); + expect(CollectionsUtil.isUndefined(testObj.func)).toBeFalsy(); + }); + }); + + describe('|isNumber|', () => { + it('should verify if number', () => { + expect(CollectionsUtil.isNumber(testObj.num)).toBeTruthy(); + expect(CollectionsUtil.isNumber(testObj.nan)).toBeTruthy(); + + expect(CollectionsUtil.isNumber(testObj.str)).toBeFalsy(); + expect(CollectionsUtil.isNumber(testObj.arr)).toBeFalsy(); + expect(CollectionsUtil.isNumber(testObj.arrEmpty)).toBeFalsy(); + expect(CollectionsUtil.isNumber(testObj.obj)).toBeFalsy(); + expect(CollectionsUtil.isNumber(testObj.objNoProp)).toBeFalsy(); + expect(CollectionsUtil.isNumber(testObj.undef)).toBeFalsy(); + expect(CollectionsUtil.isNumber(testObj.nullVal)).toBeFalsy(); + expect(CollectionsUtil.isNumber(testObj.bool)).toBeFalsy(); + expect(CollectionsUtil.isNumber(testObj.func)).toBeFalsy(); + }); + }); + + describe('|isString|', () => { + it('should verify if string', () => { + expect(CollectionsUtil.isString(testObj.str)).toBeTruthy(); + + expect(CollectionsUtil.isString(testObj.num)).toBeFalsy(); + expect(CollectionsUtil.isString(testObj.arr)).toBeFalsy(); + expect(CollectionsUtil.isString(testObj.arrEmpty)).toBeFalsy(); + expect(CollectionsUtil.isString(testObj.obj)).toBeFalsy(); + expect(CollectionsUtil.isString(testObj.objNoProp)).toBeFalsy(); + expect(CollectionsUtil.isString(testObj.undef)).toBeFalsy(); + expect(CollectionsUtil.isString(testObj.nullVal)).toBeFalsy(); + expect(CollectionsUtil.isString(testObj.bool)).toBeFalsy(); + expect(CollectionsUtil.isString(testObj.nan)).toBeFalsy(); + expect(CollectionsUtil.isString(testObj.func)).toBeFalsy(); + }); + }); + + describe('|isBoolean|', () => { + it('should verify if boolean', () => { + expect(CollectionsUtil.isBoolean(testObj.bool)).toBeTruthy(); + + expect(CollectionsUtil.isBoolean(testObj.num)).toBeFalsy(); + expect(CollectionsUtil.isBoolean(testObj.str)).toBeFalsy(); + expect(CollectionsUtil.isBoolean(testObj.arr)).toBeFalsy(); + expect(CollectionsUtil.isBoolean(testObj.arrEmpty)).toBeFalsy(); + expect(CollectionsUtil.isBoolean(testObj.obj)).toBeFalsy(); + expect(CollectionsUtil.isBoolean(testObj.objNoProp)).toBeFalsy(); + expect(CollectionsUtil.isBoolean(testObj.undef)).toBeFalsy(); + expect(CollectionsUtil.isBoolean(testObj.nullVal)).toBeFalsy(); + expect(CollectionsUtil.isBoolean(testObj.nan)).toBeFalsy(); + expect(CollectionsUtil.isBoolean(testObj.func)).toBeFalsy(); + }); + }); + + describe('|isPrimitive|', () => { + it('should verify if value is Primitive', () => { + expect(CollectionsUtil.isPrimitive(testObj.bool)).toBeTruthy(); + expect(CollectionsUtil.isPrimitive(testObj.num)).toBeTruthy(); + expect(CollectionsUtil.isPrimitive(testObj.nan)).toBeTruthy(); + expect(CollectionsUtil.isPrimitive(testObj.str)).toBeTruthy(); + expect(CollectionsUtil.isPrimitive(testObj.undef)).toBeTruthy(); + + expect(CollectionsUtil.isPrimitive(testObj.arr)).toBeFalsy(); + expect(CollectionsUtil.isPrimitive(testObj.arrEmpty)).toBeFalsy(); + expect(CollectionsUtil.isPrimitive(testObj.obj)).toBeFalsy(); + expect(CollectionsUtil.isPrimitive(testObj.objNoProp)).toBeFalsy(); + expect(CollectionsUtil.isPrimitive(testObj.nullVal)).toBeFalsy(); + expect(CollectionsUtil.isPrimitive(testObj.func)).toBeFalsy(); + }); + }); + + describe('|isObject|', () => { + it('should verify if object', () => { + expect(CollectionsUtil.isObject(testObj.obj)).toBeTruthy(); + expect(CollectionsUtil.isObject(testObj.objNoProp)).toBeTruthy(); + expect(CollectionsUtil.isObject(testObj.arr)).toBeTruthy(); + expect(CollectionsUtil.isObject(testObj.arrEmpty)).toBeTruthy(); + expect(CollectionsUtil.isObject(testObj.nullVal)).toBeTruthy(); + + expect(CollectionsUtil.isObject(testObj.func)).toBeFalsy(); + expect(CollectionsUtil.isObject(testObj.num)).toBeFalsy(); + expect(CollectionsUtil.isObject(testObj.str)).toBeFalsy(); + expect(CollectionsUtil.isObject(testObj.undef)).toBeFalsy(); + expect(CollectionsUtil.isObject(testObj.bool)).toBeFalsy(); + expect(CollectionsUtil.isObject(testObj.nan)).toBeFalsy(); + }); + }); + + describe('|isArray|', () => { + it('should verify if array', () => { + expect(CollectionsUtil.isArray(testObj.arr)).toBeTruthy(); + expect(CollectionsUtil.isArray(testObj.arrEmpty)).toBeTruthy(); + + expect(CollectionsUtil.isArray(testObj.num)).toBeFalsy(); + expect(CollectionsUtil.isArray(testObj.str)).toBeFalsy(); + expect(CollectionsUtil.isArray(testObj.obj)).toBeFalsy(); + expect(CollectionsUtil.isArray(testObj.objNoProp)).toBeFalsy(); + expect(CollectionsUtil.isArray(testObj.undef)).toBeFalsy(); + expect(CollectionsUtil.isArray(testObj.nullVal)).toBeFalsy(); + expect(CollectionsUtil.isArray(testObj.bool)).toBeFalsy(); + expect(CollectionsUtil.isArray(testObj.nan)).toBeFalsy(); + expect(CollectionsUtil.isArray(testObj.func)).toBeFalsy(); + }); + }); + + describe('|isArrayEmpty|', () => { + it('should verify will return true when value is not Array', () => { + expect(CollectionsUtil.isArrayEmpty(testObj.num)).toBeTruthy(); + expect(CollectionsUtil.isArrayEmpty(testObj.str)).toBeTruthy(); + expect(CollectionsUtil.isArrayEmpty(testObj.obj)).toBeTruthy(); + expect(CollectionsUtil.isArrayEmpty(testObj.objNoProp)).toBeTruthy(); + expect(CollectionsUtil.isArrayEmpty(testObj.undef)).toBeTruthy(); + expect(CollectionsUtil.isArrayEmpty(testObj.nullVal)).toBeTruthy(); + expect(CollectionsUtil.isArrayEmpty(testObj.bool)).toBeTruthy(); + expect(CollectionsUtil.isArrayEmpty(testObj.nan)).toBeTruthy(); + expect(CollectionsUtil.isArrayEmpty(testObj.func)).toBeTruthy(); + }); + + it(`should verify will return true when value is Array and doesn't have elements`, () => { + expect(CollectionsUtil.isArrayEmpty(testObj.arrEmpty)).toBeTruthy(); + }); + + it('should verify will return false when value is Array and has elements', () => { + expect(CollectionsUtil.isArrayEmpty(testObj.arr)).toBeFalsy(); + }); + }); + + describe('|isNull|', () => { + it('should verify if null', () => { + expect(CollectionsUtil.isNull(testObj.nullVal)).toBeTruthy(); + + expect(CollectionsUtil.isNull(testObj.num)).toBeFalsy(); + expect(CollectionsUtil.isNull(testObj.str)).toBeFalsy(); + expect(CollectionsUtil.isNull(testObj.arr)).toBeFalsy(); + expect(CollectionsUtil.isNull(testObj.arrEmpty)).toBeFalsy(); + expect(CollectionsUtil.isNull(testObj.obj)).toBeFalsy(); + expect(CollectionsUtil.isNull(testObj.objNoProp)).toBeFalsy(); + expect(CollectionsUtil.isNull(testObj.undef)).toBeFalsy(); + expect(CollectionsUtil.isNull(testObj.bool)).toBeFalsy(); + expect(CollectionsUtil.isNull(testObj.nan)).toBeFalsy(); + expect(CollectionsUtil.isNull(testObj.func)).toBeFalsy(); + }); + }); + + describe('|isNaN|', () => { + it('should verify if nan', () => { + expect(CollectionsUtil.isNaN(testObj.nan)).toBeTruthy(); + + expect(CollectionsUtil.isNaN(testObj.func)).toBeFalsy(); + expect(CollectionsUtil.isNaN(testObj.nullVal)).toBeFalsy(); + expect(CollectionsUtil.isNaN(testObj.num)).toBeFalsy(); + expect(CollectionsUtil.isNaN(testObj.str)).toBeFalsy(); + expect(CollectionsUtil.isNaN(testObj.arr)).toBeFalsy(); + expect(CollectionsUtil.isNaN(testObj.arrEmpty)).toBeFalsy(); + expect(CollectionsUtil.isNaN(testObj.obj)).toBeFalsy(); + expect(CollectionsUtil.isNaN(testObj.objNoProp)).toBeFalsy(); + expect(CollectionsUtil.isNaN(testObj.undef)).toBeFalsy(); + expect(CollectionsUtil.isNaN(testObj.bool)).toBeFalsy(); + }); + }); + + describe('|isDate|', () => { + it('should verify if Date', () => { + expect(CollectionsUtil.isDate(testObj.date)).toBeTruthy(); + + expect(CollectionsUtil.isDate(testObj.nan)).toBeFalsy(); + expect(CollectionsUtil.isDate(testObj.func)).toBeFalsy(); + expect(CollectionsUtil.isDate(testObj.nullVal)).toBeFalsy(); + expect(CollectionsUtil.isDate(testObj.num)).toBeFalsy(); + expect(CollectionsUtil.isDate(testObj.str)).toBeFalsy(); + expect(CollectionsUtil.isDate(testObj.arr)).toBeFalsy(); + expect(CollectionsUtil.isDate(testObj.arrEmpty)).toBeFalsy(); + expect(CollectionsUtil.isDate(testObj.obj)).toBeFalsy(); + expect(CollectionsUtil.isDate(testObj.objNoProp)).toBeFalsy(); + expect(CollectionsUtil.isDate(testObj.undef)).toBeFalsy(); + expect(CollectionsUtil.isDate(testObj.bool)).toBeFalsy(); + }); + }); + + describe('|isPrimitiveOrDate|', () => { + it('should verify if Date or Primitive', () => { + expect(CollectionsUtil.isPrimitiveOrDate(testObj.date)).toBeTruthy(); + expect(CollectionsUtil.isPrimitiveOrDate(testObj.bool)).toBeTruthy(); + expect(CollectionsUtil.isPrimitiveOrDate(testObj.num)).toBeTruthy(); + expect(CollectionsUtil.isPrimitiveOrDate(testObj.nan)).toBeTruthy(); + expect(CollectionsUtil.isPrimitiveOrDate(testObj.str)).toBeTruthy(); + expect(CollectionsUtil.isPrimitiveOrDate(testObj.undef)).toBeTruthy(); + + expect(CollectionsUtil.isPrimitiveOrDate(testObj.arr)).toBeFalsy(); + expect(CollectionsUtil.isPrimitiveOrDate(testObj.arrEmpty)).toBeFalsy(); + expect(CollectionsUtil.isPrimitiveOrDate(testObj.obj)).toBeFalsy(); + expect(CollectionsUtil.isPrimitiveOrDate(testObj.objNoProp)).toBeFalsy(); + expect(CollectionsUtil.isPrimitiveOrDate(testObj.nullVal)).toBeFalsy(); + expect(CollectionsUtil.isPrimitiveOrDate(testObj.func)).toBeFalsy(); + }); + }); + + describe('|isFunction|', () => { + it('should verify if function', () => { + expect(CollectionsUtil.isFunction(testObj.func)).toBeTruthy(); + + expect(CollectionsUtil.isFunction(testObj.nullVal)).toBeFalsy(); + expect(CollectionsUtil.isFunction(testObj.num)).toBeFalsy(); + expect(CollectionsUtil.isFunction(testObj.str)).toBeFalsy(); + expect(CollectionsUtil.isFunction(testObj.arr)).toBeFalsy(); + expect(CollectionsUtil.isFunction(testObj.arrEmpty)).toBeFalsy(); + expect(CollectionsUtil.isFunction(testObj.obj)).toBeFalsy(); + expect(CollectionsUtil.isFunction(testObj.objNoProp)).toBeFalsy(); + expect(CollectionsUtil.isFunction(testObj.undef)).toBeFalsy(); + expect(CollectionsUtil.isFunction(testObj.bool)).toBeFalsy(); + expect(CollectionsUtil.isFunction(testObj.nan)).toBeFalsy(); + }); + }); + + describe('|isBooleanAndTrue|', () => { + it('should verify if boolean and true', () => { + expect(CollectionsUtil.isBooleanAndTrue(testObj.bool)).toBeTruthy(); + expect(CollectionsUtil.isBooleanAndTrue(false)).toBeFalsy(); + + expect(CollectionsUtil.isBooleanAndTrue(testObj.num)).toBeFalsy(); + expect(CollectionsUtil.isBooleanAndTrue(testObj.str)).toBeFalsy(); + expect(CollectionsUtil.isBooleanAndTrue(testObj.arr)).toBeFalsy(); + expect(CollectionsUtil.isBooleanAndTrue(testObj.arrEmpty)).toBeFalsy(); + expect(CollectionsUtil.isBooleanAndTrue(testObj.obj)).toBeFalsy(); + expect(CollectionsUtil.isBooleanAndTrue(testObj.objNoProp)).toBeFalsy(); + expect(CollectionsUtil.isBooleanAndTrue(testObj.undef)).toBeFalsy(); + expect(CollectionsUtil.isBooleanAndTrue(testObj.nullVal)).toBeFalsy(); + expect(CollectionsUtil.isBooleanAndTrue(testObj.nan)).toBeFalsy(); + expect(CollectionsUtil.isBooleanAndTrue(testObj.func)).toBeFalsy(); + }); + }); + + describe('|isLiteralObjectOrNull|', () => { + it('should verify if literal object including null', () => { + expect(CollectionsUtil.isLiteralObjectOrNull(testObj.obj)).toBeTruthy(); + expect(CollectionsUtil.isLiteralObjectOrNull(testObj.objNoProp)).toBeTruthy(); + expect(CollectionsUtil.isLiteralObjectOrNull(testObj.nullVal)).toBeTruthy(); + + expect(CollectionsUtil.isLiteralObjectOrNull(testObj.func)).toBeFalsy(); + expect(CollectionsUtil.isLiteralObjectOrNull(testObj.num)).toBeFalsy(); + expect(CollectionsUtil.isLiteralObjectOrNull(testObj.str)).toBeFalsy(); + expect(CollectionsUtil.isLiteralObjectOrNull(testObj.arr)).toBeFalsy(); + expect(CollectionsUtil.isLiteralObjectOrNull(testObj.arrEmpty)).toBeFalsy(); + expect(CollectionsUtil.isLiteralObjectOrNull(testObj.undef)).toBeFalsy(); + expect(CollectionsUtil.isLiteralObjectOrNull(testObj.bool)).toBeFalsy(); + expect(CollectionsUtil.isLiteralObjectOrNull(testObj.nan)).toBeFalsy(); + }); + }); + + describe('|isLiteralObject|', () => { + it('should verify if literal object', () => { + expect(CollectionsUtil.isLiteralObject(testObj.obj)).toBeTruthy(); + expect(CollectionsUtil.isLiteralObject(testObj.objNoProp)).toBeTruthy(); + + expect(CollectionsUtil.isLiteralObject(testObj.func)).toBeFalsy(); + expect(CollectionsUtil.isLiteralObject(testObj.num)).toBeFalsy(); + expect(CollectionsUtil.isLiteralObject(testObj.str)).toBeFalsy(); + expect(CollectionsUtil.isLiteralObject(testObj.arr)).toBeFalsy(); + expect(CollectionsUtil.isLiteralObject(testObj.arrEmpty)).toBeFalsy(); + expect(CollectionsUtil.isLiteralObject(testObj.undef)).toBeFalsy(); + expect(CollectionsUtil.isLiteralObject(testObj.nullVal)).toBeFalsy(); + expect(CollectionsUtil.isLiteralObject(testObj.bool)).toBeFalsy(); + expect(CollectionsUtil.isLiteralObject(testObj.nan)).toBeFalsy(); + }); + }); + + describe('|isLiteralObjectWithProperties|', () => { + it('should verify if plain object with properties', () => { + expect(CollectionsUtil.isLiteralObjectWithProperties(testObj.obj)).toBeTruthy(); + + expect(CollectionsUtil.isLiteralObjectWithProperties(testObj.func)).toBeFalsy(); + expect(CollectionsUtil.isLiteralObjectWithProperties(testObj.num)).toBeFalsy(); + expect(CollectionsUtil.isLiteralObjectWithProperties(testObj.str)).toBeFalsy(); + expect(CollectionsUtil.isLiteralObjectWithProperties(testObj.arr)).toBeFalsy(); + expect(CollectionsUtil.isLiteralObjectWithProperties(testObj.arrEmpty)).toBeFalsy(); + expect(CollectionsUtil.isLiteralObjectWithProperties(testObj.objNoProp)).toBeFalsy(); + expect(CollectionsUtil.isLiteralObjectWithProperties(testObj.undef)).toBeFalsy(); + expect(CollectionsUtil.isLiteralObjectWithProperties(testObj.nullVal)).toBeFalsy(); + expect(CollectionsUtil.isLiteralObjectWithProperties(testObj.bool)).toBeFalsy(); + expect(CollectionsUtil.isLiteralObjectWithProperties(testObj.nan)).toBeFalsy(); + }); + }); + + describe('|isCollection|', () => { + it('should verify if value is type of Collection', () => { + expect(CollectionsUtil.isCollection(testObj.obj)).toBeTruthy(); + expect(CollectionsUtil.isCollection(testObj.arr)).toBeTruthy(); + expect(CollectionsUtil.isCollection(testObj.arrEmpty)).toBeTruthy(); + expect(CollectionsUtil.isCollection(testObj.objNoProp)).toBeTruthy(); + expect(CollectionsUtil.isCollection(testObj.map)).toBeTruthy(); + expect(CollectionsUtil.isCollection(testObj.weakMap)).toBeTruthy(); + expect(CollectionsUtil.isCollection(testObj.set)).toBeTruthy(); + + expect(CollectionsUtil.isCollection(testObj.func)).toBeFalsy(); + expect(CollectionsUtil.isCollection(testObj.num)).toBeFalsy(); + expect(CollectionsUtil.isCollection(testObj.str)).toBeFalsy(); + expect(CollectionsUtil.isCollection(testObj.undef)).toBeFalsy(); + expect(CollectionsUtil.isCollection(testObj.nullVal)).toBeFalsy(); + expect(CollectionsUtil.isCollection(testObj.bool)).toBeFalsy(); + expect(CollectionsUtil.isCollection(testObj.nan)).toBeFalsy(); + }); + }); + + describe('|isObjectNotNull|', () => { + it('should verify if value is Object but not null', () => { + expect(CollectionsUtil.isObjectNotNull(testObj.obj)).toBeTruthy(); + expect(CollectionsUtil.isObjectNotNull(testObj.arr)).toBeTruthy(); + expect(CollectionsUtil.isObjectNotNull(testObj.arrEmpty)).toBeTruthy(); + expect(CollectionsUtil.isObjectNotNull(testObj.objNoProp)).toBeTruthy(); + expect(CollectionsUtil.isObjectNotNull(testObj.map)).toBeTruthy(); + expect(CollectionsUtil.isObjectNotNull(testObj.weakMap)).toBeTruthy(); + expect(CollectionsUtil.isObjectNotNull(testObj.set)).toBeTruthy(); + + expect(CollectionsUtil.isObjectNotNull(testObj.func)).toBeFalsy(); + expect(CollectionsUtil.isObjectNotNull(testObj.num)).toBeFalsy(); + expect(CollectionsUtil.isObjectNotNull(testObj.str)).toBeFalsy(); + expect(CollectionsUtil.isObjectNotNull(testObj.undef)).toBeFalsy(); + expect(CollectionsUtil.isObjectNotNull(testObj.nullVal)).toBeFalsy(); + expect(CollectionsUtil.isObjectNotNull(testObj.bool)).toBeFalsy(); + expect(CollectionsUtil.isObjectNotNull(testObj.nan)).toBeFalsy(); + }); + }); + + describe('|isObjectWithProperties|', () => { + it('should verify if value is Object and has properties', () => { + expect(CollectionsUtil.isObjectWithProperties(testObj.obj)).toBeTruthy(); + expect(CollectionsUtil.isObjectWithProperties(testObj.arr)).toBeTruthy(); + + expect(CollectionsUtil.isObjectWithProperties(testObj.map)).toBeFalsy(); + expect(CollectionsUtil.isObjectWithProperties(testObj.weakMap)).toBeFalsy(); + expect(CollectionsUtil.isObjectWithProperties(testObj.set)).toBeFalsy(); + expect(CollectionsUtil.isObjectWithProperties(testObj.objNoProp)).toBeFalsy(); + expect(CollectionsUtil.isObjectWithProperties(testObj.arrEmpty)).toBeFalsy(); + expect(CollectionsUtil.isObjectWithProperties(testObj.func)).toBeFalsy(); + expect(CollectionsUtil.isObjectWithProperties(testObj.num)).toBeFalsy(); + expect(CollectionsUtil.isObjectWithProperties(testObj.str)).toBeFalsy(); + expect(CollectionsUtil.isObjectWithProperties(testObj.undef)).toBeFalsy(); + expect(CollectionsUtil.isObjectWithProperties(testObj.nullVal)).toBeFalsy(); + expect(CollectionsUtil.isObjectWithProperties(testObj.bool)).toBeFalsy(); + expect(CollectionsUtil.isObjectWithProperties(testObj.nan)).toBeFalsy(); + }); + }); + + describe('|dateISO|', () => { + it('should verify will return Date in ISO format', () => { + // When + const res = CollectionsUtil.dateISO(); + + // Then + // eslint-disable-next-line max-len + expect(/^(-?(?:[1-9][0-9]*)?[0-9]{4})-(1[0-2]|0[1-9])-(3[01]|0[1-9]|[12][0-9])T(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])(\.[0-9]+)?(Z|[+-](?:2[0-3]|[01][0-9]):[0-5][0-9])?$/.test(res)); + }); + }); + + describe('|isEqual|', () => { + it('should verify will return true after deep comparison of two objects', () => { + // Given + const obj1 = { + project: 'aProject', + country: 'aCountry', + teams: ['aTeam', 'bTeam', 'cTeam', 'dTeam'], + managers: { + aTeam: 'aManager', + bTeam: 'bManager' + }, + prod: true, + age: 3, + deps: null + }; + const obj2 = { + project: 'aProject', + country: 'aCountry', + teams: ['aTeam', 'bTeam', 'cTeam', 'dTeam'], + managers: { + aTeam: 'aManager', + bTeam: 'bManager' + }, + prod: true, + age: 3, + deps: null + }; + + // When + const r = CollectionsUtil.isEqual(obj1, obj2); + + // Then + expect(r).toBeTrue(); + }); + }); + + describe('|cloneDeep|', () => { + it('should verify will deep clone provided Object', () => { + // Given + class A { + constructor(protected readonly name: string) { + } + } + + class B extends A { + constructor(name: string, + private readonly surname: string) { + super(name); + } + + getName(): string { + return this.name; + } + + getSurname(): string { + return this.surname; + } + } + + const instance = new B('aUser', 'bUser'); + + // When + const cloned = CollectionsUtil.cloneDeep(instance); + + // Then + expect(cloned).not.toBe(instance); + expect(cloned).toEqual(instance); + expect(cloned.getName()).toEqual('aUser'); + expect(cloned.getSurname()).toEqual('bUser'); + }); + + it('should verify will deep clone provided Array', () => { + // Given + const arr = ['aUser', 'bUser', 'cUser']; + + // When + const cloned = CollectionsUtil.cloneDeep(arr); + + // Then + expect(cloned).not.toBe(arr); + expect(cloned).toEqual(arr); + }); + }); + + describe('|objectValues|', () => { + it('should verify will return Array of values from provided Object', () => { + // Given + const obj = { + prop1: 1, + prop2: 2, + prop3: 3, + prop4: 4, + prop5: 5 + }; + + // When + const values = CollectionsUtil.objectValues(obj); + + // Then + expect(values).toEqual([1, 2, 3, 4, 5]); + }); + }); + + describe('|transformMapToObject|', () => { + it('should verify will remap provided Map to Object', () => { + // Given + const map = new Map(); + map.set('name', 'aName'); + map.set('age', 35); + map.set('surname', 'aSurname'); + map.set('city', 'aCity'); + map.set('graduated', true); + + // When + const obj = CollectionsUtil.transformMapToObject(map); + + // Then + expect(obj).toEqual({ + name: 'aName', + age: 35, + surname: 'aSurname', + city: 'aCity', + graduated: true + }); + }); + }); + + describe('|transformObjectToMap|', () => { + it('should verify will remap provided Object to Map', () => { + // Given + const assertionMap = new Map(); + assertionMap.set('name', 'aName'); + assertionMap.set('age', 35); + assertionMap.set('surname', 'aSurname'); + assertionMap.set('city', 'aCity'); + assertionMap.set('graduated', true); + + const obj: { [key: string]: any } = { + name: 'aName', + age: 35, + surname: 'aSurname', + city: 'aCity', + graduated: true + }; + + // When + const map = CollectionsUtil.transformObjectToMap(obj); + + // Then + expect(map).toEqual(assertionMap); + }); + }); + + describe('|iterateObject|', () => { + it('should verify method will iterate all properties from Object', () => { + // Given + const obj = { + prop1: 'prop1', + prop2: [], + prop3: {}, + prop4: true + }; + + // When/Then + let cnt = 0; + const r = CollectionsUtil.iterateObject(obj, (value, key, collection) => { + expect(obj[key]).toBe(value); + expect(obj).toBe(collection); + cnt++; + }); + + expect(cnt).toEqual(4); + expect(r).toBe(obj); + }); + + it('should verify will return null immediately if there is no iterator provided', () => { + // Given + const obj = { + prop1: 'prop1', + prop2: [], + prop3: {}, + prop4: true + }; + + // When + const r = CollectionsUtil.iterateObject(obj, null); + + // Then + expect(r).toEqual(null); + }); + + it('should verify will return null immediately if value is not literal object', () => { + // Given + const obj = [] as unknown; + + // When/Then + let cnt = 0; + const r = CollectionsUtil.iterateObject(obj as Record, () => { + cnt++; + }); + + expect(cnt).toEqual(0); + expect(r).toEqual(null); + }); + + it('should verify will return null immediately if value is null', () => { + // Given + const obj = null; + + // When/Then + let cnt = 0; + const r = CollectionsUtil.iterateObject(obj, () => { + cnt++; + }); + + expect(cnt).toEqual(0); + expect(r).toEqual(null); + }); + + it('should verify will return null immediately if value is null', () => { + // When/Then + let cnt = 0; + const r = CollectionsUtil.iterateObject(null, () => { + cnt++; + }, 'objectLookLike'); + + expect(cnt).toEqual(0); + expect(r).toEqual(null); + }); + + it('should verify will break iteration if iterator returns false or -1', () => { + // Given + const obj = { + prop1: 'prop1', + prop2: [], + prop3: {}, + prop4: true + }; + + // When/Then + let cnt1 = 0; + // @ts-ignore + const r1 = CollectionsUtil.iterateObject(obj, (value, key, collection) => { + if (cnt1 === 2) { + return false; + } + + expect(obj[key]).toBe(value); + expect(obj).toBe(collection); + cnt1++; + }); + let cnt2 = 0; + // @ts-ignore + const r2 = CollectionsUtil.iterateObject(obj, (value, key, collection) => { + if (cnt2 === 3) { + return -1; + } + + expect(obj[key]).toBe(value); + expect(obj).toBe(collection); + cnt2++; + }); + + expect(cnt2).toEqual(3); + expect(r1).toBe(obj); + expect(r2).toBe(obj); + }); + }); + + describe('|objectPairs|', () => { + it('should verify will return Array of pairs [key, value] from provided Object', () => { + // Given + const obj: { [key: string]: any } = { + prop1: 1, + prop2: 2, + prop3: 3, + prop4: 4, + prop5: 5 + }; + + // When + const values = CollectionsUtil.objectPairs(obj); + + // Then + expect(values).toEqual([ + ['prop1', 1], + ['prop2', 2], + ['prop3', 3], + ['prop4', 4], + ['prop5', 5] + ]); + }); + + it('should verify will return empty Array if provided value is not literal Object', () => { + // When + const values = CollectionsUtil.objectPairs(null); + + // Then + expect(values).toEqual([]); + }); + }); + + describe('|getObjectPropertyDescriptor|', () => { + it('should verify will get Object property descriptor', () => { + // Given + const obj = { user: 'aUser' }; + + // When + const descriptor = CollectionsUtil.getObjectPropertyDescriptor(obj, 'user'); + + // Then + expect(descriptor).toEqual({ + value: 'aUser', + enumerable: true, + configurable: true, + writable: true + }); + }); + + it('should verify will return null if provided value is not Function or Object or key is not String', () => { + // When + const descriptor1 = CollectionsUtil.getObjectPropertyDescriptor(undefined, 'user'); + const descriptor2 = CollectionsUtil.getObjectPropertyDescriptor({}, null); + const descriptor3 = CollectionsUtil.getObjectPropertyDescriptor(function() { + // No-op. + }, null); + + // Then + expect(descriptor1).toEqual(null); + expect(descriptor2).toEqual(null); + expect(descriptor3).toEqual(null); + }); + }); + + describe('|iterateClassStatics|', () => { + it('should verify will return null when no class provided', () => { + // Given + let cnt = 0; + + // When + const result = CollectionsUtil.iterateClassStatics(null, (_descriptor, _key, _fn) => { + cnt++; + }); + + // Then + expect(result).toEqual(null); + expect(cnt).toEqual(0); + }); + }); + + describe('|areMapsEqual|', () => { + it('should verify will return true when maps are same reference', () => { + // Given + const map1 = new Map([['users', ['aUser', 'bUser', 'cUser']]]); + const map2 = map1; + + // When + const result = CollectionsUtil.areMapsEqual(map1, map2); + + // Then + expect(result).toBeTrue(); + }); + + it('should verify will return false when maps are different reference and different size', () => { + // Given + const map1 = new Map([['users', ['aUser', 'bUser', 'cUser']]]); + const map2 = new Map([['holders', ['aHolder', 'bHolder', 'cHolder', 'dHolder']]]); + + // When + const result = CollectionsUtil.areMapsEqual(map1, map2); + + // Then + expect(result).toBeFalse(); + }); + + // eslint-disable-next-line max-len + it('should verify will return false when maps are different reference, same size, same content, different order', () => { + // Given + const map1 = new Map([ + [ + 'users', + ['aUser', 'bUser', 'cUser'] + ], + [ + 'data', + [ + { person: 'aPerson', salary: 89087, age: 35 }, + { person: 'bPerson', salary: 78963, age: 31 }, + { person: 'cPerson', salary: 70664, age: 37 } + ] + ] + ]); + const map2 = new Map([ + [ + 'users', + ['cUser', 'bUser', 'aUser'] + ], + [ + 'data', + [ + { person: 'aPerson', salary: 89087, age: 35 }, + { person: 'bPerson', salary: 78963, age: 31 }, + { person: 'cPerson', salary: 70664, age: 37 } + ] + ] + ]); + const map3 = new Map([ + [ + 'users', + ['aUser', 'bUser', 'cUser'] + ], + [ + 'data', + [ + { person: 'cPerson', salary: 70664, age: 37 }, + { person: 'bPerson', salary: 78963, age: 31 }, + { person: 'aPerson', salary: 89087, age: 35 } + ] + ] + ]); + + // When + const result1 = CollectionsUtil.areMapsEqual(map1, map2); + const result2 = CollectionsUtil.areMapsEqual(map1, map3); + + // Then + expect(result1).toBeFalse(); + expect(result2).toBeFalse(); + }); + + it('should verify will return false when maps are different reference, same size and different content', () => { + // Given + const map1 = new Map([['users', ['aUser', 'bUser', 'cUser']]]); + const map2 = new Map([['users', ['aUser', 'bUser', 'dUser']]]); + const map3 = new Map([['holders', ['aHolder', 'cHolder', 'dHolder']]]); + const map4 = new Map([['val0', 1], ['val2', 2], ['val3', 3], ['val-undefined', undefined]]); + const map5 = new Map([['val0', 1], ['val2', 2], ['val3', 3], ['val4', 4]]); + + // When + const result1 = CollectionsUtil.areMapsEqual(map1, map2); + const result2 = CollectionsUtil.areMapsEqual(map1, map3); + const result3 = CollectionsUtil.areMapsEqual(map4, map5); + + // Then + expect(result1).toBeFalse(); + expect(result2).toBeFalse(); + expect(result3).toBeFalse(); + }); + + it('should verify will return true when maps are different reference, same size and same content', () => { + // Given + const map1 = new Map([ + [ + 'users', + ['aUser', 'bUser', 'cUser'] + ], + [ + 'data', + new Map([ + [ + 'emails', + ['aEmail', 'bEmail', 'cEmail'] + ], + [ + 'payments', + [ + { month: 'June', salary: 88900 }, + { month: 'July', salary: 98431 } + ] + ] + ]) + ] + ]); + const map2 = new Map([ + [ + 'users', + ['aUser', 'bUser', 'cUser'] + ], + [ + 'data', + new Map([ + [ + 'emails', + ['aEmail', 'bEmail', 'cEmail'] + ], + [ + 'payments', + [ + { month: 'June', salary: 88900 }, + { month: 'July', salary: 98431 } + ] + ] + ]) + ] + ]); + const map3 = new Map([['users', 'aUser']]); + const map4 = new Map([['users', 'aUser']]); + + // When + const result1 = CollectionsUtil.areMapsEqual(map1, map2); + const result2 = CollectionsUtil.areMapsEqual(map3, map4); + + // Then + expect(result1).toBeTrue(); + expect(result2).toBeTrue(); + }); + }); + + describe('|interpolateString|', () => { + it('should verify will interpolate provided string replacers', () => { + // Given + const text = 'User %s want to delete entity %s.'; + + // When + const res = CollectionsUtil.interpolateString(text, 'John', '25'); + + // Then + expect(res).toEqual('User John want to delete entity 25.'); + }); + + it('should verify will interpolate provided Replacer replacers', () => { + // Given + const text = 'Error {0} on line [1] because http request "%s%" failure.'; + + // When + const res = CollectionsUtil.interpolateString( + text, + { searchValue: '{0}', replaceValue: 'HttpResponseError' }, + { searchValue: '[1]', replaceValue: '27' }, + { searchValue: '%s%', replaceValue: 'http://xy.com/api/entity/29' } + ); + + // Then + expect(res).toEqual( + 'Error HttpResponseError on line 27 because http request "http://xy.com/api/entity/29" failure.' + ); + }); + }); + }); + }); +}); diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/utils/collections/collections-util.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/utils/collections/collections-util.ts new file mode 100644 index 0000000000..71ab20b3e4 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/utils/collections/collections-util.ts @@ -0,0 +1,438 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import { cloneDeep, isEqual } from 'lodash'; + +import { Replacer } from '../../common'; + +import { Collections, IteratorFnResult, LiteralObjectOrNull, Nil, ObjectIterator, Primitives, PrimitivesDate, Subtract } from '../model'; + +/** + * ** Utility Class for Collections. + */ +export class CollectionsUtil { + /** + * ** Check if value is of type undefined. + */ + static isUndefined(value: any): value is undefined { + return typeof value === 'undefined'; + } + + /** + * ** Check if value has value null. + */ + static isNull(value: any): value is null { + return value === null; + } + + /** + * ** Check if value is undefined or null. + */ + static isNil(value: any): value is Nil { + return CollectionsUtil.isNull(value) || CollectionsUtil.isUndefined(value); + } + + /** + * ** Check if value is defined (opposite of isNil). + * + * - Not null. + * - Not undefined. + */ + static isDefined(value: T): boolean { + return !CollectionsUtil.isNull(value) && !CollectionsUtil.isUndefined(value); + } + + /** + * ** Check if value T is of type Number. + */ + static isNumber(num: T | any): num is number { + return typeof num === 'number'; + } + + /** + * ** Check if value T is of type String. + */ + static isString(str: T | any): str is string { + return typeof str === 'string'; + } + + /** + * ** Check if value T is of type Boolean. + */ + static isBoolean(bool: T | any): bool is boolean { + return typeof bool === 'boolean'; + } + + /** + * ** Check if value is Primitive. + * + * - String, Number, Boolean, null or undefined. + */ + static isPrimitive(value: any): value is (Primitives | Nil) { + return CollectionsUtil.isString(value) || + CollectionsUtil.isBoolean(value) || + CollectionsUtil.isNumber(value) || + CollectionsUtil.isUndefined(value); + } + + /** + * ** Check if value is NaN. + */ + static isNaN(value: T | any): boolean { + return Number.isNaN(value); + } + + /** + * ** Check if value is of type Date. + */ + static isDate(value: any): value is Date { + return value instanceof Date; + } + + /** + * ** Check if value is Primitive or Date. + * + * - String, Number, Boolean, null, undefined or Date. + */ + static isPrimitiveOrDate(value: T | any): value is (Primitives | Nil | Date) { + return CollectionsUtil.isPrimitive(value) || CollectionsUtil.isDate(value); + } + + /** + * ** Check if value T is a reference that points to function (Class/Method). + */ + // eslint-disable-next-line + static isFunction any>(value: T | any): value is (...args: any[]) => any { + return typeof value === 'function'; + } + + /** + * ** Check if value T is of type Object. + */ + static isObject>(obj: T | any): obj is Record { + return typeof obj === 'object'; + } + + /** + * ** Check if value T is instance of Array. + */ + static isArray(arr: T | any): arr is (T extends any[] ? T : any[]) { + return arr instanceof Array; + } + + /** + * ** Check if value T is not instance of Array or there are no elements in Array. + */ + static isArrayEmpty(arr: T | any): boolean { + return !CollectionsUtil.isArray(arr) || arr.length === 0; + } + + /** + * ** Check if value is Map. + */ + static isMap(obj: Map | any): obj is Map { + return obj instanceof Map; + } + + /** + * ** Check if value is WeakMap. + */ + static isWeakMap(obj: WeakMap | any): obj is WeakMap { + return obj instanceof WeakMap; + } + + /** + * ** Check if value is Set. + */ + static isSet(obj: Set | any): obj is Set { + return obj instanceof Set; + } + + /** + * ** Check if value is Collection (literal Object, Array, Map, WeakMap or Set). + */ + static isCollection(obj: Collections | any): obj is Collections { + return CollectionsUtil.isArray(obj) || + CollectionsUtil.isMap(obj) || + CollectionsUtil.isWeakMap(obj) || + CollectionsUtil.isSet(obj) || + CollectionsUtil.isLiteralObject(obj); + } + + /** + * ** Check if value is of type Object and not null. + */ + static isObjectNotNull>(obj: T | any): + obj is Record & Subtract, null> { + + return CollectionsUtil.isObject(obj) && !CollectionsUtil.isNull(obj); + } + + /** + * ** Check if some variable is of type Boolean and is true. + */ + static isBooleanAndTrue(bool: boolean | any): boolean { + return CollectionsUtil.isBoolean(bool) && bool; + } + + /** + * ** Check if value is literal Object or null. + * + * - Not an Array, Map, WeakMap or Set. + */ + static isLiteralObjectOrNull>(obj: T | any): obj is LiteralObjectOrNull { + return CollectionsUtil.isObject(obj) && + !CollectionsUtil.isArray(obj) && + !CollectionsUtil.isMap(obj) && + !CollectionsUtil.isWeakMap(obj) && + !CollectionsUtil.isSet(obj); + } + + /** + * ** Check if provided value is literal Object. + * + * - Not and Array, Map, WeakMap, Set or null. + */ + static isLiteralObject>(obj: T | any): boolean { + return CollectionsUtil.isLiteralObjectOrNull(obj) && !CollectionsUtil.isNull(obj); + } + + /** + * ** Check if value is Object and has properties. + */ + static isObjectWithProperties>(obj: T | any): boolean { + return CollectionsUtil.isObjectNotNull(obj) && Object.keys(obj).length > 0; + } + + /** + * ** Return current Date in ISO string format. + */ + static dateISO(): string { + return new Date().toISOString(); + } + + /** + * ** Performs deep comparison between two values to determine if the are equivalent. + */ + static isEqual(value1: any, value2: any): boolean { + return isEqual(value1, value2); + } + + /** + * ** Create recursive deep cloned value from provided one. + */ + static cloneDeep(value: T): T { + return cloneDeep(value); + } + + /** + * ** Iterates own enumerable properties in Object. + * + * - use Object.keys method for extraction, and executes provided iteratorFn. + * - if iteratorFn returns false or -1 it will break iteration. + * - all other return values continue until last property. + * + * - flag as third parameter is optional: + * - Without flag or with flag and has value 'plainObject, method will iterate only through literal Objects. + * - With flag and has value 'objectLookLike', method will try to iterate through everything + * that passes type value === 'object' (literal Object, Array, Map, Set, WeakMap, etc..). + */ + static iterateObject>(obj: T, + iteratorFn: ObjectIterator, + flag: 'plainObject' | 'objectLookLike' = 'plainObject'): T | null { + + if (!CollectionsUtil.isFunction(iteratorFn)) { + return null; + } + + if (flag === 'objectLookLike') { + if (!CollectionsUtil.isObjectNotNull(obj)) { + return null; + } + } else { + if (!CollectionsUtil.isLiteralObject(obj)) { + return null; + } + } + + const objectKeys = Object.keys(obj); + for (const key of objectKeys) { + const resultOfIteratorFn = iteratorFn(obj[key] as T[keyof T], key, obj); + if (resultOfIteratorFn === false || resultOfIteratorFn === -1) { + break; + } + } + + return obj; + } + + /** + * ** Check if value is Literal Object and has properties. + */ + static isLiteralObjectWithProperties>(obj: T | unknown): boolean { + return CollectionsUtil.isLiteralObject(obj) && Object.keys(obj).length > 0; + } + + /** + * ** Iterates over object properties and return Array of its values. + */ + static objectValues>(obj: T | null | undefined): Array { + const _result: Array = []; + + CollectionsUtil.iterateObject(obj, (value) => { + _result.push(value); + }); + + return _result; + } + + /** + * ** Transform given Map to Object. + */ + static transformMapToObject>(map: T): { [key: string]: unknown } { + const obj: { [key: string]: any } = {}; + + map.forEach((value, key) => obj[key] = value); + + return obj; + } + + /** + * ** Transform given Object to Map. + */ + static transformObjectToMap(obj: T): Map { + const map = new Map(); + + CollectionsUtil.iterateObject(obj, (value, key) => { + map.set(key, value); + }); + + return map; + } + + /** + * ** Iterates over object properties and return Array of its keys/values in pairs. + *

+ * - Returns Array of subArrays that have 2 elements each, first element key and second element value. + */ + static objectPairs>(obj: T | null | undefined): Array<[string, T[keyof T]]> { + if (!CollectionsUtil.isLiteralObject(obj)) { + return []; + } + + return Object.entries(obj) as Array<[string, T[keyof T]]>; + } + + /** + * ** Return own property Descriptor from provided object/function. + */ + static getObjectPropertyDescriptor>(obj: T, key: string): PropertyDescriptor { + if (!((CollectionsUtil.isFunction(obj) || CollectionsUtil.isObject(obj)) && CollectionsUtil.isString(key))) { + return null; + } + + return Object.getOwnPropertyDescriptor(obj, key); + } + + /** + * ** Iterates own enumerable properties (statics) of Function (Class). + * + * - use Object.getOwnPropertyDescriptors method for extraction, and executes provided iteratorFn. + * - if iteratorFn returns false or -1 will break iteration. + * - all other return values means continue until last property. + */ + static iterateClassStatics>(fn: T, + iteratorFn: (descriptor: PropertyDescriptor, + key: string, + fn: T) => IteratorFnResult) { + + if (!CollectionsUtil.isFunction(fn) || !CollectionsUtil.isFunction(iteratorFn)) { + return null; + } + + const descriptors = Object.getOwnPropertyDescriptors(fn); + CollectionsUtil.iterateObject( + descriptors, + (descriptor: PropertyDescriptor, key) => iteratorFn(descriptor, key, fn) + ); + + return fn; + } + + /** + * ** Check if two Maps are Equal. + * + * - They are equal if they have same references. + * - They are equal if they have same keys and same values for compared keys. + */ + static areMapsEqual(m1: Map, m2: Map): boolean { + const evaluateDeepComparison = () => { + for (const [key, val] of m1) { + const compareVal = m2.get(key); + + if (CollectionsUtil.isMap(val)) { + if (!CollectionsUtil.areMapsEqual(val, compareVal as Map)) { + return false; + } + + continue; + } + + if (!CollectionsUtil.isEqual(val, compareVal) || (CollectionsUtil.isUndefined(compareVal) && !m2.has(key))) { + return false; + } + } + + return true; + }; + + return CollectionsUtil.isMap(m1) + && CollectionsUtil.isMap(m2) + && ( + m1 === m2 + || ( + m1.size === m2.size + && evaluateDeepComparison() + ) + ); + } + + /** + * ** Interpolate string and replace while iterating through provided strings. + * + * - Replacers are strings that are replaced on every place where %s is found starting from index 0. + */ + static interpolateString(target: string, ...replacers: string[]): string; + /** + * ** Interpolate text and replace while iterating through provided replacers. + * + * - Replacers are objects ofType {@link Replacer} that are iterates and consumes, + * searchValue is matcher and replaceValue is value that is placed on match. + */ + static interpolateString(target: string, ...replacers: Array>): string; + /** + * @inheritDoc + */ + static interpolateString(target: string, ...replacers: string[] | Array>): string { + let response = target; + + replacers.forEach((replacer: string | Replacer) => { + if (CollectionsUtil.isString(replacer)) { + response = response.replace('%s', replacer); + } else { + response = response.replace( + replacer.searchValue, + replacer.replaceValue + ); + } + }); + + return response; + } +} diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/utils/collections/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/utils/collections/index.ts new file mode 100644 index 0000000000..5128426006 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/utils/collections/index.ts @@ -0,0 +1,8 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './collections-util'; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/utils/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/utils/index.ts new file mode 100644 index 0000000000..49a593f8bc --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/utils/index.ts @@ -0,0 +1,10 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './model'; +export * from './collections'; +export * from './url'; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/utils/model/data-source.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/utils/model/data-source.ts new file mode 100644 index 0000000000..3472fc5865 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/utils/model/data-source.ts @@ -0,0 +1,52 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +/* eslint-disable @typescript-eslint/no-explicit-any */ + +// Represent union of null and undefined. +export type Nil = null | undefined; +// Represent union of string, number or boolean. +export type Primitives = string | number | boolean; +// Represent union of Primitives and Date. +export type PrimitivesDate = Primitives | Date; +// Represent union of Primitives and Nil. +export type PrimitivesNil = Primitives | Nil; + +// Represent Literal object. +export type LiteralObject = { [p: string]: T }; +// Represent union of Literal object and null. +export type LiteralObjectOrNull = LiteralObject | null; +// Represent union of Literal object and null. +export type LiteralObjectNull = LiteralObject | null; +// Represent union of Primitives, Nil and Arrays of them. +export type PrimitivesNilArrays = PrimitivesNil | PrimitivesNil[]; +// Represent union of Primitives, Nil and Literal objects of them. +export type PrimitivesNilObject = PrimitivesNil | LiteralObject; + +// Represent union of LiteralObject, Array, Map, WeakMap and Set. +export type Collections = + LiteralObject + | T[] + | Map + | WeakMap + | Set + | { [key: string]: T }; + +// Extracts element from Array and its type. +export type ArrayElement> = ArrayType[number]; + +/** + * The state of a DataSource. + */ +export interface DataSource> { + /** + * ** Map of DataSource. + * + * - Each DataSource can be either a primitive, null or undefined, other DataSource or array of DataSources. + */ + readonly [key: string]: PrimitivesNilArrays | DataSource | Array> | T; +} diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/utils/model/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/utils/model/index.ts new file mode 100644 index 0000000000..a5d4cf7546 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/utils/model/index.ts @@ -0,0 +1,9 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './shorthand-types'; +export * from './data-source'; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/utils/model/shorthand-types.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/utils/model/shorthand-types.ts new file mode 100644 index 0000000000..046f7a2c10 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/utils/model/shorthand-types.ts @@ -0,0 +1,29 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import { ArrayElement } from './data-source'; + +// Iterator function for Object. +export type ObjectIterator = (value: TObject[keyof TObject], key: string, collection: TObject) => TResult; +// Iterator function for Array. +export type ArrayIterator = (value: TCollection, index: number, collection: TCollection[]) => TResult; +// Mapper function for Array. +export type ArrayMapper = (value: TCollection, index: number, collection: TCollection[]) => TResult; +// Accepted result from Iterator function. +export type IteratorFnResult = void | number | boolean; + +// Predicate function for Object. +export type ObjectPredicateFn = (value: TObject[keyof TObject], key?: string, collection?: TObject) => boolean; +// Predicate function for Array. +export type ArrayPredicateFn = (value: ArrayElement, index?: number, collection?: TArray) => boolean; +// Predicate function Generic. +export type PredicateFn = (value: T) => boolean; + +// Subtract type from something. +export type Subtract = T & Exclude; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/utils/public-api.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/utils/public-api.ts new file mode 100644 index 0000000000..f128589636 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/utils/public-api.ts @@ -0,0 +1,31 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +export { CollectionsUtil } from './collections'; +export { + ObjectIterator, + IteratorFnResult, + ArrayIterator, + ArrayMapper, + ArrayPredicateFn, + PredicateFn, + ObjectPredicateFn, + Subtract, + Nil, + Primitives, + Collections, + ArrayElement, + LiteralObjectOrNull, + LiteralObject, + PrimitivesDate, + PrimitivesNil, + LiteralObjectNull, + PrimitivesNilArrays, + PrimitivesNilObject +} from './model'; + +export { UrlUtil } from './url'; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/utils/url/index.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/utils/url/index.ts new file mode 100644 index 0000000000..7feba46d0c --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/utils/url/index.ts @@ -0,0 +1,8 @@ + + +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +export * from './url.util'; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/utils/url/url.util.spec.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/utils/url/url.util.spec.ts new file mode 100644 index 0000000000..16ad782b45 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/utils/url/url.util.spec.ts @@ -0,0 +1,38 @@ +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { TIE_SWAGGER_DOC_LOCATION, UrlUtil } from './url.util'; + +const TEST_VALUE_NO_FORWARD_SLASH = 'https://some_value'; + +const TEST_VALUE_WITH_FORWARD_SLASH = 'https://some_value/'; + +describe('UrlUtil', () => { + it('normalize Endpoint with Empty input', () => { + expect(UrlUtil.normalizeEndpoint(null)) + .toEqual(''); + }); + + it('normalize Endpoint with no Forward Slash at the end', () => { + expect(UrlUtil.normalizeEndpoint(TEST_VALUE_NO_FORWARD_SLASH)) + .toEqual(TEST_VALUE_WITH_FORWARD_SLASH); + }); + + it('normalize Endpoint with Forward Slash at the end', () => { + expect(UrlUtil.normalizeEndpoint(TEST_VALUE_WITH_FORWARD_SLASH)) + .toEqual(TEST_VALUE_WITH_FORWARD_SLASH); + }); + + it('constructTieSwaggerUiEndpoint with Empty Input', () => { + expect(UrlUtil.constructTieSwaggerUiEndpoint(null)) + .toEqual(TIE_SWAGGER_DOC_LOCATION); + }); + + it('constructTieSwaggerUiEndpoint with Valid Input', () => { + expect(UrlUtil.constructTieSwaggerUiEndpoint(TEST_VALUE_WITH_FORWARD_SLASH)) + .toEqual(TEST_VALUE_WITH_FORWARD_SLASH + TIE_SWAGGER_DOC_LOCATION); + }); + +}); diff --git a/projects/frontend/shared-components/gui/projects/shared/src/lib/utils/url/url.util.ts b/projects/frontend/shared-components/gui/projects/shared/src/lib/utils/url/url.util.ts new file mode 100644 index 0000000000..cca98a0cf4 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/lib/utils/url/url.util.ts @@ -0,0 +1,26 @@ +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +export const FORWARD_SLASH = '/'; + +export const TIE_SWAGGER_DOC_LOCATION = 'swagger-ui.html#'; + +export class UrlUtil { + static normalizeEndpoint(endPoint: string): string { + if (!endPoint) { + return ''; + } + + if (endPoint.endsWith(FORWARD_SLASH)) { + return endPoint; + } + + return endPoint + FORWARD_SLASH; + } + + static constructTieSwaggerUiEndpoint(endpointBasePath: string) { + return UrlUtil.normalizeEndpoint(endpointBasePath) + TIE_SWAGGER_DOC_LOCATION; + } +} diff --git a/projects/frontend/shared-components/gui/projects/shared/src/public-api.ts b/projects/frontend/shared-components/gui/projects/shared/src/public-api.ts new file mode 100644 index 0000000000..3cd472edc5 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/public-api.ts @@ -0,0 +1,30 @@ +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +/* + * Public API Surface of shared + */ + +// Utils public APIs. +export * from './lib/utils/public-api'; + +// Shared Module and public APIs. +export * from './lib/features/taurus-shared-features.module'; +export * from './lib/features/public-api'; + +// Common public APIs. +export * from './lib/common/public-api'; + +// Core/NgRx Modules and public APIs. +export * from './lib/core/taurus-shared-core.module'; +export * from './lib/core/ngrx/taurus-shared-ngrx.module'; +export * from './lib/core/ngrx/helper-modules'; +export * from './lib/core/public-api'; + +// Utils unit-testing APIs. +export * from './lib/unit-testing/public-api'; + +// Common components +export * from './lib/commons/public-api'; diff --git a/projects/frontend/shared-components/gui/projects/shared/src/test.ts b/projects/frontend/shared-components/gui/projects/shared/src/test.ts new file mode 100644 index 0000000000..714bd074d3 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/src/test.ts @@ -0,0 +1,31 @@ +/* + * Copyright 2021-2023 VMware, Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js'; +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context(path: string, deep?: boolean, filter?: RegExp): { + keys(): string[]; + (id: string): T; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment( + BrowserDynamicTestingModule, + platformBrowserDynamicTesting(), + { + teardown: { destroyAfterEach: false } + } +); +// Then we find all the tests. +const context = require.context('./', true, /\.spec\.ts$/); +// And load the modules. +context.keys().map(context); diff --git a/projects/frontend/shared-components/gui/projects/shared/tsconfig.lib.json b/projects/frontend/shared-components/gui/projects/shared/tsconfig.lib.json new file mode 100644 index 0000000000..31c97c29a7 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/tsconfig.lib.json @@ -0,0 +1,25 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "../../out-tsc/lib", + "target": "es2015", + "declaration": true, + "declarationMap": true, + "inlineSources": true, + "types": [], + "lib": [ + "dom", + "es2018" + ] + }, + "angularCompilerOptions": { + "enableIvy": true, + "skipTemplateCodegen": true, + "strictMetadataEmit": true, + "enableResourceInlining": true + }, + "exclude": [ + "src/test.ts", + "**/*.spec.ts" + ] +} diff --git a/projects/frontend/shared-components/gui/projects/shared/tsconfig.lib.prod.json b/projects/frontend/shared-components/gui/projects/shared/tsconfig.lib.prod.json new file mode 100644 index 0000000000..80bc6423cf --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/tsconfig.lib.prod.json @@ -0,0 +1,11 @@ +{ + "extends": "./tsconfig.lib.json", + "compilerOptions": { + "declarationMap": false + }, + "angularCompilerOptions": { + "compilationMode": "full", + "enableIvy": true, + "fullTemplateTypeCheck": true + } +} diff --git a/projects/frontend/shared-components/gui/projects/shared/tsconfig.spec.json b/projects/frontend/shared-components/gui/projects/shared/tsconfig.spec.json new file mode 100644 index 0000000000..f1c08bf277 --- /dev/null +++ b/projects/frontend/shared-components/gui/projects/shared/tsconfig.spec.json @@ -0,0 +1,16 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "../../out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "src/test.ts" + ], + "include": [ + "**/*.spec.ts", + "**/*.d.ts" + ] +} diff --git a/projects/frontend/shared-components/gui/proxy.config.json b/projects/frontend/shared-components/gui/proxy.config.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/projects/frontend/shared-components/gui/proxy.config.json @@ -0,0 +1 @@ +{} diff --git a/projects/frontend/shared-components/gui/tsconfig.json b/projects/frontend/shared-components/gui/tsconfig.json new file mode 100644 index 0000000000..c2d1b65669 --- /dev/null +++ b/projects/frontend/shared-components/gui/tsconfig.json @@ -0,0 +1,38 @@ +{ + "compileOnSave": false, + "compilerOptions": { + "baseUrl": "./", + "outDir": "./dist/out-tsc", + "forceConsistentCasingInFileNames": true, + "strict": false, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "sourceMap": true, + "declaration": false, + "downlevelIteration": true, + "allowSyntheticDefaultImports": true, + "experimentalDecorators": true, + "resolveJsonModule": true, + "esModuleInterop": true, + "moduleResolution": "node", + "importHelpers": true, + "target": "es2017", + "typeRoots": [ + "node_modules/@types" + ], + "module": "es2020", + "lib": [ + "es2020", + "dom" + ], + "paths": {} + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + } +} diff --git a/projects/frontend/shared-components/gui/version.txt b/projects/frontend/shared-components/gui/version.txt index 4a36342fca..8acdd82b76 100644 --- a/projects/frontend/shared-components/gui/version.txt +++ b/projects/frontend/shared-components/gui/version.txt @@ -1 +1 @@ -3.0.0 +0.0.1