From 68949daa0379b0165c1d716470a4c55bafd15a38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20B=C3=A4dorf?= Date: Wed, 21 Apr 2021 05:54:34 +0000 Subject: [PATCH] Feature/35521 ium capabilities (#9158) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Started experimenting with capabilities * Started adding akita store * Use current user store to filter projects * Work on integrating capabilities * Added capability checks * Fix project select * Working on ium specs * Change ng select option list format * Fix proejct search label * Fix some specs * Fix issue with principal select * Add spec for missing placeholder user capabilities * Add new capability specs, fix existing ones * Fix import * Update shrinkwrap * Add JSDoc deprecations Co-authored-by: Oliver Günther --- config/locales/js-en.yml | 1 + docker-compose.yml | 1 + frontend/npm-shrinkwrap.json | 840 +++++++++++++++++- frontend/package.json | 1 + .../ee-trial-form.component.ts | 2 +- ...ter-toggled-multiselect-value.component.ts | 2 +- frontend/src/app/components/states.service.ts | 6 +- .../work-package-filter-values.spec.ts | 2 +- .../work-package-filter-values.ts | 2 +- .../wp-static-queries.service.ts | 2 +- .../permissions/permissions.service.ts | 12 +- .../src/app/modules/apiv3/api-v3.service.ts | 7 + .../capabilities/apiv3-capabilities-paths.ts | 69 ++ .../capabilities/apiv3-capability-paths.ts} | 36 +- .../capabilities/capability-cache.service.ts | 48 + .../common/openproject-common.module.ts | 8 +- .../current-user/current-user.module.ts | 41 + .../current-user/current-user.query.ts | 21 + .../current-user/current-user.service.ts | 129 +++ .../current-user/current-user.store.ts | 58 ++ .../hal/resources/capability-resource.ts | 34 + .../invite-user.component.ts | 15 +- .../principal/principal-search.component.html | 8 +- .../principal/principal-search.component.ts | 95 +- .../project-search.component.html | 22 +- .../project-search.component.ts | 62 +- .../project-selection.component.ts | 59 +- .../summary/summary.component.ts | 1 + spec/features/users/invite_user_modal_spec.rb | 70 +- spec/support/browsers/chrome.rb | 19 +- .../components/users/invite_user_modal.rb | 6 + 31 files changed, 1478 insertions(+), 201 deletions(-) create mode 100644 frontend/src/app/modules/apiv3/endpoints/capabilities/apiv3-capabilities-paths.ts rename frontend/src/app/{components/user/current-user.service.ts => modules/apiv3/endpoints/capabilities/apiv3-capability-paths.ts} (67%) create mode 100644 frontend/src/app/modules/apiv3/endpoints/capabilities/capability-cache.service.ts create mode 100644 frontend/src/app/modules/current-user/current-user.module.ts create mode 100644 frontend/src/app/modules/current-user/current-user.query.ts create mode 100644 frontend/src/app/modules/current-user/current-user.service.ts create mode 100644 frontend/src/app/modules/current-user/current-user.store.ts create mode 100644 frontend/src/app/modules/hal/resources/capability-resource.ts diff --git a/config/locales/js-en.yml b/config/locales/js-en.yml index 585783cebea..382f5efbfa3 100644 --- a/config/locales/js-en.yml +++ b/config/locales/js-en.yml @@ -1050,6 +1050,7 @@ en: required: 'Please select a project' next_button: 'Next' no_results: 'No projects were found' + no_invite_rights: 'You are not allowed to invite members to this project' type: required: 'Please select the type to be invited' user: diff --git a/docker-compose.yml b/docker-compose.yml index 064e567a7a1..8672a19ada7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -147,6 +147,7 @@ services: CAPYBARA_APP_HOSTNAME: backend-test OPENPROJECT_CLI_PROXY: http://frontend-test:4200 OPENPROJECT_TESTING_NO_HEADLESS: "true" + OPENPROJECT_TESTING_AUTO_DEVTOOLS: "true" volumes: - ".:/home/dev/openproject" - "fedata-test:/home/dev/openproject/public/assets/frontend" diff --git a/frontend/npm-shrinkwrap.json b/frontend/npm-shrinkwrap.json index 825ee439b8b..67334d34c1b 100644 --- a/frontend/npm-shrinkwrap.json +++ b/frontend/npm-shrinkwrap.json @@ -21,6 +21,7 @@ "@angular/platform-browser": "~11.2.3", "@angular/platform-browser-dynamic": "~11.2.3", "@angular/router": "~11.2.3", + "@datorama/akita": "^6.1.3", "@fullcalendar/angular": "5.5.0", "@fullcalendar/core": "5.5.0", "@fullcalendar/daygrid": "5.5.0", @@ -2699,6 +2700,18 @@ "to-fast-properties": "^2.0.0" } }, + "node_modules/@datorama/akita": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/@datorama/akita/-/akita-6.1.3.tgz", + "integrity": "sha512-bruAfUxwLh/lYVwe8iErNwhTr1/Qf4B5VitR+RgeMNVzWx+9/N15+5/9hFVT5yCYMr5Iw8XBFG3cAb+tBm3NVg==", + "dependencies": { + "schematics-utilities": "^1.1.1" + }, + "peerDependencies": { + "rxjs": ">= 6.1.0 < 7", + "tslib": "^2.0.0" + } + }, "node_modules/@eslint/eslintrc": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.0.tgz", @@ -11533,6 +11546,11 @@ "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.15.tgz", "integrity": "sha512-rlrc3yU3+JNOpZ9zj5pQtxnx2THmvRykwL4Xlxoa8I9lHBlVbbyPhgyPMioxVZ4NqyxaVVtaJnzsyOidQIhyyQ==" }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=" + }, "node_modules/lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -11693,14 +11711,6 @@ "node": ">=6" } }, - "node_modules/make-dir/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "bin": { - "semver": "bin/semver" - } - }, "node_modules/make-error": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.5.tgz", @@ -12555,7 +12565,6 @@ "version": "2.5.0", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", - "dev": true, "dependencies": { "hosted-git-info": "^2.1.4", "resolve": "^1.10.0", @@ -12566,8 +12575,7 @@ "node_modules/normalize-package-data/node_modules/hosted-git-info": { "version": "2.8.8", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz", - "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==", - "dev": true + "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==" }, "node_modules/normalize-path": { "version": "3.0.0", @@ -12699,6 +12707,59 @@ "node": ">=10" } }, + "node_modules/npm-registry-client": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/npm-registry-client/-/npm-registry-client-8.6.0.tgz", + "integrity": "sha512-Qs6P6nnopig+Y8gbzpeN/dkt+n7IyVd8f45NTMotGk6Qo7GfBmzwYx6jRLoOOgKiMnaQfYxsuyQlD8Mc3guBhg==", + "dependencies": { + "concat-stream": "^1.5.2", + "graceful-fs": "^4.1.6", + "normalize-package-data": "~1.0.1 || ^2.0.0", + "npm-package-arg": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0", + "once": "^1.3.3", + "request": "^2.74.0", + "retry": "^0.10.0", + "safe-buffer": "^5.1.1", + "semver": "2 >=2.2.1 || 3.x || 4 || 5", + "slide": "^1.1.3", + "ssri": "^5.2.4" + }, + "optionalDependencies": { + "npmlog": "2 || ^3.1.0 || ^4.0.0" + } + }, + "node_modules/npm-registry-client/node_modules/hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==" + }, + "node_modules/npm-registry-client/node_modules/npm-package-arg": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-6.1.1.tgz", + "integrity": "sha512-qBpssaL3IOZWi5vEKUKW0cO7kzLeT+EQO9W8RsLOZf76KF9E/K9+wH0C7t06HXPpaH8WH5xF1MExLuCwbTqRUg==", + "dependencies": { + "hosted-git-info": "^2.7.1", + "osenv": "^0.1.5", + "semver": "^5.6.0", + "validate-npm-package-name": "^3.0.0" + } + }, + "node_modules/npm-registry-client/node_modules/retry": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.10.1.tgz", + "integrity": "sha1-52OI0heZLCUnUCQdPTlW/tmNj/Q=", + "engines": { + "node": "*" + } + }, + "node_modules/npm-registry-client/node_modules/ssri": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-5.3.0.tgz", + "integrity": "sha512-XRSIPqLij52MtgoQavH/x/dU1qVKtWUAAZeOHsR9c2Ddi4XerFy3mc1alf+dLJKl9EUIm/Ht+EowFkTUOA6GAQ==", + "dependencies": { + "safe-buffer": "^5.1.1" + } + }, "node_modules/npm-registry-fetch": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-9.0.0.tgz", @@ -13121,6 +13182,14 @@ "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", "integrity": "sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=" }, + "node_modules/os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/os-tmpdir": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", @@ -13129,6 +13198,15 @@ "node": ">=0.10.0" } }, + "node_modules/osenv": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", + "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", + "dependencies": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } + }, "node_modules/p-finally": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", @@ -15381,6 +15459,339 @@ "node": ">= 4" } }, + "node_modules/schematics-utilities": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/schematics-utilities/-/schematics-utilities-1.1.3.tgz", + "integrity": "sha512-5HnrH+MJkUmK7KfRpA457FY0BZatX2oxNts54P1347xlICCC7KjAh0r2Tue20Xfruw1mS3X5woTxa8od+JsqUA==", + "dependencies": { + "@angular-devkit/core": "^7.3.6", + "@angular-devkit/schematics": "^7.3.6", + "npm-registry-client": "^8.5.1", + "parse5": "^5.0.0", + "rxjs": "^6.4.0", + "typescript": "^3.3.3333" + } + }, + "node_modules/schematics-utilities/node_modules/@angular-devkit/core": { + "version": "7.3.10", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-7.3.10.tgz", + "integrity": "sha512-h8Yj2+UfBsPI7jZ8X88tImO/7RPgNWUcKF8Uq/J5eUSN6z0FMO0lluD4sM7X8aikb7RK8MwkwrqB/xfxvvkOow==", + "dependencies": { + "ajv": "6.9.1", + "chokidar": "2.0.4", + "fast-json-stable-stringify": "2.0.0", + "rxjs": "6.3.3", + "source-map": "0.7.3" + }, + "engines": { + "node": ">= 8.9.0", + "npm": ">= 5.5.1" + } + }, + "node_modules/schematics-utilities/node_modules/@angular-devkit/core/node_modules/rxjs": { + "version": "6.3.3", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.3.3.tgz", + "integrity": "sha512-JTWmoY9tWCs7zvIk/CvRjhjGaOd+OVBM987mxFo+OW66cGpdKjZcpmc74ES1sB//7Kl/PAe8+wEakuhG4pcgOw==", + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" + } + }, + "node_modules/schematics-utilities/node_modules/@angular-devkit/schematics": { + "version": "7.3.10", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-7.3.10.tgz", + "integrity": "sha512-LMTGQ8kJb80LjTttu0ZqWXddzYtDwjKtMKY9X0A60Iz8/wbGl0j+wYG7KAVoRF0JeieYXs8Dl9KWdjyJyvJ/RA==", + "dependencies": { + "@angular-devkit/core": "7.3.10", + "rxjs": "6.3.3" + }, + "engines": { + "node": ">= 8.9.0", + "npm": ">= 5.5.1" + } + }, + "node_modules/schematics-utilities/node_modules/@angular-devkit/schematics/node_modules/rxjs": { + "version": "6.3.3", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.3.3.tgz", + "integrity": "sha512-JTWmoY9tWCs7zvIk/CvRjhjGaOd+OVBM987mxFo+OW66cGpdKjZcpmc74ES1sB//7Kl/PAe8+wEakuhG4pcgOw==", + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" + } + }, + "node_modules/schematics-utilities/node_modules/ajv": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.9.1.tgz", + "integrity": "sha512-XDN92U311aINL77ieWHmqCcNlwjoP5cHXDxIxbf2MaPYuCXOHS7gHH8jktxeK5omgd52XbSTX6a4Piwd1pQmzA==", + "dependencies": { + "fast-deep-equal": "^2.0.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "node_modules/schematics-utilities/node_modules/anymatch": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", + "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", + "dependencies": { + "micromatch": "^3.1.4", + "normalize-path": "^2.1.1" + } + }, + "node_modules/schematics-utilities/node_modules/binary-extensions": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz", + "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/schematics-utilities/node_modules/braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dependencies": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/schematics-utilities/node_modules/braces/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/schematics-utilities/node_modules/chokidar": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.0.4.tgz", + "integrity": "sha512-z9n7yt9rOvIJrMhvDtDictKrkFHeihkNl6uWMmZlmL6tJtX9Cs+87oK+teBx+JIgzvbX3yZHT3eF8vpbDxHJXQ==", + "deprecated": "Chokidar 2 will break on node v14+. Upgrade to chokidar 3 with 15x less dependencies.", + "dependencies": { + "anymatch": "^2.0.0", + "async-each": "^1.0.0", + "braces": "^2.3.0", + "glob-parent": "^3.1.0", + "inherits": "^2.0.1", + "is-binary-path": "^1.0.0", + "is-glob": "^4.0.0", + "lodash.debounce": "^4.0.8", + "normalize-path": "^2.1.1", + "path-is-absolute": "^1.0.0", + "readdirp": "^2.0.0", + "upath": "^1.0.5" + }, + "optionalDependencies": { + "fsevents": "^1.2.2" + } + }, + "node_modules/schematics-utilities/node_modules/fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=" + }, + "node_modules/schematics-utilities/node_modules/fast-json-stable-stringify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", + "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" + }, + "node_modules/schematics-utilities/node_modules/fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "dependencies": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/schematics-utilities/node_modules/fill-range/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/schematics-utilities/node_modules/fsevents": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", + "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", + "deprecated": "fsevents 1 will break on node v14+ and could be using insecure binaries. Upgrade to fsevents 2.", + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "dependencies": { + "bindings": "^1.5.0", + "nan": "^2.12.1" + }, + "engines": { + "node": ">= 4.0" + } + }, + "node_modules/schematics-utilities/node_modules/glob-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", + "dependencies": { + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" + } + }, + "node_modules/schematics-utilities/node_modules/glob-parent/node_modules/is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "dependencies": { + "is-extglob": "^2.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/schematics-utilities/node_modules/is-binary-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", + "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", + "dependencies": { + "binary-extensions": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/schematics-utilities/node_modules/is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/schematics-utilities/node_modules/is-number/node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/schematics-utilities/node_modules/micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dependencies": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/schematics-utilities/node_modules/normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "dependencies": { + "remove-trailing-separator": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/schematics-utilities/node_modules/parse5": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz", + "integrity": "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==" + }, + "node_modules/schematics-utilities/node_modules/readdirp": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", + "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", + "dependencies": { + "graceful-fs": "^4.1.11", + "micromatch": "^3.1.10", + "readable-stream": "^2.0.2" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/schematics-utilities/node_modules/to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "dependencies": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/schematics-utilities/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, + "node_modules/schematics-utilities/node_modules/typescript": { + "version": "3.9.9", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.9.tgz", + "integrity": "sha512-kdMjTiekY+z/ubJCATUPlRDl39vXYiMV9iyeMuEuXZh2we6zz80uovNN2WlAxmmdE/Z/YQe+EbOEXB5RHEED3w==", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, "node_modules/screenfull": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/screenfull/-/screenfull-4.2.1.tgz", @@ -15403,9 +15814,9 @@ } }, "node_modules/semver": { - "version": "5.5.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.1.tgz", - "integrity": "sha512-PqpAxfrEhlSUWge8dwIp4tZnQ25DIOthpiaHNIthsjEFQD6EvqUKUDM7L8O2rShkFccYo1VjJR0coWfNkCubRw==", + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", "bin": { "semver": "bin/semver" } @@ -15663,6 +16074,14 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "node_modules/slide": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/slide/-/slide-1.1.6.tgz", + "integrity": "sha1-VusCfWW00tzmyy4tMsTUr8nh1wc=", + "engines": { + "node": "*" + } + }, "node_modules/smart-buffer": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.1.0.tgz", @@ -16151,7 +16570,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==", - "dev": true, "dependencies": { "spdx-expression-parse": "^3.0.0", "spdx-license-ids": "^3.0.0" @@ -16160,14 +16578,12 @@ "node_modules/spdx-exceptions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", - "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", - "dev": true + "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==" }, "node_modules/spdx-expression-parse": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", - "dev": true, "dependencies": { "spdx-exceptions": "^2.1.0", "spdx-license-ids": "^3.0.0" @@ -16176,8 +16592,7 @@ "node_modules/spdx-license-ids": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.7.tgz", - "integrity": "sha512-U+MTEOO0AiDzxwFvoa4JVnMV6mZlJKk2sBLt90s7G0Gd0Mlknc7kxEn3nuDPNZRta7O2uy8oLcZLVT+4sqNZHQ==", - "dev": true + "integrity": "sha512-U+MTEOO0AiDzxwFvoa4JVnMV6mZlJKk2sBLt90s7G0Gd0Mlknc7kxEn3nuDPNZRta7O2uy8oLcZLVT+4sqNZHQ==" }, "node_modules/spdy": { "version": "4.0.2", @@ -17915,7 +18330,6 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", - "dev": true, "dependencies": { "spdx-correct": "^3.0.0", "spdx-expression-parse": "^3.0.0" @@ -21562,6 +21976,14 @@ "to-fast-properties": "^2.0.0" } }, + "@datorama/akita": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/@datorama/akita/-/akita-6.1.3.tgz", + "integrity": "sha512-bruAfUxwLh/lYVwe8iErNwhTr1/Qf4B5VitR+RgeMNVzWx+9/N15+5/9hFVT5yCYMr5Iw8XBFG3cAb+tBm3NVg==", + "requires": { + "schematics-utilities": "^1.1.1" + } + }, "@eslint/eslintrc": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.0.tgz", @@ -28802,6 +29224,11 @@ "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.15.tgz", "integrity": "sha512-rlrc3yU3+JNOpZ9zj5pQtxnx2THmvRykwL4Xlxoa8I9lHBlVbbyPhgyPMioxVZ4NqyxaVVtaJnzsyOidQIhyyQ==" }, + "lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=" + }, "lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -28931,13 +29358,6 @@ "requires": { "pify": "^4.0.1", "semver": "^5.6.0" - }, - "dependencies": { - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" - } } }, "make-error": { @@ -29650,7 +30070,6 @@ "version": "2.5.0", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", - "dev": true, "requires": { "hosted-git-info": "^2.1.4", "resolve": "^1.10.0", @@ -29661,8 +30080,7 @@ "hosted-git-info": { "version": "2.8.8", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz", - "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==", - "dev": true + "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==" } } }, @@ -29763,6 +30181,56 @@ } } }, + "npm-registry-client": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/npm-registry-client/-/npm-registry-client-8.6.0.tgz", + "integrity": "sha512-Qs6P6nnopig+Y8gbzpeN/dkt+n7IyVd8f45NTMotGk6Qo7GfBmzwYx6jRLoOOgKiMnaQfYxsuyQlD8Mc3guBhg==", + "requires": { + "concat-stream": "^1.5.2", + "graceful-fs": "^4.1.6", + "normalize-package-data": "~1.0.1 || ^2.0.0", + "npm-package-arg": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0", + "npmlog": "2 || ^3.1.0 || ^4.0.0", + "once": "^1.3.3", + "request": "^2.74.0", + "retry": "^0.10.0", + "safe-buffer": "^5.1.1", + "semver": "2 >=2.2.1 || 3.x || 4 || 5", + "slide": "^1.1.3", + "ssri": "^5.2.4" + }, + "dependencies": { + "hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==" + }, + "npm-package-arg": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-6.1.1.tgz", + "integrity": "sha512-qBpssaL3IOZWi5vEKUKW0cO7kzLeT+EQO9W8RsLOZf76KF9E/K9+wH0C7t06HXPpaH8WH5xF1MExLuCwbTqRUg==", + "requires": { + "hosted-git-info": "^2.7.1", + "osenv": "^0.1.5", + "semver": "^5.6.0", + "validate-npm-package-name": "^3.0.0" + } + }, + "retry": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.10.1.tgz", + "integrity": "sha1-52OI0heZLCUnUCQdPTlW/tmNj/Q=" + }, + "ssri": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-5.3.0.tgz", + "integrity": "sha512-XRSIPqLij52MtgoQavH/x/dU1qVKtWUAAZeOHsR9c2Ddi4XerFy3mc1alf+dLJKl9EUIm/Ht+EowFkTUOA6GAQ==", + "requires": { + "safe-buffer": "^5.1.1" + } + } + } + }, "npm-registry-fetch": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-9.0.0.tgz", @@ -30104,11 +30572,25 @@ "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", "integrity": "sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=" }, + "os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=" + }, "os-tmpdir": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" }, + "osenv": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", + "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", + "requires": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } + }, "p-finally": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", @@ -31961,6 +32443,282 @@ "ajv-keywords": "^3.1.0" } }, + "schematics-utilities": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/schematics-utilities/-/schematics-utilities-1.1.3.tgz", + "integrity": "sha512-5HnrH+MJkUmK7KfRpA457FY0BZatX2oxNts54P1347xlICCC7KjAh0r2Tue20Xfruw1mS3X5woTxa8od+JsqUA==", + "requires": { + "@angular-devkit/core": "^7.3.6", + "@angular-devkit/schematics": "^7.3.6", + "npm-registry-client": "^8.5.1", + "parse5": "^5.0.0", + "rxjs": "^6.4.0", + "typescript": "^3.3.3333" + }, + "dependencies": { + "@angular-devkit/core": { + "version": "7.3.10", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-7.3.10.tgz", + "integrity": "sha512-h8Yj2+UfBsPI7jZ8X88tImO/7RPgNWUcKF8Uq/J5eUSN6z0FMO0lluD4sM7X8aikb7RK8MwkwrqB/xfxvvkOow==", + "requires": { + "ajv": "6.9.1", + "chokidar": "2.0.4", + "fast-json-stable-stringify": "2.0.0", + "rxjs": "6.3.3", + "source-map": "0.7.3" + }, + "dependencies": { + "rxjs": { + "version": "6.3.3", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.3.3.tgz", + "integrity": "sha512-JTWmoY9tWCs7zvIk/CvRjhjGaOd+OVBM987mxFo+OW66cGpdKjZcpmc74ES1sB//7Kl/PAe8+wEakuhG4pcgOw==", + "requires": { + "tslib": "^1.9.0" + } + } + } + }, + "@angular-devkit/schematics": { + "version": "7.3.10", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-7.3.10.tgz", + "integrity": "sha512-LMTGQ8kJb80LjTttu0ZqWXddzYtDwjKtMKY9X0A60Iz8/wbGl0j+wYG7KAVoRF0JeieYXs8Dl9KWdjyJyvJ/RA==", + "requires": { + "@angular-devkit/core": "7.3.10", + "rxjs": "6.3.3" + }, + "dependencies": { + "rxjs": { + "version": "6.3.3", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.3.3.tgz", + "integrity": "sha512-JTWmoY9tWCs7zvIk/CvRjhjGaOd+OVBM987mxFo+OW66cGpdKjZcpmc74ES1sB//7Kl/PAe8+wEakuhG4pcgOw==", + "requires": { + "tslib": "^1.9.0" + } + } + } + }, + "ajv": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.9.1.tgz", + "integrity": "sha512-XDN92U311aINL77ieWHmqCcNlwjoP5cHXDxIxbf2MaPYuCXOHS7gHH8jktxeK5omgd52XbSTX6a4Piwd1pQmzA==", + "requires": { + "fast-deep-equal": "^2.0.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "anymatch": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", + "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", + "requires": { + "micromatch": "^3.1.4", + "normalize-path": "^2.1.1" + } + }, + "binary-extensions": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz", + "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==" + }, + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "chokidar": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.0.4.tgz", + "integrity": "sha512-z9n7yt9rOvIJrMhvDtDictKrkFHeihkNl6uWMmZlmL6tJtX9Cs+87oK+teBx+JIgzvbX3yZHT3eF8vpbDxHJXQ==", + "requires": { + "anymatch": "^2.0.0", + "async-each": "^1.0.0", + "braces": "^2.3.0", + "fsevents": "^1.2.2", + "glob-parent": "^3.1.0", + "inherits": "^2.0.1", + "is-binary-path": "^1.0.0", + "is-glob": "^4.0.0", + "lodash.debounce": "^4.0.8", + "normalize-path": "^2.1.1", + "path-is-absolute": "^1.0.0", + "readdirp": "^2.0.0", + "upath": "^1.0.5" + } + }, + "fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=" + }, + "fast-json-stable-stringify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", + "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "fsevents": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", + "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", + "optional": true, + "requires": { + "bindings": "^1.5.0", + "nan": "^2.12.1" + } + }, + "glob-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", + "requires": { + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" + }, + "dependencies": { + "is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "requires": { + "is-extglob": "^2.1.0" + } + } + } + }, + "is-binary-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", + "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", + "requires": { + "binary-extensions": "^1.0.0" + } + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + }, + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "requires": { + "remove-trailing-separator": "^1.0.1" + } + }, + "parse5": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz", + "integrity": "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==" + }, + "readdirp": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", + "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", + "requires": { + "graceful-fs": "^4.1.11", + "micromatch": "^3.1.10", + "readable-stream": "^2.0.2" + } + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "requires": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + } + }, + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, + "typescript": { + "version": "3.9.9", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.9.tgz", + "integrity": "sha512-kdMjTiekY+z/ubJCATUPlRDl39vXYiMV9iyeMuEuXZh2we6zz80uovNN2WlAxmmdE/Z/YQe+EbOEXB5RHEED3w==" + } + } + }, "screenfull": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/screenfull/-/screenfull-4.2.1.tgz", @@ -31980,9 +32738,9 @@ } }, "semver": { - "version": "5.5.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.1.tgz", - "integrity": "sha512-PqpAxfrEhlSUWge8dwIp4tZnQ25DIOthpiaHNIthsjEFQD6EvqUKUDM7L8O2rShkFccYo1VjJR0coWfNkCubRw==" + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" }, "semver-dsl": { "version": "1.0.1", @@ -32199,6 +32957,11 @@ } } }, + "slide": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/slide/-/slide-1.1.6.tgz", + "integrity": "sha1-VusCfWW00tzmyy4tMsTUr8nh1wc=" + }, "smart-buffer": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.1.0.tgz", @@ -32631,7 +33394,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==", - "dev": true, "requires": { "spdx-expression-parse": "^3.0.0", "spdx-license-ids": "^3.0.0" @@ -32640,14 +33402,12 @@ "spdx-exceptions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", - "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", - "dev": true + "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==" }, "spdx-expression-parse": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", - "dev": true, "requires": { "spdx-exceptions": "^2.1.0", "spdx-license-ids": "^3.0.0" @@ -32656,8 +33416,7 @@ "spdx-license-ids": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.7.tgz", - "integrity": "sha512-U+MTEOO0AiDzxwFvoa4JVnMV6mZlJKk2sBLt90s7G0Gd0Mlknc7kxEn3nuDPNZRta7O2uy8oLcZLVT+4sqNZHQ==", - "dev": true + "integrity": "sha512-U+MTEOO0AiDzxwFvoa4JVnMV6mZlJKk2sBLt90s7G0Gd0Mlknc7kxEn3nuDPNZRta7O2uy8oLcZLVT+4sqNZHQ==" }, "spdy": { "version": "4.0.2", @@ -34058,7 +34817,6 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", - "dev": true, "requires": { "spdx-correct": "^3.0.0", "spdx-expression-parse": "^3.0.0" diff --git a/frontend/package.json b/frontend/package.json index 46eb280e347..b2073b6006f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -42,6 +42,7 @@ "@angular/platform-browser": "~11.2.3", "@angular/platform-browser-dynamic": "~11.2.3", "@angular/router": "~11.2.3", + "@datorama/akita": "^6.1.3", "@fullcalendar/angular": "5.5.0", "@fullcalendar/core": "5.5.0", "@fullcalendar/daygrid": "5.5.0", diff --git a/frontend/src/app/components/enterprise/enterprise-modal/enterprise-trial-form/ee-trial-form.component.ts b/frontend/src/app/components/enterprise/enterprise-modal/enterprise-trial-form/ee-trial-form.component.ts index 4f0e969e16a..0b1e32fb861 100644 --- a/frontend/src/app/components/enterprise/enterprise-modal/enterprise-trial-form/ee-trial-form.component.ts +++ b/frontend/src/app/components/enterprise/enterprise-modal/enterprise-trial-form/ee-trial-form.component.ts @@ -30,7 +30,7 @@ import { Component, ElementRef } from "@angular/core"; import { FormBuilder, Validators } from "@angular/forms"; import { I18nService } from "app/modules/common/i18n/i18n.service"; import { EnterpriseTrialData, EnterpriseTrialService } from "core-components/enterprise/enterprise-trial.service"; -import { CurrentUserService } from "core-components/user/current-user.service"; +import { CurrentUserService } from "core-app/modules/current-user/current-user.service"; import { I18nHelpers } from "core-app/helpers/i18n/localized-link"; const newsletterURL = 'https://www.openproject.com/newsletter/'; diff --git a/frontend/src/app/components/filters/filter-toggled-multiselect-value/filter-toggled-multiselect-value.component.ts b/frontend/src/app/components/filters/filter-toggled-multiselect-value/filter-toggled-multiselect-value.component.ts index 3abe8254988..3df580c53c5 100644 --- a/frontend/src/app/components/filters/filter-toggled-multiselect-value/filter-toggled-multiselect-value.component.ts +++ b/frontend/src/app/components/filters/filter-toggled-multiselect-value/filter-toggled-multiselect-value.component.ts @@ -49,7 +49,7 @@ import { HalResourceSortingService } from "core-app/modules/hal/services/hal-res import { PathHelperService } from "core-app/modules/common/path-helper/path-helper.service"; import { NgSelectComponent } from "@ng-select/ng-select"; import { APIV3Service } from "core-app/modules/apiv3/api-v3.service"; -import { CurrentUserService } from "core-components/user/current-user.service"; +import { CurrentUserService } from "core-app/modules/current-user/current-user.service"; @Component({ selector: 'filter-toggled-multiselect-value', diff --git a/frontend/src/app/components/states.service.ts b/frontend/src/app/components/states.service.ts index 9d2ca693ff5..6d8bb30aa78 100644 --- a/frontend/src/app/components/states.service.ts +++ b/frontend/src/app/components/states.service.ts @@ -17,6 +17,7 @@ import { QueryGroupByResource } from "core-app/modules/hal/resources/query-group import { VersionResource } from "core-app/modules/hal/resources/version-resource"; import { WorkPackageDisplayRepresentationValue } from "core-app/modules/work_packages/routing/wp-view-base/view-services/wp-view-display-representation.service"; import { TimeEntryResource } from "core-app/modules/hal/resources/time-entry-resource"; +import { CapabilityResource } from "core-app/modules/hal/resources/capability-resource"; export class States extends StatesGroup { name = 'MainStore'; @@ -40,7 +41,10 @@ export class States extends StatesGroup { statuses = multiInput(); /* /api/v3/time_entries */ - timeEntries:MultiInputState = multiInput(); + timeEntries = multiInput(); + + /* /api/v3/capabilities */ + capabilities = multiInput(); /* /api/v3/versions */ versions = multiInput(); diff --git a/frontend/src/app/components/wp-edit-form/work-package-filter-values.spec.ts b/frontend/src/app/components/wp-edit-form/work-package-filter-values.spec.ts index ecd07bf5482..db554a8e9d7 100644 --- a/frontend/src/app/components/wp-edit-form/work-package-filter-values.spec.ts +++ b/frontend/src/app/components/wp-edit-form/work-package-filter-values.spec.ts @@ -27,7 +27,7 @@ //++ import { TestBed } from "@angular/core/testing"; -import { CurrentUserService } from "core-components/user/current-user.service"; +import { CurrentUserService } from "core-app/modules/current-user/current-user.service"; import { HalResourceService } from "core-app/modules/hal/services/hal-resource.service"; import { Injector } from "@angular/core"; import { SchemaCacheService } from "core-components/schemas/schema-cache.service"; diff --git a/frontend/src/app/components/wp-edit-form/work-package-filter-values.ts b/frontend/src/app/components/wp-edit-form/work-package-filter-values.ts index 0ec66f5cee7..5333c0c91b4 100644 --- a/frontend/src/app/components/wp-edit-form/work-package-filter-values.ts +++ b/frontend/src/app/components/wp-edit-form/work-package-filter-values.ts @@ -1,6 +1,6 @@ import { HalResource } from 'core-app/modules/hal/resources/hal-resource'; import { QueryFilterInstanceResource } from 'core-app/modules/hal/resources/query-filter-instance-resource'; -import { CurrentUserService } from "core-components/user/current-user.service"; +import { CurrentUserService } from "core-app/modules/current-user/current-user.service"; import { HalResourceService } from 'core-app/modules/hal/services/hal-resource.service'; import { Injector } from '@angular/core'; import { AngularTrackingHelpers } from "core-components/angular/tracking-functions"; diff --git a/frontend/src/app/components/wp-query-select/wp-static-queries.service.ts b/frontend/src/app/components/wp-query-select/wp-static-queries.service.ts index f89e300e834..26c86b5c5a4 100644 --- a/frontend/src/app/components/wp-query-select/wp-static-queries.service.ts +++ b/frontend/src/app/components/wp-query-select/wp-static-queries.service.ts @@ -32,7 +32,7 @@ import { Injectable } from '@angular/core'; import { PathHelperService } from "core-app/modules/common/path-helper/path-helper.service"; import { CurrentProjectService } from "core-components/projects/current-project.service"; import { StateService } from "@uirouter/core"; -import { CurrentUserService } from "core-components/user/current-user.service"; +import { CurrentUserService } from "core-app/modules/current-user/current-user.service"; @Injectable() export class WorkPackageStaticQueriesService { diff --git a/frontend/src/app/core/services/permissions/permissions.service.ts b/frontend/src/app/core/services/permissions/permissions.service.ts index 7571e704fe1..66b80790be9 100644 --- a/frontend/src/app/core/services/permissions/permissions.service.ts +++ b/frontend/src/app/core/services/permissions/permissions.service.ts @@ -1,8 +1,8 @@ import { Injectable } from '@angular/core'; import { APIV3Service } from 'core-app/modules/apiv3/api-v3.service'; -import { Observable } from 'rxjs'; +import { Observable, of } from 'rxjs'; import { CurrentProjectService } from 'core-components/projects/current-project.service'; -import { map } from 'rxjs/operators'; +import { map, catchError } from 'rxjs/operators'; import { FilterOperator } from 'core-components/api/api-v3/api-v3-filter-builder'; @Injectable({ @@ -22,6 +22,12 @@ export class PermissionsService { .memberships .available_projects .list({ filters }) - .pipe(map(collection => !!collection.elements.length)); + .pipe( + map(collection => !!collection.elements.length), + catchError((error) => { + console.error(error); + return of(false); + }), + ); } } diff --git a/frontend/src/app/modules/apiv3/api-v3.service.ts b/frontend/src/app/modules/apiv3/api-v3.service.ts index ab018029b8a..69e621ccc93 100644 --- a/frontend/src/app/modules/apiv3/api-v3.service.ts +++ b/frontend/src/app/modules/apiv3/api-v3.service.ts @@ -36,6 +36,7 @@ import { Constructor } from "@angular/cdk/table"; import { PathHelperService } from "core-app/modules/common/path-helper/path-helper.service"; import { Apiv3GridsPaths } from "core-app/modules/apiv3/endpoints/grids/apiv3-grids-paths"; import { Apiv3TimeEntriesPaths } from "core-app/modules/apiv3/endpoints/time-entries/apiv3-time-entries-paths"; +import { Apiv3CapabilitiesPaths } from "core-app/modules/apiv3/endpoints/capabilities/apiv3-capabilities-paths"; import { Apiv3MembershipsPaths } from "core-app/modules/apiv3/endpoints/memberships/apiv3-memberships-paths"; import { Apiv3UsersPaths } from "core-app/modules/apiv3/endpoints/users/apiv3-users-paths"; import { Apiv3PlaceholderUsersPaths } from 'core-app/modules/apiv3/endpoints/placeholder-users/apiv3-placeholder-users-paths.ts'; @@ -90,6 +91,12 @@ export class APIV3Service { // /api/v3/time_entries public readonly time_entries = this.apiV3CustomEndpoint(Apiv3TimeEntriesPaths); + // /api/v3/actions + public readonly actions = this.apiV3CollectionEndpoint('actions'); + + // /api/v3/capabilities + public readonly capabilities = this.apiV3CustomEndpoint(Apiv3CapabilitiesPaths); + // /api/v3/memberships public readonly memberships = this.apiV3CustomEndpoint(Apiv3MembershipsPaths); diff --git a/frontend/src/app/modules/apiv3/endpoints/capabilities/apiv3-capabilities-paths.ts b/frontend/src/app/modules/apiv3/endpoints/capabilities/apiv3-capabilities-paths.ts new file mode 100644 index 00000000000..4c7a9e11c25 --- /dev/null +++ b/frontend/src/app/modules/apiv3/endpoints/capabilities/apiv3-capabilities-paths.ts @@ -0,0 +1,69 @@ +//-- copyright +// OpenProject is an open source project management software. +// Copyright (C) 2012-2021 the OpenProject GmbH +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License version 3. +// +// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +// Copyright (C) 2006-2013 Jean-Philippe Lang +// Copyright (C) 2010-2013 the ChiliProject Team +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +// +// See docs/COPYRIGHT.rdoc for more details. +//++ + + +import { Apiv3CapabilityPaths } from "core-app/modules/apiv3/endpoints/capabilities/apiv3-capability-paths"; +import { CapabilityResource } from "core-app/modules/hal/resources/capability-resource"; +import { APIV3Service } from "core-app/modules/apiv3/api-v3.service"; +import { Observable } from "rxjs"; +import { CollectionResource } from "core-app/modules/hal/resources/collection-resource"; +import { CachableAPIV3Collection } from "core-app/modules/apiv3/cache/cachable-apiv3-collection"; +import { MultiInputState } from "reactivestates"; +import { + Apiv3ListParameters, + Apiv3ListResourceInterface, + listParamsString +} from "core-app/modules/apiv3/paths/apiv3-list-resource.interface"; +import { CapabilityCacheService } from "core-app/modules/apiv3/endpoints/capabilities/capability-cache.service"; +import { StateCacheService } from "core-app/modules/apiv3/cache/state-cache.service"; + +export class Apiv3CapabilitiesPaths + extends CachableAPIV3Collection + implements Apiv3ListResourceInterface { + constructor(protected apiRoot:APIV3Service, + protected basePath:string) { + super(apiRoot, basePath, 'capabilities', Apiv3CapabilityPaths); + } + + /** + * Load a list of capabilities with a given list parameter filter + * @param params + */ + public list(params?:Apiv3ListParameters):Observable> { + return this + .halResourceService + .get>(this.path + listParamsString(params)) + .pipe( + this.cacheResponse() + ); + } + + protected createCache():StateCacheService { + return new CapabilityCacheService(this.injector, this.states.capabilities); + } +} diff --git a/frontend/src/app/components/user/current-user.service.ts b/frontend/src/app/modules/apiv3/endpoints/capabilities/apiv3-capability-paths.ts similarity index 67% rename from frontend/src/app/components/user/current-user.service.ts rename to frontend/src/app/modules/apiv3/endpoints/capabilities/apiv3-capability-paths.ts index d3505b7b6b0..2af69e860d0 100644 --- a/frontend/src/app/components/user/current-user.service.ts +++ b/frontend/src/app/modules/apiv3/endpoints/capabilities/apiv3-capability-paths.ts @@ -26,35 +26,13 @@ // See docs/COPYRIGHT.rdoc for more details. //++ -import { Injectable } from "@angular/core"; +import { CapabilityResource } from "core-app/modules/hal/resources/capability-resource"; +import { CachableAPIV3Resource } from "core-app/modules/apiv3/cache/cachable-apiv3-resource"; +import { StateCacheService } from "core-app/modules/apiv3/cache/state-cache.service"; +import { Apiv3CapabilitiesPaths } from "core-app/modules/apiv3/endpoints/capabilities/apiv3-capabilities-paths"; -@Injectable({ providedIn: 'root' }) -export class CurrentUserService { - public get isLoggedIn() { - return this.userMeta.length > 0; - } - - public get userId() { - return this.userMeta.data('id'); - } - - public get href() { - return `/api/v3/users/${this.userId}`; - } - - public get name() { - return this.userMeta.data('name'); - } - - public get mail() { - return this.userMeta.data('mail'); - } - - public get language() { - return I18n.locale || 'en'; - } - - private get userMeta():JQuery { - return jQuery('meta[name=current_user]'); +export class Apiv3CapabilityPaths extends CachableAPIV3Resource { + protected createCache():StateCacheService { + return (this.parent as Apiv3CapabilitiesPaths).cache; } } diff --git a/frontend/src/app/modules/apiv3/endpoints/capabilities/capability-cache.service.ts b/frontend/src/app/modules/apiv3/endpoints/capabilities/capability-cache.service.ts new file mode 100644 index 00000000000..b9c6aa88bc6 --- /dev/null +++ b/frontend/src/app/modules/apiv3/endpoints/capabilities/capability-cache.service.ts @@ -0,0 +1,48 @@ +//-- copyright +// OpenProject is an open source project management software. +// Copyright (C) 2012-2021 the OpenProject GmbH +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License version 3. +// +// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +// Copyright (C) 2006-2013 Jean-Philippe Lang +// Copyright (C) 2010-2013 the ChiliProject Team +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +// +// See docs/COPYRIGHT.rdoc for more details. +//++ + +import { CapabilityResource } from "core-app/modules/hal/resources/capability-resource"; +import { InjectField } from "core-app/helpers/angular/inject-field.decorator"; +import { SchemaCacheService } from "core-components/schemas/schema-cache.service"; +import { States } from "core-components/states.service"; +import { Injector } from "@angular/core"; +import { StateCacheService } from "core-app/modules/apiv3/cache/state-cache.service"; +import { MultiInputState } from "reactivestates"; + +export class CapabilityCacheService extends StateCacheService { + @InjectField() readonly states:States; + + constructor(readonly injector:Injector, state:MultiInputState) { + super(state); + } + + updateValue(id:string, val:CapabilityResource):Promise { + this.putValue(id, val); + return Promise.resolve(val); + } +} diff --git a/frontend/src/app/modules/common/openproject-common.module.ts b/frontend/src/app/modules/common/openproject-common.module.ts index f069d9e3fa5..5eae4172f28 100644 --- a/frontend/src/app/modules/common/openproject-common.module.ts +++ b/frontend/src/app/modules/common/openproject-common.module.ts @@ -40,10 +40,10 @@ import {StateService, UIRouterModule} from '@uirouter/angular'; import {HookService} from '../plugins/hook-service'; import {OpenprojectAccessibilityModule} from 'core-app/modules/a11y/openproject-a11y.module'; +import {CurrentUserModule} from 'core-app/modules/current-user/current-user.module'; import {IconTriggeredContextMenuComponent} from 'core-components/op-context-menu/icon-triggered-context-menu/icon-triggered-context-menu.component'; import {CurrentProjectService} from 'core-components/projects/current-project.service'; -import {CurrentUserService} from 'core-components/user/current-user.service'; import {TablePaginationComponent} from 'core-components/table-pagination/table-pagination.component'; import {SortHeaderDirective} from 'core-components/wp-table/sort-header/sort-header.directive'; import {ZenModeButtonComponent} from 'core-components/wp-buttons/zen-mode-toggle-button/zen-mode-toggle-button.component'; @@ -87,14 +87,9 @@ import {OpenprojectPrincipalRenderingModule} from "core-app/modules/principal/pr export function bootstrapModule(injector:Injector) { // Ensure error reporter is run const currentProject = injector.get(CurrentProjectService); - const currentUser = injector.get(CurrentUserService); const routerState = injector.get(StateService); window.ErrorReporter.addContext((scope) => { - if (currentUser.isLoggedIn) { - scope.setUser({ name: currentUser.name, id: currentUser.userId, email: currentUser.mail }); - } - if (currentProject.inProjectContext) { scope.setTag('project', currentProject.identifier!); } @@ -124,6 +119,7 @@ export function bootstrapModule(injector:Injector) { DragulaModule, // Our own A11y module OpenprojectAccessibilityModule, + CurrentUserModule, NgSelectModule, NgOptionHighlightModule, diff --git a/frontend/src/app/modules/current-user/current-user.module.ts b/frontend/src/app/modules/current-user/current-user.module.ts new file mode 100644 index 00000000000..5c66df0d292 --- /dev/null +++ b/frontend/src/app/modules/current-user/current-user.module.ts @@ -0,0 +1,41 @@ +import { Injector, NgModule } from "@angular/core"; +import { CollectionResource } from "core-app/modules/hal/resources/collection-resource"; +import { CapabilityResource } from "core-app/modules/hal/resources/capability-resource"; + +import { CurrentUserService } from "./current-user.service"; +import { CurrentUserStore } from "./current-user.store"; +import { CurrentUserQuery } from "./current-user.query"; + +export function bootstrapModule(injector:Injector) { + const currentUserService = injector.get(CurrentUserService); + + window.ErrorReporter.addContext((scope) => { + currentUserService.user$.subscribe(({ id, name, mail }) => { + scope.setUser({ + name, + mail, + id: id || undefined, // scope expects undefined instead of null + }); + }); + }); + + const userMeta = document.querySelectorAll('meta[name=current_user]')[0] as HTMLElement|undefined; + currentUserService.setUser({ + id: userMeta?.dataset.id || null, + name: userMeta?.dataset.name || null, + mail: userMeta?.dataset.mail || null, + }); +} + +@NgModule({ + providers: [ + CurrentUserService, + CurrentUserStore, + CurrentUserQuery, + ], +}) +export class CurrentUserModule { + constructor(injector:Injector) { + bootstrapModule(injector); + } +} diff --git a/frontend/src/app/modules/current-user/current-user.query.ts b/frontend/src/app/modules/current-user/current-user.query.ts new file mode 100644 index 00000000000..f8f0931f02b --- /dev/null +++ b/frontend/src/app/modules/current-user/current-user.query.ts @@ -0,0 +1,21 @@ +import { Injectable } from '@angular/core'; +import { Query } from '@datorama/akita'; +import { Observable, combineLatest } from 'rxjs'; +import { map } from 'rxjs/operators'; +import { + CurrentUserStore, + CurrentUserState, + CurrentUser, +} from './current-user.store'; +import { CapabilityResource } from "core-app/modules/hal/resources/capability-resource"; + +@Injectable() +export class CurrentUserQuery extends Query { + constructor(protected store: CurrentUserStore) { + super(store); + } + + isLoggedIn$ = this.select(state => !!state.id); + user$ = this.select(({ id, name, mail }) => ({ id, name, mail })); + capabilities$ = this.select('capabilities'); +} diff --git a/frontend/src/app/modules/current-user/current-user.service.ts b/frontend/src/app/modules/current-user/current-user.service.ts new file mode 100644 index 00000000000..e569bd8f049 --- /dev/null +++ b/frontend/src/app/modules/current-user/current-user.service.ts @@ -0,0 +1,129 @@ +//-- copyright +// OpenProject is an open source project management software. +// Copyright (C) 2012-2021 the OpenProject GmbH +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License version 3. +// +// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +// Copyright (C) 2006-2013 Jean-Philippe Lang +// Copyright (C) 2010-2013 the ChiliProject Team +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +// +// See docs/COPYRIGHT.rdoc for more details. +//++ + +import { Injectable } from "@angular/core"; +import { APIV3Service } from "core-app/modules/apiv3/api-v3.service"; +import { take } from 'rxjs/operators'; +import { CurrentUserStore, CurrentUser } from "./current-user.store"; +import { CapabilityResource } from "core-app/modules/hal/resources/capability-resource"; +import { CurrentUserQuery } from "./current-user.query"; + + +@Injectable({ providedIn: 'root' }) +export class CurrentUserService { + constructor( + private apiV3Service: APIV3Service, + private currentUserStore: CurrentUserStore, + private currentUserQuery: CurrentUserQuery, + ) { + this.setupLegacyDataListeners(); + } + + public capabilities$ = this.currentUserQuery.capabilities$; + public isLoggedIn$ = this.currentUserQuery.isLoggedIn$; + public user$ = this.currentUserQuery.user$; + + public setUser(user: CurrentUser) { + this.currentUserStore.update(state => ({ + ...state, + ...user, + })); + + this.getCapabilities(); + } + + public getCapabilities() { + this.user$.pipe(take(1)).subscribe((user) => { + if (!user.id) { + this.currentUserStore.update(state => ({ + ...state, + capabilities: [], + })); + + return; + } + + this.apiV3Service.capabilities.list({ + filters: [ ['principal', '=', [user.id]], ], + pageSize: 1000, + }).subscribe((data) => { + this.currentUserStore.update(state => ({ + ...state, + capabilities: data.elements, + })); + }); + }); + + return this.currentUserQuery.capabilities$; + } + + // Everything below this is deprecated legacy interfacing and should not be used + + + private setupLegacyDataListeners() { + this.currentUserQuery.user$.subscribe(user => this._user = user); + this.currentUserQuery.isLoggedIn$.subscribe(isLoggedIn => this._isLoggedIn = isLoggedIn); + } + + private _isLoggedIn = false; + /** @deprecated Use the store mechanism `currentUserQuery.isLoggedIn$` */ + public get isLoggedIn() { + return this._isLoggedIn; + } + + private _user: CurrentUser = { + id: null, + name: null, + mail: null, + }; + + /** @deprecated Use the store mechanism `currentUserQuery.user$` */ + public get userId() { + return this._user.id || ''; + } + + /** @deprecated Use the store mechanism `currentUserQuery.user$` */ + public get name() { + return this._user.name || ''; + } + + /** @deprecated Use the store mechanism `currentUserQuery.user$` */ + public get mail() { + return this._user.mail || ''; + } + + /** @deprecated Use the store mechanism `currentUserQuery.user$` */ + public get href() { + return `/api/v3/users/${this.userId}`; + } + + /** @deprecated Use `I18nService.locale` instead */ + public get language() { + return I18n.locale || 'en'; + } +} diff --git a/frontend/src/app/modules/current-user/current-user.store.ts b/frontend/src/app/modules/current-user/current-user.store.ts new file mode 100644 index 00000000000..39f11807b51 --- /dev/null +++ b/frontend/src/app/modules/current-user/current-user.store.ts @@ -0,0 +1,58 @@ +//-- copyright +// OpenProject is an open source project management software. +// Copyright (C) 2012-2021 the OpenProject GmbH +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License version 3. +// +// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +// Copyright (C) 2006-2013 Jean-Philippe Lang +// Copyright (C) 2010-2013 the ChiliProject Team +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +// +// See docs/COPYRIGHT.rdoc for more details. +//++ + +import { Injectable } from "@angular/core"; +import { Store, StoreConfig } from '@datorama/akita'; +import { CapabilityResource } from "core-app/modules/hal/resources/capability-resource"; + +export interface CurrentUser { + id: string|null; + name: string|null; + mail: string|null; +} + +export interface CurrentUserState extends CurrentUser { + capabilities: CapabilityResource[]; +} + +export function createInitialState(): CurrentUserState { + return { + id: null, + name: null, + mail: null, + capabilities: [], + }; +} + +@Injectable() +@StoreConfig({ name: 'current-user' }) +export class CurrentUserStore extends Store { + constructor() { + super(createInitialState()); + } +} diff --git a/frontend/src/app/modules/hal/resources/capability-resource.ts b/frontend/src/app/modules/hal/resources/capability-resource.ts new file mode 100644 index 00000000000..9f77d23a51d --- /dev/null +++ b/frontend/src/app/modules/hal/resources/capability-resource.ts @@ -0,0 +1,34 @@ +//-- copyright +// OpenProject is an open source project management software. +// Copyright (C) 2012-2021 the OpenProject GmbH +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License version 3. +// +// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +// Copyright (C) 2006-2013 Jean-Philippe Lang +// Copyright (C) 2010-2013 the ChiliProject Team +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU General Public License +// as published by the Free Software Foundation; either version 2 +// of the License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +// +// See docs/COPYRIGHT.rdoc for more details. +//++ + +import { HalResource } from 'core-app/modules/hal/resources/hal-resource'; +import { SchemaResource } from "core-app/modules/hal/resources/schema-resource"; +import { SchemaCacheService } from "core-components/schemas/schema-cache.service"; +import { InjectField } from "core-app/helpers/angular/inject-field.decorator"; + +export class CapabilityResource extends HalResource {} diff --git a/frontend/src/app/modules/invite-user-modal/invite-user.component.ts b/frontend/src/app/modules/invite-user-modal/invite-user.component.ts index b4a802920e8..260adc50fc0 100644 --- a/frontend/src/app/modules/invite-user-modal/invite-user.component.ts +++ b/frontend/src/app/modules/invite-user-modal/invite-user.component.ts @@ -7,13 +7,14 @@ import { OnInit, ViewEncapsulation, } from '@angular/core'; -import {OpModalLocalsMap} from 'core-app/modules/modal/modal.types'; -import {OpModalComponent} from 'core-app/modules/modal/modal.component'; -import {OpModalLocalsToken} from "core-app/modules/modal/modal.service"; -import {APIV3Service} from "core-app/modules/apiv3/api-v3.service"; -import {RoleResource} from "core-app/modules/hal/resources/role-resource"; -import {HalResource} from "core-app/modules/hal/resources/hal-resource"; -import {ProjectResource} from "core-app/modules/hal/resources/project-resource"; +import { OpModalLocalsMap } from 'core-app/modules/modal/modal.types'; +import { OpModalComponent } from 'core-app/modules/modal/modal.component'; +import { OpModalLocalsToken } from "core-app/modules/modal/modal.service"; +import { APIV3Service } from "core-app/modules/apiv3/api-v3.service"; +import { ApiV3FilterBuilder } from "core-components/api/api-v3/api-v3-filter-builder"; +import { RoleResource } from "core-app/modules/hal/resources/role-resource"; +import { HalResource } from "core-app/modules/hal/resources/hal-resource"; +import { ProjectResource } from "core-app/modules/hal/resources/project-resource"; enum Steps { ProjectSelection, diff --git a/frontend/src/app/modules/invite-user-modal/principal/principal-search.component.html b/frontend/src/app/modules/invite-user-modal/principal/principal-search.component.html index f7d05c3fded..4ff4ab9da36 100644 --- a/frontend/src/app/modules/invite-user-modal/principal/principal-search.component.html +++ b/frontend/src/app/modules/invite-user-modal/principal/principal-search.component.html @@ -7,13 +7,13 @@ [clearable]="true" [clearOnBackspace]="false" [clearSearchOnAdd]="false" - bindLabel="name" - bindValue="value" + [compareWith]="compareWith" + bindValue="principal" autofocus #ngselect > - {{item.value.name }} + {{ item.principal.name || item.name }} @@ -22,7 +22,7 @@ class="ng-option-label" > -
{{ item.value.name }}
+
{{ item.principal.name }}
(''); public input = ''; - public items$:Observable; - public canInviteByEmail$:Observable; - public canCreateNewPlaceholder$:Observable; + public items$: Observable = this.input$ + .pipe( + this.untilDestroyed(), + debounceTime(200), + distinctUntilChanged(), + switchMap(this.loadPrincipalData.bind(this)), + ); + + public canInviteByEmail$ = combineLatest( + this.items$, + this.input$, + this.currentUserService.capabilities$.pipe( + map(capabilities => !!capabilities.find(c => c.action.href.endsWith('/users/create'))), + distinctUntilChanged(), + ), + ).pipe( + map(([elements, input, canCreateUsers]) => + canCreateUsers + && this.type === PrincipalType.User + && input?.includes('@') + && !elements.find((el:any) => el.email === input) + ), + ); + + public canCreateNewPlaceholder$ = combineLatest( + this.items$, + this.input$, + this.currentUserService.capabilities$.pipe( + map(capabilities => !!capabilities.find(c => c.action.href.endsWith('/placeholderUsers/create'))), + distinctUntilChanged(), + ), + ).pipe( + map(([elements, input, hasCapability]) => { + if (!hasCapability) { + return false; + } + + if (this.type !== PrincipalType.Placeholder) { + return false; + } + + return !!input && !elements.find((el:any) => el.name === input); + }), + ); + public showAddTag = false; public text = { @@ -52,6 +101,7 @@ export class PrincipalSearchComponent extends UntilDestroyedMixin implements OnI public I18n:I18nService, readonly elementRef:ElementRef, readonly apiV3Service:APIV3Service, + readonly currentUserService:CurrentUserService, ) { super(); @@ -59,34 +109,6 @@ export class PrincipalSearchComponent extends UntilDestroyedMixin implements OnI this.input = input; }); - this.items$ = this.input$ - .pipe( - this.untilDestroyed(), - debounceTime(200), - distinctUntilChanged(), - switchMap(this.loadPrincipalData.bind(this)), - ); - - this.canInviteByEmail$ = combineLatest( - this.items$, - this.input$, - ).pipe( - map(([elements, input]) => this.type === PrincipalType.User && input?.includes('@') && !elements.find((el:any) => el.email === input)), - ); - - this.canCreateNewPlaceholder$ = combineLatest( - this.items$, - this.input$, - ).pipe( - map(([elements, input]) => { - if (this.type !== PrincipalType.Placeholder) { - return false; - } - - return !!input && !elements.find((el:any) => el.name === input); - }), - ); - combineLatest( this.canInviteByEmail$, this.canCreateNewPlaceholder$, @@ -129,17 +151,20 @@ export class PrincipalSearchComponent extends UntilDestroyedMixin implements OnI }) .pipe( map(({ members, nonMembers }) => [ - ...nonMembers.elements.map((nonMember:any) => ({ - value: nonMember, + ...nonMembers.elements.map((nonMember:PrincipalLike) => ({ + principal: nonMember, disabled: false, })), - ...members.elements.map((member:any) => ({ - value: member, + ...members.elements.map((member:PrincipalLike) => ({ + principal: member, disabled: true, })), ]), shareReplay(1), ); + } + compareWith = (a: NgSelectPrincipalOption, b: PrincipalLike) => { + return a.principal.id === b.id; } } diff --git a/frontend/src/app/modules/invite-user-modal/project-selection/project-search.component.html b/frontend/src/app/modules/invite-user-modal/project-selection/project-search.component.html index 60b11d52939..c339a345f8c 100644 --- a/frontend/src/app/modules/invite-user-modal/project-selection/project-search.component.html +++ b/frontend/src/app/modules/invite-user-modal/project-selection/project-search.component.html @@ -6,13 +6,29 @@ [clearable]="true" [clearOnBackspace]="false" [clearSearchOnAdd]="false" - bindLabel="name" + [compareWith]="compareWith" + bindValue="project" autofocus #ngselect > - + + {{ item.project?.name || item.name }} + + -
{{ item.name }}
+
+ +
{{ item.project.name }}
+ + +
{{ text.noInviteRights }}
+
diff --git a/frontend/src/app/modules/invite-user-modal/project-selection/project-search.component.ts b/frontend/src/app/modules/invite-user-modal/project-selection/project-search.component.ts index d9bfea7f5f1..64236fc08c5 100644 --- a/frontend/src/app/modules/invite-user-modal/project-selection/project-search.component.ts +++ b/frontend/src/app/modules/invite-user-modal/project-selection/project-search.component.ts @@ -5,12 +5,19 @@ import { ElementRef, } from '@angular/core'; import { FormControl, NgControl } from "@angular/forms"; -import { Observable, Subject } from "rxjs"; +import { Observable, BehaviorSubject, combineLatest } from "rxjs"; import { debounceTime, distinctUntilChanged, filter, map, switchMap, tap } from "rxjs/operators"; import { APIV3Service } from "core-app/modules/apiv3/api-v3.service"; import { ApiV3FilterBuilder } from "core-components/api/api-v3/api-v3-filter-builder"; import { I18nService } from "core-app/modules/common/i18n/i18n.service"; import { UntilDestroyedMixin } from "core-app/helpers/angular/until-destroyed.mixin"; +import { ProjectResource } from "core-app/modules/hal/resources/project-resource"; +import { CurrentUserService } from 'core-app/modules/current-user/current-user.service'; + +interface NgSelectProjectOption { + project: ProjectResource, + disabled: boolean; +}; @Component({ selector: 'op-ium-project-search', @@ -21,34 +28,57 @@ export class ProjectSearchComponent extends UntilDestroyedMixin implements OnIni public text = { noResultsFound: this.I18n.t('js.invite_user_modal.project.no_results'), + noInviteRights: this.I18n.t('js.invite_user_modal.project.no_invite_rights'), }; - public input$ = new Subject(); - public items$:Observable; + public input$ = new BehaviorSubject(''); + public items$ = combineLatest([ + this.input$.pipe( + debounceTime(100), + switchMap((searchTerm:string) => { + const filters = new ApiV3FilterBuilder(); + if (searchTerm) { + filters.add('name_and_identifier', '~', [searchTerm]); + } + return this.apiV3Service.projects + .filtered(filters) + .get() + .pipe(map(collection => collection.elements)); + }) + ), + this.currentUserService.capabilities$.pipe( + map(capabilities => capabilities.filter(c => c.action.href.endsWith('/memberships/create'))) + ), + ]) + .pipe( + this.untilDestroyed(), + map(([ projects, projectInviteCapabilities ]) => { + const mapped = projects.map((project: ProjectResource) => ({ + project, + disabled: !projectInviteCapabilities.find(cap => cap.context.id === project.id), + })); + mapped.sort( + (a: NgSelectProjectOption, b: NgSelectProjectOption) => (a.disabled ? 1 : 0) - (b.disabled ? 1 : 0), + ); + return mapped; + }) + ); constructor( readonly I18n:I18nService, readonly elementRef:ElementRef, readonly apiV3Service:APIV3Service, + readonly currentUserService:CurrentUserService, ) { super(); - - this.items$ = this.input$ - .pipe( - this.untilDestroyed(), - debounceTime(100), - switchMap((searchTerm:string) => { - const filters = new ApiV3FilterBuilder(); - if (searchTerm) { - filters.add('name_and_identifier', '~', [searchTerm]); - } - return this.apiV3Service.projects.filtered(filters).get().pipe(map(collection => collection.elements)); - }), - ); } ngOnInit() { // Make sure we have initial data setTimeout(() => this.input$.next('')); } + + compareWith = (a: NgSelectProjectOption, b: ProjectResource) => { + return a.project.id === b.id; + } } diff --git a/frontend/src/app/modules/invite-user-modal/project-selection/project-selection.component.ts b/frontend/src/app/modules/invite-user-modal/project-selection/project-selection.component.ts index 8b3b0675438..c9230e76980 100644 --- a/frontend/src/app/modules/invite-user-modal/project-selection/project-selection.component.ts +++ b/frontend/src/app/modules/invite-user-modal/project-selection/project-selection.component.ts @@ -11,10 +11,13 @@ import { FormGroup, Validators, } from '@angular/forms'; -import {I18nService} from "core-app/modules/common/i18n/i18n.service"; -import {BannersService} from "core-app/modules/common/enterprise/banners.service"; -import {IOpOptionListOption} from "core-app/modules/common/option-list/option-list.component"; -import {PrincipalType} from '../invite-user.component'; +import { take } from "rxjs/operators"; +import { I18nService } from "core-app/modules/common/i18n/i18n.service"; +import { BannersService } from "core-app/modules/common/enterprise/banners.service"; +import { CurrentUserService } from 'core-app/modules/current-user/current-user.service'; +import { IOpOptionListOption } from "core-app/modules/common/option-list/option-list.component"; +import { ProjectResource } from "core-app/modules/hal/resources/project-resource"; +import { PrincipalType } from '../invite-user.component'; @Component({ selector: 'op-ium-project-selection', @@ -23,7 +26,7 @@ import {PrincipalType} from '../invite-user.component'; }) export class ProjectSelectionComponent implements OnInit { @Input() type:PrincipalType; - @Input() project:any = null; + @Input() project:ProjectResource|null; @Output() close = new EventEmitter(); @Output() save = new EventEmitter<{project:any, type:string}>(); @@ -64,27 +67,43 @@ export class ProjectSelectionComponent implements OnInit { readonly I18n:I18nService, readonly elementRef:ElementRef, readonly bannersService:BannersService, + readonly currentUserService:CurrentUserService, ) {} ngOnInit() { this.typeControl?.setValue(this.type); this.projectControl?.setValue(this.project); - this.typeOptions.push({ - value: PrincipalType.Placeholder, - title: this.bannersService.eeShowBanners - ? this.I18n.t('js.invite_user_modal.type.placeholder.title_no_ee') - : this.I18n.t('js.invite_user_modal.type.placeholder.title'), - description: this.bannersService.eeShowBanners - ? this.I18n.t('js.invite_user_modal.type.placeholder.description_no_ee', { - eeHref: this.bannersService.getEnterPriseEditionUrl({ - referrer: 'placeholder-users', - hash: 'placeholder-users', - }), - }) - : this.I18n.t('js.invite_user_modal.type.placeholder.description'), - disabled: this.bannersService.eeShowBanners, - }); + this.setPlaceholderOption(); + } + + private setPlaceholderOption() { + if (this.bannersService.eeShowBanners) { + this.typeOptions.push({ + value: PrincipalType.Placeholder, + title: this.I18n.t('js.invite_user_modal.type.placeholder.title_no_ee'), + description: this.I18n.t('js.invite_user_modal.type.placeholder.description_no_ee', { + eeHref: this.bannersService.getEnterPriseEditionUrl({ + referrer: 'placeholder-users', + hash: 'placeholder-users', + }), + }), + disabled: true, + }); + } else { + this.currentUserService.capabilities$.pipe(take(1)).subscribe((capabilities) => { + if (!capabilities.find(c => c.action.href.endsWith('/placeholderUsers/read'))) { + return; + } + // We only add the option if the user has placeholder read rights + this.typeOptions.push({ + value: PrincipalType.Placeholder, + title: this.I18n.t('js.invite_user_modal.type.placeholder.title'), + description: this.I18n.t('js.invite_user_modal.type.placeholder.description'), + disabled: false, + }); + }); + } } onSubmit($e:Event) { diff --git a/frontend/src/app/modules/invite-user-modal/summary/summary.component.ts b/frontend/src/app/modules/invite-user-modal/summary/summary.component.ts index 637d8700a86..99f9809dcba 100644 --- a/frontend/src/app/modules/invite-user-modal/summary/summary.component.ts +++ b/frontend/src/app/modules/invite-user-modal/summary/summary.component.ts @@ -80,6 +80,7 @@ export class SummaryComponent { } private createPrincipal(principal:PrincipalLike):Observable { + console.log(principal); if (principal instanceof HalResource) { return of(principal); } diff --git a/spec/features/users/invite_user_modal_spec.rb b/spec/features/users/invite_user_modal_spec.rb index 54e992dee6c..4ee9990b621 100644 --- a/spec/features/users/invite_user_modal_spec.rb +++ b/spec/features/users/invite_user_modal_spec.rb @@ -56,10 +56,8 @@ feature 'Invite user modal', type: :feature, js: true do it 'will invite that principal to the project' do modal.run_all_steps - assignee_field.expect_active! - # TODO assignee field should contain the user name now - #assignee_field.expect_value principal.name - assignee_field.expect_value nil + assignee_field.expect_inactive! + assignee_field.expect_display_value added_principal.name new_member = project.reload.member_principals.find_by(user_id: added_principal.id) expect(new_member).to be_present @@ -67,7 +65,7 @@ feature 'Invite user modal', type: :feature, js: true do end end - describe 'inviting a non-project existing user' do + describe 'inviting a principal to a project' do describe 'through the assignee field' do let(:wp_page) { Pages::FullWorkPackage.new(work_package, project) } let(:assignee_field) { wp_page.edit_field :assignee } @@ -80,19 +78,16 @@ feature 'Invite user modal', type: :feature, js: true do find('.ng-dropdown-footer button', text: 'Invite', wait: 10).click end - context 'with a non project user' do - let!(:principal) do - FactoryBot.create :user, - firstname: 'Nonproject', - lastname: 'User' - end + context 'with an existing user' do + let!(:principal) { FactoryBot.create :user, + firstname: 'Nonproject firstname', + lastname: 'nonproject lastname' + } it 'can add an existing user to the project' do modal.run_all_steps - # TODO assignee field should close and contain the user name now - #assignee_field.expect_value principal.name - assignee_field.expect_active! - assignee_field.expect_value nil + assignee_field.expect_inactive! + assignee_field.expect_display_value principal.name # But the user got created new_member = project.reload.members.find_by(user_id: principal.id) @@ -111,12 +106,42 @@ feature 'Invite user modal', type: :feature, js: true do let(:added_principal) { User.find_by!(mail: principal.mail) } end end + + context 'when the current user does not have permissions to invite a user to the instance by email' do + let(:permissions) { %i[view_work_packages edit_work_packages manage_members] } + it 'does not show the invite user option' do + modal.project_step + ngselect = modal.open_select_in_step principal.mail + expect(ngselect).to have_text "No users were found" + expect(ngselect).not_to have_text "Invite: #{principal.mail}" + end + end + + context 'when the current user does not have permissions to invite a user in this project' do + let(:permissions) { %i[view_work_packages edit_work_packages manage_members manage_user] } + + let(:project_no_permissions) { FactoryBot.create :project } + let(:role_no_permissions) { FactoryBot.create :role, + permissions: %i[view_work_packages edit_work_packages] + } + let!(:membership_no_permission) { + FactoryBot.create :member, + user: current_user, + project: project_no_permissions, + roles: [role_no_permissions] + } + + it 'disables projects for which you do not have rights' do + ngselect = modal.open_select_in_step + expect(ngselect).to have_text "#{project_no_permissions.name}\nYou are not allowed to invite members to this project" + end + end end describe 'inviting placeholders' do let(:principal) { FactoryBot.build :placeholder_user, name: 'MY NEW PLACEHOLDER' } - context 'system has enterprise', with_ee: %i[placeholder_users] do + context 'an enterprise system', with_ee: %i[placeholder_users] do describe 'create a new placeholder' do context 'with permissions to manage placeholders' do let(:permissions) { %i[view_work_packages edit_work_packages manage_members manage_placeholder_user] } @@ -127,15 +152,18 @@ feature 'Invite user modal', type: :feature, js: true do end context 'without permissions to manage placeholders' do - let(:permissions) { %i[view_work_packages edit_work_packages manage_members manage_placeholder_user] } + let(:permissions) { %i[view_work_packages edit_work_packages manage_members] } it 'does not allow to invite a new placeholder' do - skip "TODO wait for permissions API" + modal.within_modal do + expect(page).to have_selector '.op-option-list--item', count: 2 + end end end end context 'with an existing placeholder' do - let(:principal) { FactoryBot.create :placeholder_user } + let(:principal) { FactoryBot.create :placeholder_user, name: 'EXISTING PLACEHOLDER' } + let(:permissions) { %i[view_work_packages edit_work_packages manage_members manage_placeholder_user] } it_behaves_like 'invites the principal to the project' do let(:added_principal) { principal } @@ -143,7 +171,7 @@ feature 'Invite user modal', type: :feature, js: true do end end - context 'system has no enterprise' do + context 'non-enterprise system' do it 'shows the modal with placeholder option disabled' do modal.within_modal do expect(page).to have_field 'Placeholder user', disabled: true @@ -171,7 +199,7 @@ feature 'Invite user modal', type: :feature, js: true do wp_page.visit! end - it 'can add an existing user to the project' do + it 'cannot add an existing user to the project' do assignee_field.activate! expect(page).to have_no_selector('.ng-dropdown-footer', text: 'Invite') diff --git a/spec/support/browsers/chrome.rb b/spec/support/browsers/chrome.rb index 2d7ceb4f5cd..720bbc64a8c 100644 --- a/spec/support/browsers/chrome.rb +++ b/spec/support/browsers/chrome.rb @@ -9,24 +9,23 @@ def register_chrome(language, name: :"chrome_#{language}") if ActiveRecord::Type::Boolean.new.cast(ENV['OPENPROJECT_TESTING_NO_HEADLESS']) # Maximize the window however large the available space is - options.add_argument('start-maximized') - # options.add_argument('window-size=1920,1080') + options.add_argument('--start-maximized') # Open dev tools for quick access if ActiveRecord::Type::Boolean.new.cast(ENV['OPENPROJECT_TESTING_AUTO_DEVTOOLS']) - options.add_argument('auto-open-devtools-for-tabs') + options.add_argument('--auto-open-devtools-for-tabs') end else - options.add_argument('window-size=1920,1080') - options.add_argument('headless') + options.add_argument('--window-size=1920,1080') + options.add_argument('--headless') end - options.add_argument('no-sandbox') - options.add_argument('disable-gpu') - options.add_argument('disable-popup-blocking') - options.add_argument("lang=#{language}") + options.add_argument('--no-sandbox') + options.add_argument('--disable-gpu') + options.add_argument('--disable-popup-blocking') + options.add_argument("--lang=#{language}") # This is REQUIRED for running in a docker container # https://github.com/grosser/parallel_tests/issues/658 - options.add_argument('disable-dev-shm-usage') + options.add_argument('--disable-dev-shm-usage') options.add_preference(:download, directory_upgrade: true, diff --git a/spec/support/components/users/invite_user_modal.rb b/spec/support/components/users/invite_user_modal.rb index 3bdd5b06515..815212cc920 100644 --- a/spec/support/components/users/invite_user_modal.rb +++ b/spec/support/components/users/invite_user_modal.rb @@ -98,6 +98,12 @@ module Components click_next if next_step end + def open_select_in_step(query = '') + search_autocomplete modal_element.find('.ng-select-container'), + query: query, + results_selector: 'body' + end + def principal_step(next_step: true) if invite_user? autocomplete principal_name, select_text: "Invite: #{principal_name}"