From 835703a8ee9a08ee5badf125761c15028607a347 Mon Sep 17 00:00:00 2001 From: Marvin Perry Date: Thu, 6 Jul 2023 13:24:12 -0400 Subject: [PATCH 1/6] refactor(): wip on implementing a hub page entity --- package-lock.json | 2 +- packages/common/package-lock.json | 370 ++++++++++++++++++ packages/common/src/core/types/IHubPage.ts | 14 +- packages/common/src/pages/HubPage.ts | 170 ++++++++ packages/common/src/pages/HubPages.ts | 74 +++- .../src/pages/_internal/computeProps.ts | 46 +++ .../src/pages/_internal/getPropertyMap.ts | 24 ++ 7 files changed, 693 insertions(+), 7 deletions(-) create mode 100644 packages/common/package-lock.json create mode 100644 packages/common/src/pages/HubPage.ts create mode 100644 packages/common/src/pages/_internal/computeProps.ts create mode 100644 packages/common/src/pages/_internal/getPropertyMap.ts diff --git a/package-lock.json b/package-lock.json index e9ccfea8046..1e686eda7dd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -64966,7 +64966,7 @@ }, "packages/common": { "name": "@esri/hub-common", - "version": "13.17.3", + "version": "13.18.0", "license": "Apache-2.0", "dependencies": { "abab": "^2.0.5", diff --git a/packages/common/package-lock.json b/packages/common/package-lock.json new file mode 100644 index 00000000000..3055669cf41 --- /dev/null +++ b/packages/common/package-lock.json @@ -0,0 +1,370 @@ +{ + "name": "@esri/hub-common", + "version": "13.18.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "@esri/hub-common", + "version": "13.18.0", + "license": "Apache-2.0", + "dependencies": { + "abab": "^2.0.5", + "adlib": "^3.0.8", + "ajv": "^6.12.6", + "fast-xml-parser": "^3.21.0", + "json-schema-typed": "^7.0.3", + "jsonapi-typescript": "^0.1.3", + "tslib": "^1.13.0" + }, + "devDependencies": { + "@types/adlib": "^3.0.1", + "typescript": "^3.8.1" + }, + "peerDependencies": { + "@esri/arcgis-rest-auth": "^2.14.0 || 3", + "@esri/arcgis-rest-feature-layer": "^3.2.0", + "@esri/arcgis-rest-portal": "^2.18.0 || 3", + "@esri/arcgis-rest-request": "^2.14.0 || 3", + "@esri/arcgis-rest-types": "^2.15.0 || 3" + } + }, + "node_modules/@esri/arcgis-rest-auth": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/@esri/arcgis-rest-auth/-/arcgis-rest-auth-3.6.0.tgz", + "integrity": "sha512-rr7Lv9HAmwTI8UnDGQer5mt1S4LlgTKyD9loQ6WqXn/wdo2y29pUVTWafcOejftnidfZxkkKOhZ/eh7YCKGmZg==", + "peer": true, + "dependencies": { + "@esri/arcgis-rest-types": "^3.6.0", + "tslib": "^1.13.0" + }, + "peerDependencies": { + "@esri/arcgis-rest-request": "^3.0.0" + } + }, + "node_modules/@esri/arcgis-rest-feature-layer": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/@esri/arcgis-rest-feature-layer/-/arcgis-rest-feature-layer-3.6.0.tgz", + "integrity": "sha512-a5BywhJpqzpJIszrXZNDcKdRt9fqGXc/Y60TqS/mVVg0tJIu5Q4VpQQzK4cMkP3D3tFmj7VB2xLBWAzpH7aYVA==", + "peer": true, + "dependencies": { + "@esri/arcgis-rest-types": "^3.6.0", + "tslib": "^1.13.0" + }, + "peerDependencies": { + "@esri/arcgis-rest-auth": "^3.0.0", + "@esri/arcgis-rest-request": "^3.0.0" + } + }, + "node_modules/@esri/arcgis-rest-portal": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/@esri/arcgis-rest-portal/-/arcgis-rest-portal-3.6.0.tgz", + "integrity": "sha512-TPLcbQn+PfKqGlkCvlDYs2AQs7KdTKnM17AhGytnQkqYamNlhkZMAlgC4qq5/H7URTIqlQx52fx/OOfTfO0ABw==", + "peer": true, + "dependencies": { + "@esri/arcgis-rest-types": "^3.6.0", + "tslib": "^1.13.0" + }, + "peerDependencies": { + "@esri/arcgis-rest-auth": "^3.0.0", + "@esri/arcgis-rest-request": "^3.0.0" + } + }, + "node_modules/@esri/arcgis-rest-request": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/@esri/arcgis-rest-request/-/arcgis-rest-request-3.6.0.tgz", + "integrity": "sha512-JDFx1ZjheFUfW/YXWQ0DwxgIwPdQFBtIl5+Bvee4cyCmhxpO1/b82tv7nOLRMnMYBkC2aKRCgpAwkecaJZDpdw==", + "peer": true, + "dependencies": { + "tslib": "^1.10.0" + } + }, + "node_modules/@esri/arcgis-rest-types": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/@esri/arcgis-rest-types/-/arcgis-rest-types-3.6.0.tgz", + "integrity": "sha512-t6QWdVNmqB9OloYAvVWYNjvqlnrXs/m0nCRNwPGt3ZiAPXn5CpkpSn2UD6LPqGp6vPxKG81lbFQvwCw9az4qIg==", + "peer": true + }, + "node_modules/@types/adlib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/adlib/-/adlib-3.0.1.tgz", + "integrity": "sha512-7vRDaOFejVMdjzKagD45fK+mFs8pb0h9zmvsGrYsaQY9z+X3xUqf71uNMv8+OiVa8u/w1A7DS29LSUFzOnstRw==", + "dev": true + }, + "node_modules/abab": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", + "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==" + }, + "node_modules/adlib": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/adlib/-/adlib-3.0.8.tgz", + "integrity": "sha512-CbQ+mcm45pdDN0aAiGYP6FZWDor+BM4DbqJsrUptM79a2+FbiP8QdF0xHABbgCkioApKqg8lI33Gl6X8GFelfA==", + "dependencies": { + "esm": "^3.2.25" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "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/esm": { + "version": "3.2.25", + "resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz", + "integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + }, + "node_modules/fast-xml-parser": { + "version": "3.21.1", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-3.21.1.tgz", + "integrity": "sha512-FTFVjYoBOZTJekiUsawGsSYV9QL0A+zDYCRj7y34IO6Jg+2IMYEtQa+bbictpdpV8dHxXywqU7C0gRDEOFtBFg==", + "dependencies": { + "strnum": "^1.0.4" + }, + "bin": { + "xml2js": "cli.js" + }, + "funding": { + "type": "paypal", + "url": "https://paypal.me/naturalintelligence" + } + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, + "node_modules/json-schema-typed": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-7.0.3.tgz", + "integrity": "sha512-7DE8mpG+/fVw+dTpjbxnx47TaMnDfOI1jwft9g1VybltZCduyRQPJPvc+zzKY9WPHxhPWczyFuYa6I8Mw4iU5A==" + }, + "node_modules/json-typescript": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/json-typescript/-/json-typescript-1.1.2.tgz", + "integrity": "sha512-Np07MUsYMKbB0nNlw/MMIRjUK7ehO48LA4FsrzrhCfTUxMKbvOBAo0sc0b4nQ80ge9d32sModCunCgoyUojgUA==" + }, + "node_modules/jsonapi-typescript": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/jsonapi-typescript/-/jsonapi-typescript-0.1.3.tgz", + "integrity": "sha512-uPcPS01GeM+4HIyn18s7l1yD2S3uLZy2TX1UkQffCM0bLb3TMwudXUyVXPHTMZ4vdZT8MqKqN2vjB5PogTAdFQ==", + "dependencies": { + "json-typescript": "^1.0.0" + } + }, + "node_modules/punycode": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", + "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/strnum": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz", + "integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==" + }, + "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/typescript": { + "version": "3.9.10", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.10.tgz", + "integrity": "sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dependencies": { + "punycode": "^2.1.0" + } + } + }, + "dependencies": { + "@esri/arcgis-rest-auth": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/@esri/arcgis-rest-auth/-/arcgis-rest-auth-3.6.0.tgz", + "integrity": "sha512-rr7Lv9HAmwTI8UnDGQer5mt1S4LlgTKyD9loQ6WqXn/wdo2y29pUVTWafcOejftnidfZxkkKOhZ/eh7YCKGmZg==", + "peer": true, + "requires": { + "@esri/arcgis-rest-types": "^3.6.0", + "tslib": "^1.13.0" + } + }, + "@esri/arcgis-rest-feature-layer": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/@esri/arcgis-rest-feature-layer/-/arcgis-rest-feature-layer-3.6.0.tgz", + "integrity": "sha512-a5BywhJpqzpJIszrXZNDcKdRt9fqGXc/Y60TqS/mVVg0tJIu5Q4VpQQzK4cMkP3D3tFmj7VB2xLBWAzpH7aYVA==", + "peer": true, + "requires": { + "@esri/arcgis-rest-types": "^3.6.0", + "tslib": "^1.13.0" + } + }, + "@esri/arcgis-rest-portal": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/@esri/arcgis-rest-portal/-/arcgis-rest-portal-3.6.0.tgz", + "integrity": "sha512-TPLcbQn+PfKqGlkCvlDYs2AQs7KdTKnM17AhGytnQkqYamNlhkZMAlgC4qq5/H7URTIqlQx52fx/OOfTfO0ABw==", + "peer": true, + "requires": { + "@esri/arcgis-rest-types": "^3.6.0", + "tslib": "^1.13.0" + } + }, + "@esri/arcgis-rest-request": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/@esri/arcgis-rest-request/-/arcgis-rest-request-3.6.0.tgz", + "integrity": "sha512-JDFx1ZjheFUfW/YXWQ0DwxgIwPdQFBtIl5+Bvee4cyCmhxpO1/b82tv7nOLRMnMYBkC2aKRCgpAwkecaJZDpdw==", + "peer": true, + "requires": { + "tslib": "^1.10.0" + } + }, + "@esri/arcgis-rest-types": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/@esri/arcgis-rest-types/-/arcgis-rest-types-3.6.0.tgz", + "integrity": "sha512-t6QWdVNmqB9OloYAvVWYNjvqlnrXs/m0nCRNwPGt3ZiAPXn5CpkpSn2UD6LPqGp6vPxKG81lbFQvwCw9az4qIg==", + "peer": true + }, + "@types/adlib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/adlib/-/adlib-3.0.1.tgz", + "integrity": "sha512-7vRDaOFejVMdjzKagD45fK+mFs8pb0h9zmvsGrYsaQY9z+X3xUqf71uNMv8+OiVa8u/w1A7DS29LSUFzOnstRw==", + "dev": true + }, + "abab": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", + "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==" + }, + "adlib": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/adlib/-/adlib-3.0.8.tgz", + "integrity": "sha512-CbQ+mcm45pdDN0aAiGYP6FZWDor+BM4DbqJsrUptM79a2+FbiP8QdF0xHABbgCkioApKqg8lI33Gl6X8GFelfA==", + "requires": { + "esm": "^3.2.25" + } + }, + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "esm": { + "version": "3.2.25", + "resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz", + "integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==" + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + }, + "fast-xml-parser": { + "version": "3.21.1", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-3.21.1.tgz", + "integrity": "sha512-FTFVjYoBOZTJekiUsawGsSYV9QL0A+zDYCRj7y34IO6Jg+2IMYEtQa+bbictpdpV8dHxXywqU7C0gRDEOFtBFg==", + "requires": { + "strnum": "^1.0.4" + } + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, + "json-schema-typed": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-7.0.3.tgz", + "integrity": "sha512-7DE8mpG+/fVw+dTpjbxnx47TaMnDfOI1jwft9g1VybltZCduyRQPJPvc+zzKY9WPHxhPWczyFuYa6I8Mw4iU5A==" + }, + "json-typescript": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/json-typescript/-/json-typescript-1.1.2.tgz", + "integrity": "sha512-Np07MUsYMKbB0nNlw/MMIRjUK7ehO48LA4FsrzrhCfTUxMKbvOBAo0sc0b4nQ80ge9d32sModCunCgoyUojgUA==" + }, + "jsonapi-typescript": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/jsonapi-typescript/-/jsonapi-typescript-0.1.3.tgz", + "integrity": "sha512-uPcPS01GeM+4HIyn18s7l1yD2S3uLZy2TX1UkQffCM0bLb3TMwudXUyVXPHTMZ4vdZT8MqKqN2vjB5PogTAdFQ==", + "requires": { + "json-typescript": "^1.0.0" + } + }, + "punycode": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", + "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==" + }, + "strnum": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz", + "integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==" + }, + "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.10", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.10.tgz", + "integrity": "sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q==", + "dev": true + }, + "uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "requires": { + "punycode": "^2.1.0" + } + } + } +} diff --git a/packages/common/src/core/types/IHubPage.ts b/packages/common/src/core/types/IHubPage.ts index b083e1e599f..7133798f713 100644 --- a/packages/common/src/core/types/IHubPage.ts +++ b/packages/common/src/core/types/IHubPage.ts @@ -1,8 +1,16 @@ -import { IWithLayout } from "../traits/IWithLayout"; import { IHubItemEntity } from "./IHubItemEntity"; - +// import { IWithVersioningBehavior } from "../behaviors"; +import { + IWithLayout, + // IWithPermissions, + // IWithSlug, +} from "../traits/index"; /** * DRAFT: Under development and more properties will likely be added * @internal */ -export interface IHubPage extends IHubItemEntity, IWithLayout {} +export interface IHubPage extends IHubItemEntity, IWithLayout { + // IWithPermissions, + // IWithSlug, + // IWithVersioningBehavior +} diff --git a/packages/common/src/pages/HubPage.ts b/packages/common/src/pages/HubPage.ts new file mode 100644 index 00000000000..1c42b7d051f --- /dev/null +++ b/packages/common/src/pages/HubPage.ts @@ -0,0 +1,170 @@ +import { + IHubPage, + // IWithPermissionBehavior, + // IWithStoreBehavior, + // IWithSharingBehavior, +} from "../core"; + +import { IArcGISContext } from "../ArcGISContext"; +import { HubItemEntity } from "../core/HubItemEntity"; + +// import { DEFAULT_PAGE } from "./defaults"; +import { + // createPage, + deletePage, + // ENTERPRISE_PAGE_ITEM_TYPE, + fetchPage, + // HUB_PAGE_ITEM_TYPE, + // updatePage, +} from "./HubPages"; + +/** + * Hub Page Class + * NOTE: This is a minimal implementation. Create operations are not supported at this time + */ +export class HubPage extends HubItemEntity { + // implements + // IWithStoreBehavior, + // IWithPermissionBehavior, + // IWithSharingBehavior, + /** + * Private constructor so we don't have `new` all over the place. Allows for + * more flexibility in how we create the HubPageManager over time. + * @param context + */ + private constructor(page: IHubPage, context: IArcGISContext) { + super(page, context); + } + + /** + * Create an instance from an IHubPage object + * @param json - JSON object to create a HubPage from + * @param context - ArcGIS context + * @returns + */ + static fromJson(json: Partial, context: IArcGISContext): HubPage { + // merge what we have with the default values + const pojo = this.applyDefaults(json, context); + return new HubPage(pojo, context); + } + + /** + * + * NOT IMPLEMENTED YET: Create a new HubPage, returning a HubPage instance. + * By default, this does not save the page to the backing store. + * @param partialPage + * @param context + * @returns + */ + static async create( + partialPage: Partial, + context: IArcGISContext, + save: boolean = false + ): Promise { + const pojo = this.applyDefaults(partialPage, context); + // return an instance of HubPage + const instance = HubPage.fromJson(pojo, context); + if (save) { + await instance.save(); + } + return instance; + } + + /** + * Fetch a Page from the backing store and return a HubPage instance. + * @param identifier - Identifier of the page to load + * @param context + * @returns + */ + static async fetch( + identifier: string, + context: IArcGISContext + ): Promise { + // fetch the page by id or slug + try { + const page = await fetchPage(identifier, context.hubRequestOptions); + // create an instance of HubPage from the page + return HubPage.fromJson(page, context); + } catch (ex) { + if ( + (ex as Error).message === + "CONT_0001: Item does not exist or is inaccessible." + ) { + throw new Error(`Page not found.`); + } else { + throw ex; + } + } + } + + private static applyDefaults( + partialPage: Partial, + context: IArcGISContext + ): IHubPage { + // ensure we have the orgUrlKey + // if (!partialPage.orgUrlKey) { + // partialPage.orgUrlKey = context.portal.urlKey; + // } + // extend the partial over the defaults + const pojo = { ...DEFAULT_PAGE, ...partialPage } as IHubPage; + // pojo.type = context.isPortal + // ? ENTERPRISE_PAGE_ITEM_TYPE + // : HUB_PAGE_ITEM_TYPE; + return pojo; + } + + /** + * Apply a new state to the instance + * @param changes + */ + update(changes: Partial): void { + if (this.isDestroyed) { + throw new Error("HubPage is already destroyed."); + } + // merge partial onto existing entity + this.entity = { ...this.entity, ...changes }; + } + + /** + * Save the HubPage to the backing store. + * Currently Pages are stored as Items in Portal + * @returns + */ + async save(): Promise { + if (this.isDestroyed) { + throw new Error("HubPage is already destroyed."); + } + + if (this.entity.id) { + // update it + this.entity = await updatePage( + this.entity, + this.context.hubRequestOptions + ); + } else { + // create it + this.entity = await createPage( + this.entity, + this.context.hubRequestOptions + ); + } + // call the after save hook on superclass + await super.afterSave(); + + return; + } + + /** + * Delete the HubPage from the store + * set a flag to indicate that it is destroyed + * @returns + */ + async delete(): Promise { + if (this.isDestroyed) { + throw new Error("HubPage is already destroyed."); + } + this.isDestroyed = true; + // Delegate to module fn + await deletePage(this.entity.id, this.context.userRequestOptions); + } +} diff --git a/packages/common/src/pages/HubPages.ts b/packages/common/src/pages/HubPages.ts index cb5ad40c6f2..9c6e8b6fb86 100644 --- a/packages/common/src/pages/HubPages.ts +++ b/packages/common/src/pages/HubPages.ts @@ -1,4 +1,3 @@ -import { IItem } from "@esri/arcgis-rest-types"; import { getFamily } from "../content/get-family"; import { getHubRelativeUrl } from "../content/_internal/internalContentUtils"; import { fetchItemEnrichments } from "../items/_enrichments"; @@ -6,10 +5,79 @@ import { getProp } from "../objects"; import { getItemThumbnailUrl } from "../resources"; import { IHubSearchResult } from "../search"; import { parseInclude } from "../search/_internal/parseInclude"; -import { IHubRequestOptions } from "../types"; +import { IHubRequestOptions, IModel } from "../types"; import { getItemHomeUrl } from "../urls"; +import { IRequestOptions } from "@esri/arcgis-rest-request"; +import { getItem, IItem } from "@esri/arcgis-rest-portal"; import { unique } from "../util"; -import { mapBy } from "../utils"; +import { mapBy, isGuid } from "../utils"; +import { getItemBySlug } from "../items/slugs"; +import { IHubPage } from "../core"; +import { fetchModelFromItem } from "../models"; +import { PropertyMapper } from "../core/_internal/PropertyMapper"; +import { getPropertyMap } from "./_internal/getPropertyMap"; +import { computeProps } from "./_internal/computeProps"; +import { IUserRequestOptions } from "@esri/arcgis-rest-auth"; +import { IUserItemOptions, removeItem } from "@esri/arcgis-rest-portal"; + +/** + * @private + * Get a Hub Page by id or slug + * @param identifier item id or slug + * @param requestOptions + */ +export async function fetchPage( + identifier: string, + requestOptions: IRequestOptions +): Promise { + let getPrms; + if (isGuid(identifier)) { + // get item by id + getPrms = getItem(identifier, requestOptions); + } else { + getPrms = getItemBySlug(identifier, requestOptions); + } + return getPrms.then((item) => { + if (!item) return null; + return convertItemToPage(item, requestOptions); + }); +} + +/** + * @private + * Convert an Hub Page Item into a Hub Project, fetching any additional + * information that may be required + * @param item + * @param auth + * @returns + */ +export async function convertItemToPage( + item: IItem, + requestOptions: IRequestOptions +): Promise { + const model = await fetchModelFromItem(item, requestOptions); + // TODO: In the future we will handle the boundary fetching from resource + const mapper = new PropertyMapper, IModel>( + getPropertyMap() + ); + const prj = mapper.storeToEntity(model, {}) as IHubPage; + return computeProps(model, prj, requestOptions); +} + +/** + * @private + * Remove a Hub Page + * @param id + * @param requestOptions + */ +export async function deletePage( + id: string, + requestOptions: IUserRequestOptions +): Promise { + const ro = { ...requestOptions, ...{ id } } as IUserItemOptions; + await removeItem(ro); + return; +} /** * Fetch Page specific Enrichments diff --git a/packages/common/src/pages/_internal/computeProps.ts b/packages/common/src/pages/_internal/computeProps.ts new file mode 100644 index 00000000000..ce9d274a56f --- /dev/null +++ b/packages/common/src/pages/_internal/computeProps.ts @@ -0,0 +1,46 @@ +import { IRequestOptions } from "@esri/arcgis-rest-request"; +import { UserSession } from "@esri/arcgis-rest-auth"; +import { getItemThumbnailUrl } from "../../resources"; +import { IHubPage } from "../../core"; +import { IModel } from "../../types"; +// import { PageDefaultCapabilities } from "./ProjectBusinessRules"; +// import { processEntityCapabilities } from "../../capabilities"; +import { isDiscussable } from "../../discussions"; + +/** + * Given a model and a page, set various computed properties that can't be directly mapped + * @private + * @param model + * @param page + * @param requestOptions + * @returns + */ +export function computeProps( + model: IModel, + page: Partial, + requestOptions: IRequestOptions +): IHubPage { + let token: string; + if (requestOptions.authentication) { + const session: UserSession = requestOptions.authentication as UserSession; + token = session.token; + } + // thumbnail url + page.thumbnailUrl = getItemThumbnailUrl(model.item, requestOptions, token); + + // Handle Dates + page.createdDate = new Date(model.item.created); + page.createdDateSource = "item.created"; + page.updatedDate = new Date(model.item.modified); + page.updatedDateSource = "item.modified"; + page.isDiscussable = isDiscussable(page); + + // // Handle capabilities + // page.capabilities = processEntityCapabilities( + // model.data.settings?.capabilities || {}, + // PageDefaultCapabilities + // ); + + // cast b/c this takes a partial but returns a full page + return page as IHubPage; +} diff --git a/packages/common/src/pages/_internal/getPropertyMap.ts b/packages/common/src/pages/_internal/getPropertyMap.ts new file mode 100644 index 00000000000..283a5de1a75 --- /dev/null +++ b/packages/common/src/pages/_internal/getPropertyMap.ts @@ -0,0 +1,24 @@ +import { IPropertyMap } from "../../core/_internal/PropertyMapper"; +import { getBasePropertyMap } from "../../core/_internal/getBasePropertyMap"; + +/** + * Returns an Array of IPropertyMap objects + * that define the projection of properties from a IModel to an IHubPage + * @returns + * @private + */ + +export function getPropertyMap(): IPropertyMap[] { + const map = getBasePropertyMap(); + + /** + * page-specific mappings. + */ + // map.push({ entityKey: "permissions", storeKey: "data.permissions" }); + const valueProps = ["headContent", "layout"]; + valueProps.forEach((entityKey) => { + map.push({ entityKey, storeKey: `data.values.${entityKey}` }); + }); + + return map; +} From fb148452cea13cf17ddff4bcbeb6f2866bb0a826 Mon Sep 17 00:00:00 2001 From: Marvin Perry Date: Thu, 6 Jul 2023 16:34:46 -0400 Subject: [PATCH 2/6] feat(): add HubPage test --- packages/common/test/pages/HubPage.test.ts | 365 +++++++++++++++++++++ 1 file changed, 365 insertions(+) create mode 100644 packages/common/test/pages/HubPage.test.ts diff --git a/packages/common/test/pages/HubPage.test.ts b/packages/common/test/pages/HubPage.test.ts new file mode 100644 index 00000000000..a87e91fe911 --- /dev/null +++ b/packages/common/test/pages/HubPage.test.ts @@ -0,0 +1,365 @@ +import * as PortalModule from "@esri/arcgis-rest-portal"; +import { ArcGISContextManager } from "../../src/ArcGISContextManager"; +import { HubPage } from "../../src/pages/HubPage"; +import { MOCK_AUTH } from "../mocks/mock-auth"; +import * as HubPagesModule from "../../src/pages/HubPages"; +import { + // IDeepCatalogInfo, + // IHubCatalog, + IHubPage, +} from "../../src"; +// import { Catalog } from "../../src/search"; +import * as ContainsModule from "../../src/core/_internal/deepContains"; +describe("HubPage Class:", () => { + let authdCtxMgr: ArcGISContextManager; + let portalCtxMgr: ArcGISContextManager; + let unauthdCtxMgr: ArcGISContextManager; + beforeEach(async () => { + unauthdCtxMgr = await ArcGISContextManager.create(); + // When we pass in all this information, the context + // manager will not try to fetch anything, so no need + // to mock those calls + authdCtxMgr = await ArcGISContextManager.create({ + authentication: MOCK_AUTH, + currentUser: { + username: "casey", + } as unknown as PortalModule.IUser, + portal: { + name: "DC R&D Center", + id: "BRXFAKE", + urlKey: "fake-org", + } as unknown as PortalModule.IPortal, + portalUrl: "https://fake-org.maps.arcgis.com", + }); + portalCtxMgr = await ArcGISContextManager.create({ + authentication: MOCK_AUTH, + currentUser: { + username: "casey", + } as unknown as PortalModule.IUser, + portal: { + isPortal: true, + name: "My Portal Install", + id: "BRXFAKE", + urlKey: "fake-org", + } as unknown as PortalModule.IPortal, + portalUrl: "https://myserver.com", + }); + }); + + describe("static methods:", () => { + it("loads from minimal json", () => { + const createSpy = spyOn(HubPagesModule, "createPage"); + const chk = HubPage.fromJson({ name: "Test Page" }, authdCtxMgr.context); + + expect(createSpy).not.toHaveBeenCalled(); + expect(chk.toJson().name).toEqual("Test Page"); + // adds empty permissions and catalog + const json = chk.toJson(); + expect(json.permissions).toEqual([]); + // expect(json.catalog).toEqual({ schemaVersion: 0 }); + }); + it("loads based on identifier", async () => { + const fetchSpy = spyOn(HubPagesModule, "fetchPage").and.callFake( + (id: string) => { + return Promise.resolve({ + id, + name: "Test Page", + }); + } + ); + + const chk = await HubPage.fetch("3ef", authdCtxMgr.context); + expect(fetchSpy).toHaveBeenCalledTimes(1); + expect(chk.toJson().id).toBe("3ef"); + expect(chk.toJson().name).toBe("Test Page"); + }); + + it("throws if site not found", async () => { + const fetchSpy = spyOn(HubPagesModule, "fetchPage").and.callFake( + (id: string) => { + const err = new Error( + "CONT_0001: Item does not exist or is inaccessible." + ); + return Promise.reject(err); + } + ); + try { + await HubPage.fetch("3ef", authdCtxMgr.context); + } catch (ex) { + expect(fetchSpy).toHaveBeenCalledTimes(1); + expect(ex.message).toBe("Page not found."); + } + }); + + it("handle load errors", async () => { + const fetchSpy = spyOn(HubPagesModule, "fetchPage").and.callFake( + (id: string) => { + const err = new Error("ZOMG!"); + return Promise.reject(err); + } + ); + try { + await HubPage.fetch("3ef", authdCtxMgr.context); + } catch (ex) { + expect(fetchSpy).toHaveBeenCalledTimes(1); + expect(ex.message).toBe("ZOMG!"); + } + }); + }); + + it("save calls createPage if object does not have an id", async () => { + const createSpy = spyOn(HubPagesModule, "createPage").and.callFake( + (p: IHubPage) => { + return Promise.resolve(p); + } + ); + const chk = await HubPage.fromJson( + { name: "Test Page" }, + authdCtxMgr.context + ); + await chk.save(); + expect(createSpy).toHaveBeenCalledTimes(1); + expect(chk.toJson().name).toEqual("Test Page"); + }); + + it("create saves the instance if passed true", async () => { + const createSpy = spyOn(HubPagesModule, "createPage").and.callFake( + (p: IHubPage) => { + p.id = "3ef"; + return Promise.resolve(p); + } + ); + const chk = await HubPage.create( + { name: "Test Page" }, + authdCtxMgr.context, + true + ); + + expect(createSpy).toHaveBeenCalledTimes(1); + expect(chk.toJson().name).toEqual("Test Page"); + expect(chk.toJson().type).toEqual("Hub Site Application"); + }); + it("create does not save by default", async () => { + const createSpy = spyOn(HubPagesModule, "createPage"); + const chk = await HubPage.create( + { name: "Test Page", orgUrlKey: "foo" }, + portalCtxMgr.context + ); + + expect(createSpy).not.toHaveBeenCalled(); + expect(chk.toJson().name).toEqual("Test Page"); + expect(chk.toJson().type).toEqual("Site Application"); + }); + + // it("update applies partial chagnes to internal state", () => { + // const chk = HubPage.fromJson( + // { name: "Test Page", catalog: { schemaVersion: 0 } }, + // authdCtxMgr.context + // ); + // chk.update({ + // name: "Test Site 2", + // permissions: [ + // { + // permission: "hub:project:create", + // collaborationType: "group", + // collaborationId: "3ef", + // }, + // ], + // catalog: { schemaVersion: 2 }, + // }); + // expect(chk.toJson().name).toEqual("Test Site 2"); + // expect(chk.toJson().catalog).toEqual({ schemaVersion: 2 }); + + // chk.update({ tags: ["one", "two"] }); + // expect(chk.toJson().tags).toEqual(["one", "two"]); + // }); + + // it("save updates if object has id", async () => { + // const updateSpy = spyOn(HubPagesModule, "updatePage").and.callFake( + // (p: IHubPage) => { + // return Promise.resolve(p); + // } + // ); + // const chk = HubPage.fromJson( + // { + // id: "bc3", + // name: "Test Page", + // catalog: { schemaVersion: 0 }, + // }, + // authdCtxMgr.context + // ); + // await chk.save(); + // expect(updateSpy).toHaveBeenCalledTimes(1); + // }); + + it("delete", async () => { + const deleteSpy = spyOn(HubPagesModule, "deletePage").and.callFake(() => { + return Promise.resolve(); + }); + const chk = HubPage.fromJson({ name: "Test Page" }, authdCtxMgr.context); + await chk.delete(); + expect(deleteSpy).toHaveBeenCalledTimes(1); + // all fns should now throw an error + expect(() => { + chk.toJson(); + }).toThrowError("Entity is already destroyed."); + + expect(() => { + chk.update({ name: "Test Site 2" } as IHubPage); + }).toThrowError("HubPage is already destroyed."); + + // async calls + try { + await chk.delete(); + } catch (e) { + expect(e.message).toEqual("HubPage is already destroyed."); + } + + try { + await chk.save(); + } catch (e) { + expect(e.message).toEqual("HubPage is already destroyed."); + } + }); + + // it("internal instance accessors", () => { + // const chk = HubPage.fromJson( + // { name: "Test Page", catalog: { schemaVersion: 0 } }, + // authdCtxMgr.context + // ); + + // expect(chk.catalog instanceof Catalog).toBeTruthy(); + // }); + + // it("setting catalog updates catalog instance", () => { + // const chk = HubPage.fromJson( + // { name: "Test Page", catalog: { schemaVersion: 0 } }, + // authdCtxMgr.context + // ); + // chk.update({ catalog: { schemaVersion: 2 } }); + // expect(chk.toJson().catalog).toEqual({ schemaVersion: 2 }); + // expect(chk.catalog.schemaVersion).toEqual(2); + // }); + // describe(" contains:", () => { + // it("checks site catalog by default", async () => { + // const containsSpy = spyOn(ContainsModule, "deepContains").and.callFake( + // (id: string, h: IDeepCatalogInfo[]) => { + // return Promise.resolve({ + // identifier: id, + // isContained: true, + // catalogInfo: {}, + // }); + // } + // ); + // const chk = HubPage.fromJson( + // { + // id: "3ef", + // catalog: createCatalog("00a"), + // }, + // authdCtxMgr.context + // ); + // const result = await chk.contains("cc0"); + // expect(containsSpy).toHaveBeenCalledTimes(1); + // const hiearchy = containsSpy.calls.argsFor(0)[1]; + // expect(hiearchy.length).toBe(1); + // expect(hiearchy[0].catalog).toEqual( + // createCatalog("00a"), + // "should pass the site catalog" + // ); + // expect(result).toEqual({ + // identifier: "cc0", + // isContained: true, + // catalogInfo: {}, + // }); + // }); + + // it("adds site catalog to others", async () => { + // const containsSpy = spyOn(ContainsModule, "deepContains").and.callFake( + // (id: string, h: IDeepCatalogInfo[]) => { + // return Promise.resolve({ + // identifier: id, + // isContained: true, + // catalogInfo: {}, + // }); + // } + // ); + // const chk = HubPage.fromJson( + // { + // id: "3ef", + // catalog: createCatalog("00a"), + // }, + // authdCtxMgr.context + // ); + // // pass in a project catalog + // const result = await chk.contains("cc0", [ + // { id: "4ef", entityType: "item", catalog: createCatalog("00b") }, + // ]); + // expect(containsSpy).toHaveBeenCalledTimes(1); + // const hiearchy = containsSpy.calls.argsFor(0)[1]; + // expect(hiearchy.length).toBe(2); + // expect(hiearchy[0].catalog).toEqual( + // createCatalog("00b"), + // "should pass the project catalog" + // ); + // expect(hiearchy[1].catalog).toEqual( + // createCatalog("00a"), + // "should pass the site catalog" + // ); + // expect(result).toEqual({ + // identifier: "cc0", + // isContained: true, + // catalogInfo: {}, + // }); + // }); + + // it("caches catalogs", async () => { + // const containsSpy = spyOn(ContainsModule, "deepContains").and.callFake( + // (id: string, h: IDeepCatalogInfo[]) => { + // return Promise.resolve({ + // identifier: id, + // isContained: true, + // catalogInfo: { + // "3ef": { + // catalog: createCatalog("00a"), + // }, + // "4ef": { + // catalog: createCatalog("00b"), + // }, + // }, + // }); + // } + // ); + // const chk = HubPage.fromJson( + // { + // id: "3ef", + // catalog: createCatalog("00a"), + // }, + // authdCtxMgr.context + // ); + // // First call will warm the cache + // await chk.contains("cc0", [{ id: "4ef", entityType: "item" }]); + // // second call will use the cache + // await chk.contains("cc1", [{ id: "4ef", entityType: "item" }]); + // expect(containsSpy).toHaveBeenCalledTimes(2); + // // verify first call does not send the 4ef catalog + // const hiearchy = containsSpy.calls.argsFor(0)[1]; + // expect(hiearchy.length).toBe(2); + // expect(hiearchy[0].catalog).not.toBeDefined(); + // expect(hiearchy[1].catalog).toEqual( + // createCatalog("00a"), + // "should pass the site catalog" + // ); + // // verify second call does send the 4ef catalog + // const hiearchy2 = containsSpy.calls.argsFor(1)[1]; + // expect(hiearchy2.length).toBe(2); + // expect(hiearchy2[0].catalog).toEqual( + // createCatalog("00b"), + // "should pass the project catalog" + // ); + // expect(hiearchy2[1].catalog).toEqual( + // createCatalog("00a"), + // "should pass the site catalog" + // ); + // }); + // }); +}); From 4f351fa913cef3a9d1d1798d3cfd5277b51d861a Mon Sep 17 00:00:00 2001 From: Michael Juniper Date: Mon, 10 Jul 2023 16:14:53 -0600 Subject: [PATCH 3/6] feat: hubpage entity --- package-lock.json | 46 +++++---- packages/common/src/core/fetchHubEntity.ts | 4 +- packages/common/src/core/types/IHubPage.ts | 13 +-- packages/common/src/pages/HubPage.ts | 57 ++++++----- packages/common/src/pages/HubPages.ts | 99 ++++++++++++++++++- .../src/pages/_internal/computeProps.ts | 4 +- packages/common/src/pages/defaults.ts | 48 +++++++++ 7 files changed, 215 insertions(+), 56 deletions(-) create mode 100644 packages/common/src/pages/defaults.ts diff --git a/package-lock.json b/package-lock.json index 1e686eda7dd..33cecab5ab7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22644,9 +22644,10 @@ }, "node_modules/cz-lerna-changelog/node_modules/npm/node_modules/lodash._baseindexof": { "version": "3.1.0", - "extraneous": true, + "dev": true, "inBundle": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/cz-lerna-changelog/node_modules/npm/node_modules/lodash._baseuniq": { "version": "4.6.0", @@ -22661,21 +22662,24 @@ }, "node_modules/cz-lerna-changelog/node_modules/npm/node_modules/lodash._bindcallback": { "version": "3.0.1", - "extraneous": true, + "dev": true, "inBundle": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/cz-lerna-changelog/node_modules/npm/node_modules/lodash._cacheindexof": { "version": "3.0.2", - "extraneous": true, + "dev": true, "inBundle": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/cz-lerna-changelog/node_modules/npm/node_modules/lodash._createcache": { "version": "3.1.2", - "extraneous": true, + "dev": true, "inBundle": true, "license": "MIT", + "peer": true, "dependencies": { "lodash._getnative": "^3.0.0" } @@ -22689,9 +22693,10 @@ }, "node_modules/cz-lerna-changelog/node_modules/npm/node_modules/lodash._getnative": { "version": "3.9.1", - "extraneous": true, + "dev": true, "inBundle": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/cz-lerna-changelog/node_modules/npm/node_modules/lodash._root": { "version": "3.0.1", @@ -22709,9 +22714,10 @@ }, "node_modules/cz-lerna-changelog/node_modules/npm/node_modules/lodash.restparam": { "version": "3.6.1", - "extraneous": true, + "dev": true, "inBundle": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/cz-lerna-changelog/node_modules/npm/node_modules/lodash.union": { "version": "4.6.0", @@ -83376,7 +83382,8 @@ "lodash._baseindexof": { "version": "3.1.0", "bundled": true, - "extraneous": true + "dev": true, + "peer": true }, "lodash._baseuniq": { "version": "4.6.0", @@ -83391,17 +83398,20 @@ "lodash._bindcallback": { "version": "3.0.1", "bundled": true, - "extraneous": true + "dev": true, + "peer": true }, "lodash._cacheindexof": { "version": "3.0.2", "bundled": true, - "extraneous": true + "dev": true, + "peer": true }, "lodash._createcache": { "version": "3.1.2", "bundled": true, - "extraneous": true, + "dev": true, + "peer": true, "requires": { "lodash._getnative": "^3.0.0" } @@ -83415,7 +83425,8 @@ "lodash._getnative": { "version": "3.9.1", "bundled": true, - "extraneous": true + "dev": true, + "peer": true }, "lodash._root": { "version": "3.0.1", @@ -83432,7 +83443,8 @@ "lodash.restparam": { "version": "3.6.1", "bundled": true, - "extraneous": true + "dev": true, + "peer": true }, "lodash.union": { "version": "4.6.0", diff --git a/packages/common/src/core/fetchHubEntity.ts b/packages/common/src/core/fetchHubEntity.ts index 0b2554643e3..aa109cfd987 100644 --- a/packages/common/src/core/fetchHubEntity.ts +++ b/packages/common/src/core/fetchHubEntity.ts @@ -1,6 +1,7 @@ import { fetchDiscussion } from "../discussions/fetch"; import { fetchHubContent } from "../content/fetch"; import { fetchInitiative } from "../initiatives/HubInitiatives"; +import { fetchPage } from "../pages/HubPages"; import { fetchProject } from "../projects/fetch"; import { fetchSite } from "../sites/HubSites"; import { HubEntity } from "./types/HubEntity"; @@ -34,7 +35,8 @@ export async function fetchHubEntity( result = await fetchDiscussion(identifier, context.hubRequestOptions); break; case "page": - throw new Error(`FetchPage not implemented`); + result = await fetchPage(identifier, context.hubRequestOptions); + break; case "content": result = await fetchHubContent(identifier, context.requestOptions); } diff --git a/packages/common/src/core/types/IHubPage.ts b/packages/common/src/core/types/IHubPage.ts index 7133798f713..66b5de8c9d2 100644 --- a/packages/common/src/core/types/IHubPage.ts +++ b/packages/common/src/core/types/IHubPage.ts @@ -3,14 +3,15 @@ import { IHubItemEntity } from "./IHubItemEntity"; import { IWithLayout, // IWithPermissions, - // IWithSlug, + IWithSlug, } from "../traits/index"; /** * DRAFT: Under development and more properties will likely be added * @internal */ -export interface IHubPage extends IHubItemEntity, IWithLayout { - // IWithPermissions, - // IWithSlug, - // IWithVersioningBehavior -} +export interface IHubPage + extends IHubItemEntity, + IWithLayout, + // IWithPermissions, + // IWithVersioningBehavior + IWithSlug {} diff --git a/packages/common/src/pages/HubPage.ts b/packages/common/src/pages/HubPage.ts index 1c42b7d051f..6c5d09f8ce3 100644 --- a/packages/common/src/pages/HubPage.ts +++ b/packages/common/src/pages/HubPage.ts @@ -1,32 +1,35 @@ -import { - IHubPage, - // IWithPermissionBehavior, - // IWithStoreBehavior, - // IWithSharingBehavior, -} from "../core"; +import { IHubPage, IWithStoreBehavior, IWithSharingBehavior } from "../core"; import { IArcGISContext } from "../ArcGISContext"; import { HubItemEntity } from "../core/HubItemEntity"; -// import { DEFAULT_PAGE } from "./defaults"; import { - // createPage, - deletePage, - // ENTERPRISE_PAGE_ITEM_TYPE, - fetchPage, - // HUB_PAGE_ITEM_TYPE, - // updatePage, -} from "./HubPages"; + DEFAULT_PAGE, + ENTERPRISE_PAGE_ITEM_TYPE, + HUB_PAGE_ITEM_TYPE, +} from "./defaults"; + +import { createPage, deletePage, fetchPage, updatePage } from "./HubPages"; + +/* + TODO: + - slug stuff + - default stuff + - what about page-site lingage? + - permissions? + - capabilities? + - need to look at what is being done in the old HubSites package + - marvin does not have access to class diagram +*/ /** * Hub Page Class * NOTE: This is a minimal implementation. Create operations are not supported at this time */ -export class HubPage extends HubItemEntity { - // implements - // IWithStoreBehavior, - // IWithPermissionBehavior, - // IWithSharingBehavior, +export class HubPage + extends HubItemEntity + implements IWithStoreBehavior, IWithSharingBehavior +{ /** * Private constructor so we don't have `new` all over the place. Allows for * more flexibility in how we create the HubPageManager over time. @@ -102,14 +105,14 @@ export class HubPage extends HubItemEntity { context: IArcGISContext ): IHubPage { // ensure we have the orgUrlKey - // if (!partialPage.orgUrlKey) { - // partialPage.orgUrlKey = context.portal.urlKey; - // } + if (!partialPage.orgUrlKey) { + partialPage.orgUrlKey = context.portal.urlKey; + } // extend the partial over the defaults const pojo = { ...DEFAULT_PAGE, ...partialPage } as IHubPage; - // pojo.type = context.isPortal - // ? ENTERPRISE_PAGE_ITEM_TYPE - // : HUB_PAGE_ITEM_TYPE; + pojo.type = context.isPortal + ? ENTERPRISE_PAGE_ITEM_TYPE + : HUB_PAGE_ITEM_TYPE; return pojo; } @@ -139,13 +142,13 @@ export class HubPage extends HubItemEntity { // update it this.entity = await updatePage( this.entity, - this.context.hubRequestOptions + this.context.userRequestOptions ); } else { // create it this.entity = await createPage( this.entity, - this.context.hubRequestOptions + this.context.userRequestOptions ); } // call the after save hook on superclass diff --git a/packages/common/src/pages/HubPages.ts b/packages/common/src/pages/HubPages.ts index 9c6e8b6fb86..a166ca97259 100644 --- a/packages/common/src/pages/HubPages.ts +++ b/packages/common/src/pages/HubPages.ts @@ -9,16 +9,109 @@ import { IHubRequestOptions, IModel } from "../types"; import { getItemHomeUrl } from "../urls"; import { IRequestOptions } from "@esri/arcgis-rest-request"; import { getItem, IItem } from "@esri/arcgis-rest-portal"; -import { unique } from "../util"; +import { cloneObject, unique } from "../util"; import { mapBy, isGuid } from "../utils"; -import { getItemBySlug } from "../items/slugs"; +import { + constructSlug, + getItemBySlug, + getUniqueSlug, + setSlugKeyword, +} from "../items/slugs"; import { IHubPage } from "../core"; -import { fetchModelFromItem } from "../models"; +import { + createModel, + fetchModelFromItem, + getModel, + updateModel, +} from "../models"; import { PropertyMapper } from "../core/_internal/PropertyMapper"; import { getPropertyMap } from "./_internal/getPropertyMap"; import { computeProps } from "./_internal/computeProps"; import { IUserRequestOptions } from "@esri/arcgis-rest-auth"; import { IUserItemOptions, removeItem } from "@esri/arcgis-rest-portal"; +import { + DEFAULT_PAGE, + DEFAULT_PAGE_MODEL, + HUB_PAGE_ITEM_TYPE, + ENTERPRISE_PAGE_ITEM_TYPE, +} from "./defaults"; + +/** + * @private + * Create a new Hub Project item + * + * Minimal properties are name and org + * + * @param project + * @param requestOptions + */ +export async function createPage( + partialPage: Partial, + requestOptions: IUserRequestOptions +): Promise { + // merge incoming with the default + // this expansion solves the typing somehow + const page = { ...DEFAULT_PAGE, ...partialPage }; + + // Create a slug from the title if one is not passed in + if (!page.slug) { + page.slug = constructSlug(page.name, page.orgUrlKey); + } + // Ensure slug is unique + page.slug = await getUniqueSlug({ slug: page.slug }, requestOptions); + // add slug and status to keywords + page.typeKeywords = setSlugKeyword(page.typeKeywords, page.slug); + + // Map project object onto a default project Model + const mapper = new PropertyMapper, IModel>( + getPropertyMap() + ); + // create model from object, using the default model as a starting point + let model = mapper.entityToStore(page, cloneObject(DEFAULT_PAGE_MODEL)); + // create the item + model = await createModel(model, requestOptions); + // map the model back into a IHubProject + let newProject = mapper.storeToEntity(model, {}); + newProject = computeProps(model, newProject, requestOptions); + // and return it + return newProject as IHubPage; +} + +/** + * @private + * Update a Hub Project + * @param page + * @param requestOptions + */ +export async function updatePage( + page: IHubPage, + requestOptions: IUserRequestOptions +): Promise { + // verify that the slug is unique, excluding the current project + page.slug = await getUniqueSlug( + { slug: page.slug, existingId: page.id }, + requestOptions + ); + + // get the backing item & data + const model = await getModel(page.id, requestOptions); + // create the PropertyMapper + const mapper = new PropertyMapper, IModel>( + getPropertyMap() + ); + // Note: Although we are fetching the model, and applying changes onto it, + // we are not attempting to handle "concurrent edit" conflict resolution + // but this is where we would apply that sort of logic + const modelToUpdate = mapper.entityToStore(page, model); + // update the backing item + const updatedModel = await updateModel(modelToUpdate, requestOptions); + // now map back into a project and return that + let updatedProject = mapper.storeToEntity(updatedModel, page); + updatedProject = computeProps(model, updatedProject, requestOptions); + // the casting is needed because modelToObject returns a `Partial` + // where as this function returns a `T` + return updatedProject as IHubPage; +} /** * @private diff --git a/packages/common/src/pages/_internal/computeProps.ts b/packages/common/src/pages/_internal/computeProps.ts index ce9d274a56f..1394cc4f95b 100644 --- a/packages/common/src/pages/_internal/computeProps.ts +++ b/packages/common/src/pages/_internal/computeProps.ts @@ -5,7 +5,7 @@ import { IHubPage } from "../../core"; import { IModel } from "../../types"; // import { PageDefaultCapabilities } from "./ProjectBusinessRules"; // import { processEntityCapabilities } from "../../capabilities"; -import { isDiscussable } from "../../discussions"; +// import { isDiscussable } from "../../discussions"; /** * Given a model and a page, set various computed properties that can't be directly mapped @@ -33,7 +33,7 @@ export function computeProps( page.createdDateSource = "item.created"; page.updatedDate = new Date(model.item.modified); page.updatedDateSource = "item.modified"; - page.isDiscussable = isDiscussable(page); + // page.isDiscussable = isDiscussable(page); // // Handle capabilities // page.capabilities = processEntityCapabilities( diff --git a/packages/common/src/pages/defaults.ts b/packages/common/src/pages/defaults.ts new file mode 100644 index 00000000000..59cdb2c7bec --- /dev/null +++ b/packages/common/src/pages/defaults.ts @@ -0,0 +1,48 @@ +import { IHubPage } from "../core"; +import { IModel } from "../types"; + +export const HUB_PAGE_ITEM_TYPE = "Hub Page"; +export const ENTERPRISE_PAGE_ITEM_TYPE = "Site Page"; +export const PAGE_TYPE_KEYWORD = "hubPage"; +/** + * Default values of a IHubPage + */ +export const DEFAULT_PAGE: Partial = { + name: "No title provided", + permissions: [], + schemaVersion: 1, + tags: [], + typeKeywords: [PAGE_TYPE_KEYWORD], + view: { + contacts: [], + featuredContentIds: [], + showMap: true, + }, +}; + +/** + * Default values for a new HubPage Model + */ +export const DEFAULT_PAGE_MODEL: IModel = { + item: { + type: HUB_PAGE_ITEM_TYPE, + title: "No Title Provided", + description: "", + snippet: "", + tags: [], + typeKeywords: [PAGE_TYPE_KEYWORD], + properties: { + slug: "", + schemaVersion: 1, + }, + }, + data: { + display: "about", + permissions: [], + view: { + contacts: [], + featuredContentIds: [], + showMap: true, + }, + }, +} as unknown as IModel; From 20c190028c1d42ac89e3690b4660c5917914a3ed Mon Sep 17 00:00:00 2001 From: Michael Juniper Date: Wed, 12 Jul 2023 15:08:05 -0600 Subject: [PATCH 4/6] feat: hubpage entity --- packages/common/e2e/context-manager.e2e.ts | 2 +- packages/common/e2e/hub-page.e2e.ts | 161 ++++++++++++ .../_internal/checkCapabilityAccess.ts | 4 +- .../src/capabilities/checkCapability.ts | 9 +- packages/common/src/core/HubItemEntity.ts | 2 +- packages/common/src/core/getTypeFromEntity.ts | 6 +- .../core/schemas/getEntityEditorSchemas.ts | 13 + packages/common/src/core/types/IHubPage.ts | 10 +- packages/common/src/core/updateHubEntity.ts | 6 + packages/common/src/pages/HubPage.ts | 40 ++- .../src/pages/_internal/PageBusinessRules.ts | 87 +++++++ .../common/src/pages/_internal/PageSchema.ts | 39 +++ .../src/pages/_internal/PageUiSchemaEdit.ts | 102 ++++++++ .../src/pages/_internal/computeProps.ts | 14 +- packages/common/src/pages/index.ts | 1 + .../src/permissions/types/Permission.ts | 2 + packages/common/test/pages/HubPage.test.ts | 232 +++--------------- packages/common/test/pages/HubPages.test.ts | 2 +- 18 files changed, 498 insertions(+), 234 deletions(-) create mode 100644 packages/common/e2e/hub-page.e2e.ts create mode 100644 packages/common/src/pages/_internal/PageSchema.ts create mode 100644 packages/common/src/pages/_internal/PageUiSchemaEdit.ts diff --git a/packages/common/e2e/context-manager.e2e.ts b/packages/common/e2e/context-manager.e2e.ts index 6a63244914a..c3f2066d6e9 100644 --- a/packages/common/e2e/context-manager.e2e.ts +++ b/packages/common/e2e/context-manager.e2e.ts @@ -7,7 +7,7 @@ jasmine.DEFAULT_TIMEOUT_INTERVAL = 200000; function delay(milliseconds: number) { return new Promise((resolve) => setTimeout(resolve, milliseconds)); } -fdescribe("context-manager:", () => { +describe("context-manager:", () => { let factory: Artifactory; beforeAll(() => { jasmine.DEFAULT_TIMEOUT_INTERVAL = 200000; diff --git a/packages/common/e2e/hub-page.e2e.ts b/packages/common/e2e/hub-page.e2e.ts new file mode 100644 index 00000000000..2aa7c219cb8 --- /dev/null +++ b/packages/common/e2e/hub-page.e2e.ts @@ -0,0 +1,161 @@ +import Artifactory from "./helpers/Artifactory"; +import config from "./helpers/config"; +import { HubPage, IHubPage } from "../src"; + +jasmine.DEFAULT_TIMEOUT_INTERVAL = 200000; + +function delay(milliseconds: number) { + return new Promise((resolve) => setTimeout(resolve, milliseconds)); +} + +describe("HubPage Class", () => { + let factory: Artifactory; + beforeAll(() => { + jasmine.DEFAULT_TIMEOUT_INTERVAL = 200000; + factory = new Artifactory(config); + }); + it("can set thumbnail on a page", async () => { + // create context + const ctxMgr = await factory.getContextManager("hubBasic", "admin"); + // create a page + const newPage: Partial = { + name: "E2E Test Page", + summary: "This is the summary. Delete me", + }; + const page = await HubPage.create(newPage, ctxMgr.context); + + const imgSrc = `http://${window.location.host}/base/e2e/test-images/test-thumbnail.jpg`; + const tnImage = await fetchImage(imgSrc); + page.setThumbnail(tnImage, "test-thumbnail.jpg"); + await page.save(); + // + const chk = page.getThumbnailUrl(800); + expect(chk).toContain("test-thumbnail.jpg"); + expect(chk).toContain("w=800"); + // refetch the page + const p2 = await HubPage.fetch(page.id, ctxMgr.context); + expect(p2.getThumbnailUrl(800)).toEqual(chk); + await page.delete(); + }); + it("crud page", async () => { + // create context + const ctxMgr = await factory.getContextManager("hubBasic", "admin"); + // create a page + const newPage: Partial = { + name: "E2E Test Page", + summary: "This is the summary. Delete me", + }; + const page = await HubPage.create(newPage, ctxMgr.context); + // at this point we have a HubPage instance, but it is not yet saved + + // Set some more props on the page via the Json + page.update({ + name: "Oak Street Plaza", + tags: ["tag1", "tag2"], + }); + // save it, which actually creates the item and + // updates the internal page object + await page.save(); + // verify some server set props are set + const pojo = page.toJson(); + expect(pojo.owner).toBe(ctxMgr.context.currentUser.username || ""); + expect(pojo.createdDate).toBeDefined(); + + // const groups = ctxMgr.context.currentUser.groups || []; + // const group = groups[0]; + // if (group) { + // // add the page to the page + // page.addPermissionPolicy({ + // permission: "hub:page:edit", + // collaborationType: "group", + // collaborationId: group.id, + // }); + + // // verify that it works + // const canEditpage = page.checkPermission("hub:page:edit"); + // // current user is hub basic and edit page requires premium + // expect(canEditpage.access).toBe(false); + + // // save page and verify that the permission is there + // await page.save(); + + // const json = page.toJson(); + // expect(json.permissions).toBeDefined(); + // const p = json.permissions || []; + // expect(p[0].collaborationId).toBe(group.id); + // } + + // change something else and save it again + page.update({ summary: "This is the new summary" }); + await page.save(); + // verify the change + expect(page.toJson().summary).toBe("This is the new summary"); + + // Get the page by id, from Hub + const pageById = await HubPage.fetch(page.toJson().id, ctxMgr.context); + expect(pageById.toJson().id).toBe(page.toJson().id); + // ensure differnet instance + expect(pageById).not.toBe(page); + const id = pageById.toJson().id; + + // delete page via Hub + await page.delete(); + + // try to get it again - should fail + try { + await HubPage.fetch(id, ctxMgr.context); + } catch (ex) { + expect((ex as any).message).toBe("Page not found."); + } + }); + it("ensure unique slug", async () => { + // create context + const ctxMgr = await factory.getContextManager("hubBasic", "admin"); + + // create a page + const orgUrlKey = ctxMgr.context.portal.urlKey; + const treesPage = await HubPage.create( + { + name: "Trees Page", + summary: "This is the summary. Delete me", + slug: `${orgUrlKey}|trees`, + }, + ctxMgr.context + ); + + await treesPage.save(); + + // Add a wait here b/c the item is not indexed yet so the slug check on second create + // will return incorrect results + await delay(2000); + + const oakTreesPage = await HubPage.create( + { + name: "Oak Trees Page", + summary: "slug for this shoujld be trees-1", + slug: `${orgUrlKey}|trees`, + }, + ctxMgr.context + ); + await oakTreesPage.save(); + const json = oakTreesPage.toJson(); + expect(json.slug).toBe(`${orgUrlKey}|trees-1`); + + await treesPage.delete(); + await oakTreesPage.delete(); + + // expect page to throw if we try to save after destroy + try { + await treesPage.save(); + } catch (ex) { + expect((ex as any).message).toBe("HubPage is already destroyed."); + } + }); +}); + +// Quick and dirty fetch image fn +function fetchImage(url: string): Promise { + return fetch(url).then((response) => { + return response.blob(); + }); +} diff --git a/packages/common/src/capabilities/_internal/checkCapabilityAccess.ts b/packages/common/src/capabilities/_internal/checkCapabilityAccess.ts index 97a3fb9bc18..2b8eebef642 100644 --- a/packages/common/src/capabilities/_internal/checkCapabilityAccess.ts +++ b/packages/common/src/capabilities/_internal/checkCapabilityAccess.ts @@ -1,5 +1,5 @@ import { IArcGISContext } from "../../ArcGISContext"; -import { HubEntity } from "../../core"; +import { HubEntity, IHubItemEntity } from "../../core"; import { checkPermission, IPermissionAccessResponse } from "../../permissions"; import { ICapabilityAccessResponse, ICapabilityPermission } from "../types"; @@ -13,7 +13,7 @@ import { ICapabilityAccessResponse, ICapabilityPermission } from "../types"; export function checkCapabilityAccess( rule: ICapabilityPermission, context: IArcGISContext, - entity: HubEntity + entity: IHubItemEntity | HubEntity ): ICapabilityAccessResponse { // check if the capability is disabled for the entity; we default to false const value = entity.capabilities[rule.capability] || false; diff --git a/packages/common/src/capabilities/checkCapability.ts b/packages/common/src/capabilities/checkCapability.ts index 68b3db388a1..9f45946602a 100644 --- a/packages/common/src/capabilities/checkCapability.ts +++ b/packages/common/src/capabilities/checkCapability.ts @@ -1,7 +1,12 @@ import { Capability, ICapabilityAccessResponse, isCapability } from "./types"; import { CapabilityPermissions } from "./getWorkspaceCapabilities"; import { checkCapabilityAccess } from "./_internal"; -import { IArcGISContext, HubEntity, HubEntityType } from "../index"; +import { + IArcGISContext, + HubEntity, + IHubItemEntity, + HubEntityType, +} from "../index"; // Any of these causes checkCapability to not be defined in tests // import { getTypeFromEntity } from "../index"; // import { getTypeFromEntity } from "../core"; @@ -19,7 +24,7 @@ import { getTypeFromEntity } from "../core/getTypeFromEntity"; export function checkCapability( capability: Capability, context: IArcGISContext, - entity: HubEntity + entity: IHubItemEntity | HubEntity ): ICapabilityAccessResponse { const entityType: HubEntityType = getTypeFromEntity(entity); // Find the rule for the given entity type and capability diff --git a/packages/common/src/core/HubItemEntity.ts b/packages/common/src/core/HubItemEntity.ts index d830e967daa..2acd6c4502b 100644 --- a/packages/common/src/core/HubItemEntity.ts +++ b/packages/common/src/core/HubItemEntity.ts @@ -34,7 +34,7 @@ import { } from "./behaviors"; import { IWithThumbnailBehavior } from "./behaviors/IWithThumbnailBehavior"; -import { IHubItemEntity, SettableAccessLevel } from "./types"; +import { HubEntity, IHubItemEntity, SettableAccessLevel } from "./types"; import { sharedWith } from "./_internal/sharedWith"; import { IWithDiscussionsBehavior } from "./behaviors/IWithDiscussionsBehavior"; import { setDiscussableKeyword } from "../discussions"; diff --git a/packages/common/src/core/getTypeFromEntity.ts b/packages/common/src/core/getTypeFromEntity.ts index 612f9ac6694..bb0396fe978 100644 --- a/packages/common/src/core/getTypeFromEntity.ts +++ b/packages/common/src/core/getTypeFromEntity.ts @@ -2,14 +2,16 @@ // import { HubEntityType } from "./types/HubEntityType"; import { getFamily } from "../content/get-family"; -import { HubEntity, HubEntityType } from "./types"; +import { HubEntity, IHubItemEntity, HubEntityType } from "./types"; /** * Given a HubEntity, return it's HubEntityType * @param entity * @returns */ -export function getTypeFromEntity(entity: HubEntity): HubEntityType { +export function getTypeFromEntity( + entity: IHubItemEntity | HubEntity +): HubEntityType { let type: HubEntityType; switch (entity.type) { case "Hub Site Application": diff --git a/packages/common/src/core/schemas/getEntityEditorSchemas.ts b/packages/common/src/core/schemas/getEntityEditorSchemas.ts index 7fbe6968c18..27172bb27de 100644 --- a/packages/common/src/core/schemas/getEntityEditorSchemas.ts +++ b/packages/common/src/core/schemas/getEntityEditorSchemas.ts @@ -20,6 +20,10 @@ import { DiscussionEditorType, DiscussionEditorTypes, } from "../../discussions/_internal/DiscussionSchema"; +import { + PageEditorType, + PageEditorTypes, +} from "../../pages/_internal/PageSchema"; /** * defines the possible editor type values - these correspond @@ -31,6 +35,7 @@ export const validEditorTypes = [ ...InitiativeEditorTypes, ...SiteEditorTypes, ...DiscussionEditorTypes, + ...PageEditorTypes, ] as const; /** @@ -100,6 +105,14 @@ export const getEntityEditorSchemas = async ( import("../../discussions/_internal/DiscussionUiSchemaCreate"), }[type as DiscussionEditorType]()); break; + case "page": + const { PageSchema } = await import("../../pages/_internal/PageSchema"); + schema = cloneObject(PageSchema); + + ({ uiSchema } = await { + "hub:page:edit": () => import("../../pages/_internal/PageUiSchemaEdit"), + }[type as PageEditorType]()); + break; } // filter out properties not used in the UI schema diff --git a/packages/common/src/core/types/IHubPage.ts b/packages/common/src/core/types/IHubPage.ts index 66b5de8c9d2..73f2768d19e 100644 --- a/packages/common/src/core/types/IHubPage.ts +++ b/packages/common/src/core/types/IHubPage.ts @@ -1,10 +1,5 @@ import { IHubItemEntity } from "./IHubItemEntity"; -// import { IWithVersioningBehavior } from "../behaviors"; -import { - IWithLayout, - // IWithPermissions, - IWithSlug, -} from "../traits/index"; +import { IWithLayout, IWithPermissions, IWithSlug } from "../traits"; /** * DRAFT: Under development and more properties will likely be added * @internal @@ -12,6 +7,5 @@ import { export interface IHubPage extends IHubItemEntity, IWithLayout, - // IWithPermissions, - // IWithVersioningBehavior + IWithPermissions, IWithSlug {} diff --git a/packages/common/src/core/updateHubEntity.ts b/packages/common/src/core/updateHubEntity.ts index 209d94a128b..bf490b5a6b4 100644 --- a/packages/common/src/core/updateHubEntity.ts +++ b/packages/common/src/core/updateHubEntity.ts @@ -4,6 +4,7 @@ import { updateDiscussion } from "../discussions/edit"; import { updateInitiative } from "../initiatives/HubInitiatives"; import { updateProject } from "../projects/edit"; import { updateSite } from "../sites/HubSites"; +import { updatePage } from "../pages/HubPages"; import { HubEntity, HubEntityType, @@ -12,6 +13,7 @@ import { IHubInitiative, IHubProject, IHubSite, + IHubPage, } from "./types"; /** @@ -55,6 +57,10 @@ export const updateHubEntity = async ( entity as IHubEditableContent, context.userRequestOptions ); + break; + case "page": + result = await updatePage(entity as IHubPage, context.userRequestOptions); + break; } return result; }; diff --git a/packages/common/src/pages/HubPage.ts b/packages/common/src/pages/HubPage.ts index 6c5d09f8ce3..351315e8012 100644 --- a/packages/common/src/pages/HubPage.ts +++ b/packages/common/src/pages/HubPage.ts @@ -1,30 +1,36 @@ -import { IHubPage, IWithStoreBehavior, IWithSharingBehavior } from "../core"; - -import { IArcGISContext } from "../ArcGISContext"; -import { HubItemEntity } from "../core/HubItemEntity"; - import { DEFAULT_PAGE, ENTERPRISE_PAGE_ITEM_TYPE, HUB_PAGE_ITEM_TYPE, } from "./defaults"; +import { + IHubPage, + IWithStoreBehavior, + IWithSharingBehavior, + UiSchemaElementOptions, +} from "../core"; + +import { getEntityEditorSchemas } from "../core/schemas/getEntityEditorSchemas"; +import { IArcGISContext } from "../ArcGISContext"; +import { HubItemEntity } from "../core/HubItemEntity"; +import { IEditorConfig } from "../core/behaviors/IWithEditorBehavior"; + import { createPage, deletePage, fetchPage, updatePage } from "./HubPages"; +import { PageEditorType } from "./_internal/PageSchema"; + /* TODO: - - slug stuff - - default stuff - - what about page-site lingage? - - permissions? - - capabilities? - - need to look at what is being done in the old HubSites package - - marvin does not have access to class diagram + - when creating a site, we currently do some stuff we probably don't want to do anymore: + - protect the item + - allow for uploading assets - i think this is not used + - sharing to the collaboration group if it exists */ /** * Hub Page Class - * NOTE: This is a minimal implementation. Create operations are not supported at this time + * NOTE: This is a minimal implementation. */ export class HubPage extends HubItemEntity @@ -100,6 +106,14 @@ export class HubPage } } + static async getEditorConfig( + i18nScope: string, + type: PageEditorType, + options: UiSchemaElementOptions[] = [] + ): Promise { + return getEntityEditorSchemas(i18nScope, type, options); + } + private static applyDefaults( partialPage: Partial, context: IArcGISContext diff --git a/packages/common/src/pages/_internal/PageBusinessRules.ts b/packages/common/src/pages/_internal/PageBusinessRules.ts index 296046d6cbe..11aaf9ce30e 100644 --- a/packages/common/src/pages/_internal/PageBusinessRules.ts +++ b/packages/common/src/pages/_internal/PageBusinessRules.ts @@ -1,3 +1,90 @@ +import { EntityCapabilities, ICapabilityPermission } from "../../capabilities"; +import { IPermissionPolicy } from "../../permissions"; + +/** + * Default capabilities for a Page. If not listed here, the capability will not be available + * This hash is combined with the capabilities hash stored in the item data. Regardless of what + * properties are defined in the item data, only the capabilities defined here will be available + * @private + */ +export const PageDefaultCapabilities: EntityCapabilities = { + overview: true, + details: true, + settings: true, +}; + +/** + * List of all the Page Capability Permissions + * These are considered Hub Business Rules and are not intended + * to be modified by consumers + * @private + */ +export const PageCapabilityPermissions: ICapabilityPermission[] = [ + { + entity: "page", + capability: "overview", + permissions: ["hub:page:view"], + }, + { + entity: "page", + capability: "details", + permissions: ["hub:page:edit"], + }, + { + entity: "page", + capability: "settings", + permissions: ["hub:page:edit"], + }, +]; + +/** + * Page Permission Policies + * These define the requirements any user must meet to perform related actions + * @private + */ +export const PagePermissions = [ + "hub:page:create", + "hub:page:delete", + "hub:page:edit", + "hub:page:view", +] as const; + +/** + * Page permission policies + * @private + */ +export const PagePermissionPolicies: IPermissionPolicy[] = [ + { + permission: "hub:page:create", + subsystems: ["pages", "sites"], + authenticated: true, + privileges: ["portal:user:createItem"], + licenses: ["hub-basic", "hub-premium", "enterprise-sites"], + }, + { + permission: "hub:page:view", + subsystems: ["pages", "sites"], + authenticated: false, + licenses: ["hub-basic", "hub-premium", "enterprise-sites"], + }, + { + permission: "hub:page:edit", + authenticated: true, + subsystems: ["pages", "sites"], + entityEdit: true, + // privileges: ["portal:admin:updateItems"], // maybe this too? + licenses: ["hub-basic", "hub-premium", "enterprise-sites"], + }, + { + permission: "hub:page:delete", + authenticated: true, + subsystems: ["pages", "sites"], + entityOwner: true, + // privileges: ["portal:admin:deleteItems"], // maybe this too? + licenses: ["hub-basic", "hub-premium", "enterprise-sites"], + }, +]; + /** * Page versioning include list */ diff --git a/packages/common/src/pages/_internal/PageSchema.ts b/packages/common/src/pages/_internal/PageSchema.ts new file mode 100644 index 00000000000..a5d1cb5a5b2 --- /dev/null +++ b/packages/common/src/pages/_internal/PageSchema.ts @@ -0,0 +1,39 @@ +import { IConfigurationSchema } from "../../core"; +import { + ENTITY_ACCESS_SCHEMA, + ENTITY_CATEGORIES_SCHEMA, + ENTITY_NAME_SCHEMA, + ENTITY_TAGS_SCHEMA, +} from "../../core/schemas/shared"; + +export const PageEditorTypes = ["hub:page:edit"] as const; +export type PageEditorType = (typeof PageEditorTypes)[number]; + +/** + * defines the JSON schema for a Hub Site's editable fields + */ +export const PageSchema: IConfigurationSchema = { + required: ["name"], + type: "object", + properties: { + name: ENTITY_NAME_SCHEMA, + summary: { + type: "string", + }, + description: { + type: "string", + }, + access: ENTITY_ACCESS_SCHEMA, + groups: { + type: "array", + items: { + type: "string", + }, + }, + location: { + type: "object", + }, + tags: ENTITY_TAGS_SCHEMA, + categories: ENTITY_CATEGORIES_SCHEMA, + }, +} as unknown as IConfigurationSchema; diff --git a/packages/common/src/pages/_internal/PageUiSchemaEdit.ts b/packages/common/src/pages/_internal/PageUiSchemaEdit.ts new file mode 100644 index 00000000000..7baceeac524 --- /dev/null +++ b/packages/common/src/pages/_internal/PageUiSchemaEdit.ts @@ -0,0 +1,102 @@ +import { IUiSchema } from "../../core"; + +/** + * complete edit uiSchema for Hub Projects - this defines + * how the schema properties should be rendered in the + * project editing experience + */ +export const uiSchema: IUiSchema = { + type: "Layout", + elements: [ + { + type: "Section", + labelKey: "{{i18nScope}}.sections.basicInfo.label", + elements: [ + { + labelKey: "{{i18nScope}}.fields.name.label", + scope: "/properties/name", + type: "Control", + options: { + messages: [ + { + type: "ERROR", + keyword: "required", + icon: true, + labelKey: "{{i18nScope}}.fields.name.requiredError", + }, + ], + }, + }, + { + labelKey: "{{i18nScope}}.fields.summary.label", + scope: "/properties/summary", + type: "Control", + options: { + control: "hub-field-input-input", + type: "textarea", + helperText: { + labelKey: "{{i18nScope}}.fields.summary.helperText", + }, + }, + }, + { + labelKey: "{{i18nScope}}.fields.description.label", + scope: "/properties/description", + type: "Control", + options: { + control: "hub-field-input-input", + type: "textarea", + helperText: { + labelKey: "{{i18nScope}}.fields.description.helperText", + }, + }, + }, + + { + labelKey: "{{i18nScope}}.fields.tags.label", + scope: "/properties/tags", + type: "Control", + options: { + control: "hub-field-input-combobox", + allowCustomValues: true, + selectionMode: "multiple", + placeholderIcon: "label", + helperText: { labelKey: "{{i18nScope}}.fields.tags.helperText" }, + }, + }, + { + labelKey: "{{i18nScope}}.fields.categories.label", + scope: "/properties/categories", + type: "Control", + options: { + control: "hub-field-input-combobox", + allowCustomValues: false, + selectionMode: "multiple", + placeholderIcon: "select-category", + helperText: { + labelKey: "{{i18nScope}}.fields.categories.helperText", + }, + }, + }, + ], + }, + { + type: "Section", + labelKey: "{{i18nScope}}.sections.location.label", + options: { + helperText: { + labelKey: "{{i18nScope}}.sections.location.helperText", + }, + }, + elements: [ + { + scope: "/properties/location", + type: "Control", + options: { + control: "hub-field-input-location-picker", + }, + }, + ], + }, + ], +}; diff --git a/packages/common/src/pages/_internal/computeProps.ts b/packages/common/src/pages/_internal/computeProps.ts index 1394cc4f95b..1dfc0790dca 100644 --- a/packages/common/src/pages/_internal/computeProps.ts +++ b/packages/common/src/pages/_internal/computeProps.ts @@ -3,9 +3,8 @@ import { UserSession } from "@esri/arcgis-rest-auth"; import { getItemThumbnailUrl } from "../../resources"; import { IHubPage } from "../../core"; import { IModel } from "../../types"; -// import { PageDefaultCapabilities } from "./ProjectBusinessRules"; -// import { processEntityCapabilities } from "../../capabilities"; -// import { isDiscussable } from "../../discussions"; +import { PageDefaultCapabilities } from "./PageBusinessRules"; +import { processEntityCapabilities } from "../../capabilities"; /** * Given a model and a page, set various computed properties that can't be directly mapped @@ -33,13 +32,12 @@ export function computeProps( page.createdDateSource = "item.created"; page.updatedDate = new Date(model.item.modified); page.updatedDateSource = "item.modified"; - // page.isDiscussable = isDiscussable(page); // // Handle capabilities - // page.capabilities = processEntityCapabilities( - // model.data.settings?.capabilities || {}, - // PageDefaultCapabilities - // ); + page.capabilities = processEntityCapabilities( + model.data.settings?.capabilities || {}, + PageDefaultCapabilities + ); // cast b/c this takes a partial but returns a full page return page as IHubPage; diff --git a/packages/common/src/pages/index.ts b/packages/common/src/pages/index.ts index 8a6c43fcf65..e47ddae1594 100644 --- a/packages/common/src/pages/index.ts +++ b/packages/common/src/pages/index.ts @@ -1 +1,2 @@ export * from "./HubPages"; +export * from "./HubPage"; diff --git a/packages/common/src/permissions/types/Permission.ts b/packages/common/src/permissions/types/Permission.ts index b5e48026934..e026fb4cf8b 100644 --- a/packages/common/src/permissions/types/Permission.ts +++ b/packages/common/src/permissions/types/Permission.ts @@ -3,6 +3,7 @@ import { SitePermissions } from "../../sites/_internal/SiteBusinessRules"; import { InitiativePermissions } from "../../initiatives/_internal/InitiativeBusinessRules"; import { DiscussionPermissions } from "../../discussions/_internal/DiscussionBusinessRules"; import { ContentPermissions } from "../../content/_internal/ContentBusinessRules"; +import { PagePermissions } from "../../pages/_internal/PageBusinessRules"; /** * Defines the values for Permissions @@ -15,6 +16,7 @@ const validPermissions = [ ...InitiativePermissions, ...DiscussionPermissions, ...ContentPermissions, + ...PagePermissions, ] as const; /** diff --git a/packages/common/test/pages/HubPage.test.ts b/packages/common/test/pages/HubPage.test.ts index a87e91fe911..f874e666408 100644 --- a/packages/common/test/pages/HubPage.test.ts +++ b/packages/common/test/pages/HubPage.test.ts @@ -3,13 +3,8 @@ import { ArcGISContextManager } from "../../src/ArcGISContextManager"; import { HubPage } from "../../src/pages/HubPage"; import { MOCK_AUTH } from "../mocks/mock-auth"; import * as HubPagesModule from "../../src/pages/HubPages"; -import { - // IDeepCatalogInfo, - // IHubCatalog, - IHubPage, -} from "../../src"; -// import { Catalog } from "../../src/search"; -import * as ContainsModule from "../../src/core/_internal/deepContains"; +import { IHubPage } from "../../src"; + describe("HubPage Class:", () => { let authdCtxMgr: ArcGISContextManager; let portalCtxMgr: ArcGISContextManager; @@ -37,7 +32,7 @@ describe("HubPage Class:", () => { username: "casey", } as unknown as PortalModule.IUser, portal: { - isPortal: true, + isPortal: false, name: "My Portal Install", id: "BRXFAKE", urlKey: "fake-org", @@ -56,7 +51,6 @@ describe("HubPage Class:", () => { // adds empty permissions and catalog const json = chk.toJson(); expect(json.permissions).toEqual([]); - // expect(json.catalog).toEqual({ schemaVersion: 0 }); }); it("loads based on identifier", async () => { const fetchSpy = spyOn(HubPagesModule, "fetchPage").and.callFake( @@ -74,7 +68,7 @@ describe("HubPage Class:", () => { expect(chk.toJson().name).toBe("Test Page"); }); - it("throws if site not found", async () => { + it("throws if page not found", async () => { const fetchSpy = spyOn(HubPagesModule, "fetchPage").and.callFake( (id: string) => { const err = new Error( @@ -87,7 +81,7 @@ describe("HubPage Class:", () => { await HubPage.fetch("3ef", authdCtxMgr.context); } catch (ex) { expect(fetchSpy).toHaveBeenCalledTimes(1); - expect(ex.message).toBe("Page not found."); + expect((ex as any).message).toBe("Page not found."); } }); @@ -102,7 +96,7 @@ describe("HubPage Class:", () => { await HubPage.fetch("3ef", authdCtxMgr.context); } catch (ex) { expect(fetchSpy).toHaveBeenCalledTimes(1); - expect(ex.message).toBe("ZOMG!"); + expect((ex as any).message).toBe("ZOMG!"); } }); }); @@ -137,7 +131,7 @@ describe("HubPage Class:", () => { expect(createSpy).toHaveBeenCalledTimes(1); expect(chk.toJson().name).toEqual("Test Page"); - expect(chk.toJson().type).toEqual("Hub Site Application"); + expect(chk.toJson().type).toEqual("Hub Page"); }); it("create does not save by default", async () => { const createSpy = spyOn(HubPagesModule, "createPage"); @@ -148,49 +142,36 @@ describe("HubPage Class:", () => { expect(createSpy).not.toHaveBeenCalled(); expect(chk.toJson().name).toEqual("Test Page"); - expect(chk.toJson().type).toEqual("Site Application"); + expect(chk.toJson().type).toEqual("Hub Page"); }); - // it("update applies partial chagnes to internal state", () => { - // const chk = HubPage.fromJson( - // { name: "Test Page", catalog: { schemaVersion: 0 } }, - // authdCtxMgr.context - // ); - // chk.update({ - // name: "Test Site 2", - // permissions: [ - // { - // permission: "hub:project:create", - // collaborationType: "group", - // collaborationId: "3ef", - // }, - // ], - // catalog: { schemaVersion: 2 }, - // }); - // expect(chk.toJson().name).toEqual("Test Site 2"); - // expect(chk.toJson().catalog).toEqual({ schemaVersion: 2 }); + it("update applies partial chagnes to internal state", () => { + const chk = HubPage.fromJson({ name: "Test Page" }, authdCtxMgr.context); + chk.update({ + name: "Test Page 2", + }); + expect(chk.toJson().name).toEqual("Test Page 2"); - // chk.update({ tags: ["one", "two"] }); - // expect(chk.toJson().tags).toEqual(["one", "two"]); - // }); + chk.update({ tags: ["one", "two"] }); + expect(chk.toJson().tags).toEqual(["one", "two"]); + }); - // it("save updates if object has id", async () => { - // const updateSpy = spyOn(HubPagesModule, "updatePage").and.callFake( - // (p: IHubPage) => { - // return Promise.resolve(p); - // } - // ); - // const chk = HubPage.fromJson( - // { - // id: "bc3", - // name: "Test Page", - // catalog: { schemaVersion: 0 }, - // }, - // authdCtxMgr.context - // ); - // await chk.save(); - // expect(updateSpy).toHaveBeenCalledTimes(1); - // }); + it("save updates if object has id", async () => { + const updateSpy = spyOn(HubPagesModule, "updatePage").and.callFake( + (p: IHubPage) => { + return Promise.resolve(p); + } + ); + const chk = HubPage.fromJson( + { + id: "bc3", + name: "Test Page", + }, + authdCtxMgr.context + ); + await chk.save(); + expect(updateSpy).toHaveBeenCalledTimes(1); + }); it("delete", async () => { const deleteSpy = spyOn(HubPagesModule, "deletePage").and.callFake(() => { @@ -205,161 +186,20 @@ describe("HubPage Class:", () => { }).toThrowError("Entity is already destroyed."); expect(() => { - chk.update({ name: "Test Site 2" } as IHubPage); + chk.update({ name: "Test Page 2" } as IHubPage); }).toThrowError("HubPage is already destroyed."); // async calls try { await chk.delete(); } catch (e) { - expect(e.message).toEqual("HubPage is already destroyed."); + expect((e as any).message).toEqual("HubPage is already destroyed."); } try { await chk.save(); } catch (e) { - expect(e.message).toEqual("HubPage is already destroyed."); + expect((e as any).message).toEqual("HubPage is already destroyed."); } }); - - // it("internal instance accessors", () => { - // const chk = HubPage.fromJson( - // { name: "Test Page", catalog: { schemaVersion: 0 } }, - // authdCtxMgr.context - // ); - - // expect(chk.catalog instanceof Catalog).toBeTruthy(); - // }); - - // it("setting catalog updates catalog instance", () => { - // const chk = HubPage.fromJson( - // { name: "Test Page", catalog: { schemaVersion: 0 } }, - // authdCtxMgr.context - // ); - // chk.update({ catalog: { schemaVersion: 2 } }); - // expect(chk.toJson().catalog).toEqual({ schemaVersion: 2 }); - // expect(chk.catalog.schemaVersion).toEqual(2); - // }); - // describe(" contains:", () => { - // it("checks site catalog by default", async () => { - // const containsSpy = spyOn(ContainsModule, "deepContains").and.callFake( - // (id: string, h: IDeepCatalogInfo[]) => { - // return Promise.resolve({ - // identifier: id, - // isContained: true, - // catalogInfo: {}, - // }); - // } - // ); - // const chk = HubPage.fromJson( - // { - // id: "3ef", - // catalog: createCatalog("00a"), - // }, - // authdCtxMgr.context - // ); - // const result = await chk.contains("cc0"); - // expect(containsSpy).toHaveBeenCalledTimes(1); - // const hiearchy = containsSpy.calls.argsFor(0)[1]; - // expect(hiearchy.length).toBe(1); - // expect(hiearchy[0].catalog).toEqual( - // createCatalog("00a"), - // "should pass the site catalog" - // ); - // expect(result).toEqual({ - // identifier: "cc0", - // isContained: true, - // catalogInfo: {}, - // }); - // }); - - // it("adds site catalog to others", async () => { - // const containsSpy = spyOn(ContainsModule, "deepContains").and.callFake( - // (id: string, h: IDeepCatalogInfo[]) => { - // return Promise.resolve({ - // identifier: id, - // isContained: true, - // catalogInfo: {}, - // }); - // } - // ); - // const chk = HubPage.fromJson( - // { - // id: "3ef", - // catalog: createCatalog("00a"), - // }, - // authdCtxMgr.context - // ); - // // pass in a project catalog - // const result = await chk.contains("cc0", [ - // { id: "4ef", entityType: "item", catalog: createCatalog("00b") }, - // ]); - // expect(containsSpy).toHaveBeenCalledTimes(1); - // const hiearchy = containsSpy.calls.argsFor(0)[1]; - // expect(hiearchy.length).toBe(2); - // expect(hiearchy[0].catalog).toEqual( - // createCatalog("00b"), - // "should pass the project catalog" - // ); - // expect(hiearchy[1].catalog).toEqual( - // createCatalog("00a"), - // "should pass the site catalog" - // ); - // expect(result).toEqual({ - // identifier: "cc0", - // isContained: true, - // catalogInfo: {}, - // }); - // }); - - // it("caches catalogs", async () => { - // const containsSpy = spyOn(ContainsModule, "deepContains").and.callFake( - // (id: string, h: IDeepCatalogInfo[]) => { - // return Promise.resolve({ - // identifier: id, - // isContained: true, - // catalogInfo: { - // "3ef": { - // catalog: createCatalog("00a"), - // }, - // "4ef": { - // catalog: createCatalog("00b"), - // }, - // }, - // }); - // } - // ); - // const chk = HubPage.fromJson( - // { - // id: "3ef", - // catalog: createCatalog("00a"), - // }, - // authdCtxMgr.context - // ); - // // First call will warm the cache - // await chk.contains("cc0", [{ id: "4ef", entityType: "item" }]); - // // second call will use the cache - // await chk.contains("cc1", [{ id: "4ef", entityType: "item" }]); - // expect(containsSpy).toHaveBeenCalledTimes(2); - // // verify first call does not send the 4ef catalog - // const hiearchy = containsSpy.calls.argsFor(0)[1]; - // expect(hiearchy.length).toBe(2); - // expect(hiearchy[0].catalog).not.toBeDefined(); - // expect(hiearchy[1].catalog).toEqual( - // createCatalog("00a"), - // "should pass the site catalog" - // ); - // // verify second call does send the 4ef catalog - // const hiearchy2 = containsSpy.calls.argsFor(1)[1]; - // expect(hiearchy2.length).toBe(2); - // expect(hiearchy2[0].catalog).toEqual( - // createCatalog("00b"), - // "should pass the project catalog" - // ); - // expect(hiearchy2[1].catalog).toEqual( - // createCatalog("00a"), - // "should pass the site catalog" - // ); - // }); - // }); }); diff --git a/packages/common/test/pages/HubPages.test.ts b/packages/common/test/pages/HubPages.test.ts index 29af0fe8671..dc613e50428 100644 --- a/packages/common/test/pages/HubPages.test.ts +++ b/packages/common/test/pages/HubPages.test.ts @@ -65,7 +65,7 @@ const PAGE_ITEM: IItem = { contentOrigin: "other", }; -describe("HubPages Module", () => { +fdescribe("HubPages Module", () => { describe("enrichments:", () => { let enrichmentSpy: jasmine.Spy; let hubRo: IHubRequestOptions; From f4256c180daf0d9865a77398f58fb1079dc5ff9c Mon Sep 17 00:00:00 2001 From: Michael Juniper Date: Mon, 17 Jul 2023 14:12:22 -0600 Subject: [PATCH 5/6] feat: page entity --- packages/common/src/pages/defaults.ts | 8 +- .../common/test/core/fetchHubEntity.test.ts | 19 +- .../schemas/getEntityEditorSchema.test.ts | 2 + .../common/test/core/updateHubEntity.test.ts | 11 + packages/common/test/pages/HubPage.test.ts | 34 ++- packages/common/test/pages/HubPages.test.ts | 246 +++++++++++++++++- .../test/pages/_internal/computeProps.test.ts | 107 ++++++++ 7 files changed, 404 insertions(+), 23 deletions(-) create mode 100644 packages/common/test/pages/_internal/computeProps.test.ts diff --git a/packages/common/src/pages/defaults.ts b/packages/common/src/pages/defaults.ts index 59cdb2c7bec..3fcfa2e2084 100644 --- a/packages/common/src/pages/defaults.ts +++ b/packages/common/src/pages/defaults.ts @@ -37,12 +37,8 @@ export const DEFAULT_PAGE_MODEL: IModel = { }, }, data: { - display: "about", - permissions: [], - view: { - contacts: [], - featuredContentIds: [], - showMap: true, + values: { + layout: {}, }, }, } as unknown as IModel; diff --git a/packages/common/test/core/fetchHubEntity.test.ts b/packages/common/test/core/fetchHubEntity.test.ts index 45388b39bf8..4982db95429 100644 --- a/packages/common/test/core/fetchHubEntity.test.ts +++ b/packages/common/test/core/fetchHubEntity.test.ts @@ -1,16 +1,8 @@ import { IArcGISContext } from "../../src/ArcGISContext"; import { fetchHubEntity } from "../../src/core/fetchHubEntity"; import { HubEntityType } from "../../src/core/types/HubEntityType"; -import { getProp } from "../../src/objects/get-prop"; describe("fetchHubEntity:", () => { - it("throws for page", async () => { - try { - await fetchHubEntity("page", "123", {} as any); - } catch (e) { - expect(getProp(e, "message")).toBe("FetchPage not implemented"); - } - }); it("returns undefined for non-hub types", async () => { expect( await fetchHubEntity("foo" as HubEntityType, "123", {} as any) @@ -71,4 +63,15 @@ describe("fetchHubEntity:", () => { await fetchHubEntity("content", "123", ctx); expect(spy).toHaveBeenCalledWith("123", "fakeRequestOptions"); }); + it("fetches page", async () => { + const ctx = { + hubRequestOptions: "fakeRequestOptions", + } as unknown as IArcGISContext; + const spy = spyOn( + require("../../src/pages/HubPages"), + "fetchPage" + ).and.returnValue(Promise.resolve({})); + await fetchHubEntity("page", "123", ctx); + expect(spy).toHaveBeenCalledWith("123", "fakeRequestOptions"); + }); }); diff --git a/packages/common/test/core/schemas/getEntityEditorSchema.test.ts b/packages/common/test/core/schemas/getEntityEditorSchema.test.ts index 97c9d220ac7..9847ba3124d 100644 --- a/packages/common/test/core/schemas/getEntityEditorSchema.test.ts +++ b/packages/common/test/core/schemas/getEntityEditorSchema.test.ts @@ -3,6 +3,7 @@ import { ProjectEditorTypes } from "../../../src/projects/_internal/ProjectSchem import { InitiativeEditorTypes } from "../../../src/initiatives/_internal/InitiativeSchema"; import { SiteEditorTypes } from "../../../src/sites/_internal/SiteSchema"; import { DiscussionEditorTypes } from "../../../src/discussions/_internal/DiscussionSchema"; +import { PageEditorTypes } from "../../../src/pages/_internal/PageSchema"; import * as applyOptionsModule from "../../../src/core/schemas/internal/applyUiSchemaElementOptions"; import * as filterSchemaModule from "../../../src/core/schemas/internal/filterSchemaToUiSchema"; import * as itemsModule from "../../../src/items"; @@ -14,6 +15,7 @@ describe("getEntityEditorSchemas", () => { ...InitiativeEditorTypes, ...SiteEditorTypes, ...DiscussionEditorTypes, + ...PageEditorTypes, ].forEach(async (type, idx) => { const { schema, uiSchema } = await getEntityEditorSchemas( "some.scope", diff --git a/packages/common/test/core/updateHubEntity.test.ts b/packages/common/test/core/updateHubEntity.test.ts index f06bfc6c36f..d130680d532 100644 --- a/packages/common/test/core/updateHubEntity.test.ts +++ b/packages/common/test/core/updateHubEntity.test.ts @@ -66,4 +66,15 @@ describe("updateHubEntity:", () => { await updateHubEntity("content", {} as HubEntity, ctx); expect(spy).toHaveBeenCalledWith({}, "fakeRequestOptions"); }); + it("updates page", async () => { + const ctx = { + userRequestOptions: "fakeRequestOptions", + } as unknown as IArcGISContext; + const spy = spyOn( + require("../../src/pages/HubPages"), + "updatePage" + ).and.returnValue(Promise.resolve({})); + await updateHubEntity("page", {} as HubEntity, ctx); + expect(spy).toHaveBeenCalledWith({}, "fakeRequestOptions"); + }); }); diff --git a/packages/common/test/pages/HubPage.test.ts b/packages/common/test/pages/HubPage.test.ts index f874e666408..4cede35d1d2 100644 --- a/packages/common/test/pages/HubPage.test.ts +++ b/packages/common/test/pages/HubPage.test.ts @@ -3,7 +3,8 @@ import { ArcGISContextManager } from "../../src/ArcGISContextManager"; import { HubPage } from "../../src/pages/HubPage"; import { MOCK_AUTH } from "../mocks/mock-auth"; import * as HubPagesModule from "../../src/pages/HubPages"; -import { IHubPage } from "../../src"; +import { IHubPage, UiSchemaElementOptions } from "../../src"; +import * as schemasModule from "../../src/core/schemas/getEntityEditorSchemas"; describe("HubPage Class:", () => { let authdCtxMgr: ArcGISContextManager; @@ -32,7 +33,7 @@ describe("HubPage Class:", () => { username: "casey", } as unknown as PortalModule.IUser, portal: { - isPortal: false, + isPortal: true, name: "My Portal Install", id: "BRXFAKE", urlKey: "fake-org", @@ -99,6 +100,31 @@ describe("HubPage Class:", () => { expect((ex as any).message).toBe("ZOMG!"); } }); + it("returns editorConfig", async () => { + const spy = spyOn(schemasModule, "getEntityEditorSchemas").and.callFake( + () => { + return Promise.resolve({ schema: {}, uiSchema: {} }); + } + ); + + await HubPage.getEditorConfig("test.scope", "hub:page:edit"); + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith("test.scope", "hub:page:edit", []); + }); + + it("returns editorConfig integrating options", async () => { + const spy = spyOn(schemasModule, "getEntityEditorSchemas").and.callFake( + () => { + return Promise.resolve({ schema: {}, uiSchema: {} }); + } + ); + + const opts: UiSchemaElementOptions[] = []; + + await HubPage.getEditorConfig("test.scope", "hub:page:edit", opts); + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith("test.scope", "hub:page:edit", opts); + }); }); it("save calls createPage if object does not have an id", async () => { @@ -142,10 +168,10 @@ describe("HubPage Class:", () => { expect(createSpy).not.toHaveBeenCalled(); expect(chk.toJson().name).toEqual("Test Page"); - expect(chk.toJson().type).toEqual("Hub Page"); + expect(chk.toJson().type).toEqual("Site Page"); }); - it("update applies partial chagnes to internal state", () => { + it("update applies partial changes to internal state", () => { const chk = HubPage.fromJson({ name: "Test Page" }, authdCtxMgr.context); chk.update({ name: "Test Page 2", diff --git a/packages/common/test/pages/HubPages.test.ts b/packages/common/test/pages/HubPages.test.ts index dc613e50428..fe36722841d 100644 --- a/packages/common/test/pages/HubPages.test.ts +++ b/packages/common/test/pages/HubPages.test.ts @@ -1,16 +1,28 @@ +import * as portalModule from "@esri/arcgis-rest-portal"; import { IItem } from "@esri/arcgis-rest-portal"; - +import { MOCK_AUTH } from "../mocks/mock-auth"; +import * as modelUtils from "../../src/models"; +import * as slugUtils from "../../src/items/slugs"; +import { IRequestOptions } from "@esri/arcgis-rest-request"; import { cloneObject, enrichPageSearchResult, IHubRequestOptions, - IHubSearchResult, + IModel, } from "../../src"; +import { + createPage, + fetchPage, + deletePage, + updatePage, +} from "../../src/pages/HubPages"; +import { IHubPage } from "../../src/core/types/IHubPage"; import * as FetchEnrichments from "../../src/items/_enrichments"; +const GUID = "f995804e9e0e42cc84187258de0b710d"; const PAGE_ITEM: IItem = { - id: "f995804e9e0e42cc84187258de0b710d", - owner: "collabadmin7", + id: GUID, + owner: "vader", created: 1592615052000, modified: 1592615052000, guid: null, @@ -65,7 +77,231 @@ const PAGE_ITEM: IItem = { contentOrigin: "other", }; -fdescribe("HubPages Module", () => { +const PAGE_DATA = {}; + +const PAGE_MODEL = { + item: PAGE_ITEM, + data: PAGE_DATA, +} as IModel; + +describe("HubPages Module", () => { + describe("createPage:", () => { + it("works with very limited structure", async () => { + const slugSpy = spyOn(slugUtils, "getUniqueSlug").and.returnValue( + Promise.resolve("dcdev|hello-world") + ); + const createSpy = spyOn(modelUtils, "createModel").and.callFake( + (m: IModel) => { + const newModel = cloneObject(m); + newModel.item.id = GUID; + return Promise.resolve(newModel); + } + ); + const chk = await createPage( + { name: "Hello World", orgUrlKey: "dcdev" }, + { authentication: MOCK_AUTH } + ); + + expect(chk.id).toBe(GUID); + expect(chk.name).toBe("Hello World"); + // should ensure unique slug + expect(slugSpy.calls.count()).toBe(1); + expect(slugSpy.calls.argsFor(0)[0]).toEqual( + { slug: "dcdev|hello-world" }, + "should recieve slug" + ); + // should create the item + expect(createSpy.calls.count()).toBe(1); + const modelToCreate = createSpy.calls.argsFor(0)[0]; + expect(modelToCreate.item.title).toBe("Hello World"); + expect(modelToCreate.item.type).toBe("Hub Page"); + expect(modelToCreate.item.properties.slug).toBe("dcdev|hello-world"); + expect(modelToCreate.item.properties.orgUrlKey).toBe("dcdev"); + }); + it("works with more complete object", async () => { + // Note: this covers a branch when a slug is passed in + const slugSpy = spyOn(slugUtils, "getUniqueSlug").and.returnValue( + Promise.resolve("dcdev|hello-world") + ); + const createSpy = spyOn(modelUtils, "createModel").and.callFake( + (m: IModel) => { + const newModel = cloneObject(m); + newModel.item.id = GUID; + return Promise.resolve(newModel); + } + ); + const chk = await createPage( + { + name: "Hello World", + slug: "dcdev|hello-world", // important for coverage + description: "my desc", + orgUrlKey: "dcdev", + }, + { authentication: MOCK_AUTH } + ); + expect(chk.id).toBe(GUID); + expect(chk.name).toBe("Hello World"); + expect(chk.description).toBe("my desc"); + // should ensure unique slug + expect(slugSpy.calls.count()).toBe(1); + expect(slugSpy.calls.argsFor(0)[0]).toEqual( + { slug: "dcdev|hello-world" }, + "should recieve slug" + ); + // should create the item + expect(createSpy.calls.count()).toBe(1); + const modelToCreate = createSpy.calls.argsFor(0)[0]; + expect(modelToCreate.item.properties.slug).toBe("dcdev|hello-world"); + expect(modelToCreate.item.properties.orgUrlKey).toBe("dcdev"); + }); + }); + + describe("updatePage: ", () => { + it("updates backing model", async () => { + const slugSpy = spyOn(slugUtils, "getUniqueSlug").and.returnValue( + Promise.resolve("dcdev-wat-blarg-1") + ); + const getModelSpy = spyOn(modelUtils, "getModel").and.returnValue( + Promise.resolve(PAGE_MODEL) + ); + const updateModelSpy = spyOn(modelUtils, "updateModel").and.callFake( + (m: IModel) => { + return Promise.resolve(m); + } + ); + const page: IHubPage = { + itemControl: "edit", + id: GUID, + name: "Hello World", + tags: ["Transportation"], + description: "Some longer description", + slug: "dcdev-wat-blarg", + orgUrlKey: "dcdev", + owner: "dcdev_dude", + type: "Hub Page", + createdDate: new Date(1595878748000), + createdDateSource: "item.created", + updatedDate: new Date(1595878750000), + updatedDateSource: "item.modified", + thumbnailUrl: "", + permissions: [], + schemaVersion: 1, + canEdit: false, + canDelete: false, + typeKeywords: [], + }; + const chk = await updatePage(page, { authentication: MOCK_AUTH }); + expect(chk.id).toBe(GUID); + expect(chk.name).toBe("Hello World"); + expect(chk.description).toBe("Some longer description"); + // should ensure unique slug + expect(slugSpy.calls.count()).toBe(1); + expect(slugSpy.calls.argsFor(0)[0]).toEqual( + { slug: "dcdev-wat-blarg", existingId: GUID }, + "should recieve slug" + ); + expect(getModelSpy.calls.count()).toBe(1); + expect(updateModelSpy.calls.count()).toBe(1); + const modelToUpdate = updateModelSpy.calls.argsFor(0)[0]; + expect(modelToUpdate.item.description).toBe(page.description); + expect(modelToUpdate.item.properties.slug).toBe("dcdev-wat-blarg-1"); + }); + }); + + describe("deletePage:", () => { + it("deletes the item", async () => { + const removeSpy = spyOn(portalModule, "removeItem").and.returnValue( + Promise.resolve({ success: true }) + ); + + const result = await deletePage("3ef", { + authentication: MOCK_AUTH, + }); + expect(result).toBeUndefined(); + expect(removeSpy.calls.count()).toBe(1); + expect(removeSpy.calls.argsFor(0)[0].authentication).toBe(MOCK_AUTH); + expect(removeSpy.calls.argsFor(0)[0].id).toBe("3ef"); + }); + }); + + describe("fetchPage:", () => { + it("gets by id, if passed a guid", async () => { + const getItemSpy = spyOn(portalModule, "getItem").and.returnValue( + Promise.resolve(PAGE_ITEM) + ); + const getItemDataSpy = spyOn(portalModule, "getItemData").and.returnValue( + Promise.resolve(PAGE_DATA) + ); + + const chk = await fetchPage(GUID, { + authentication: MOCK_AUTH, + }); + expect(chk.id).toBe(GUID); + expect(chk.owner).toBe("vader"); + expect(getItemSpy.calls.count()).toBe(1); + expect(getItemSpy.calls.argsFor(0)[0]).toBe(GUID); + expect(getItemDataSpy.calls.count()).toBe(1); + expect(getItemDataSpy.calls.argsFor(0)[0]).toBe(GUID); + }); + + it("gets without auth", async () => { + const getItemSpy = spyOn(portalModule, "getItem").and.returnValue( + Promise.resolve(PAGE_ITEM) + ); + const getItemDataSpy = spyOn(portalModule, "getItemData").and.returnValue( + Promise.resolve(PAGE_DATA) + ); + const ro: IRequestOptions = { + portal: "https://gis.myserver.com/portal/sharing/rest", + }; + const chk = await fetchPage(GUID, ro); + expect(chk.id).toBe(GUID); + expect(chk.owner).toBe("vader"); + expect(chk.thumbnailUrl).toBe( + "https://gis.myserver.com/portal/sharing/rest/content/items/f995804e9e0e42cc84187258de0b710d/info/thumbnail/foo.png" + ); + expect(getItemSpy.calls.count()).toBe(1); + expect(getItemSpy.calls.argsFor(0)[0]).toBe(GUID); + expect(getItemDataSpy.calls.count()).toBe(1); + expect(getItemDataSpy.calls.argsFor(0)[0]).toBe(GUID); + }); + + it("gets by slug if not passed guid", async () => { + const getItemBySlugSpy = spyOn( + slugUtils, + "getItemBySlug" + ).and.returnValue(Promise.resolve(PAGE_ITEM)); + const getItemDataSpy = spyOn(portalModule, "getItemData").and.returnValue( + Promise.resolve(PAGE_DATA) + ); + + const chk = await fetchPage("dcdev-34th-street", { + authentication: MOCK_AUTH, + }); + expect(getItemBySlugSpy.calls.count()).toBe(1); + expect(getItemBySlugSpy.calls.argsFor(0)[0]).toBe("dcdev-34th-street"); + expect(getItemDataSpy.calls.count()).toBe(1); + expect(getItemDataSpy.calls.argsFor(0)[0]).toBe(GUID); + expect(chk.id).toBe(GUID); + expect(chk.owner).toBe("vader"); + }); + + it("returns null if no id found", async () => { + const getItemBySlugSpy = spyOn( + slugUtils, + "getItemBySlug" + ).and.returnValue(Promise.resolve(null)); + + const chk = await fetchPage("dcdev-34th-street", { + authentication: MOCK_AUTH, + }); + expect(getItemBySlugSpy.calls.count()).toBe(1); + expect(getItemBySlugSpy.calls.argsFor(0)[0]).toBe("dcdev-34th-street"); + // This next stuff is O_o but req'd by typescript + expect(chk).toEqual(null as unknown as IHubPage); + }); + }); + describe("enrichments:", () => { let enrichmentSpy: jasmine.Spy; let hubRo: IHubRequestOptions; diff --git a/packages/common/test/pages/_internal/computeProps.test.ts b/packages/common/test/pages/_internal/computeProps.test.ts new file mode 100644 index 00000000000..6c136901642 --- /dev/null +++ b/packages/common/test/pages/_internal/computeProps.test.ts @@ -0,0 +1,107 @@ +import { IPortal, IUser } from "@esri/arcgis-rest-portal"; +import { MOCK_AUTH } from "../../mocks/mock-auth"; +import { ArcGISContextManager } from "../../../src/ArcGISContextManager"; +import { computeProps } from "../../../src/pages/_internal/computeProps"; +import { IHubPage, IModel } from "../../../src"; +import { PageDefaultCapabilities } from "../../../src/pages/_internal/PageBusinessRules"; +import * as processEntitiesModule from "../../../src/capabilities"; +describe("Pages: computeProps:", () => { + let authdCtxMgr: ArcGISContextManager; + beforeEach(async () => { + // When we pass in all this information, the context + // manager will not try to fetch anything, so no need + // to mock those calls + authdCtxMgr = await ArcGISContextManager.create({ + authentication: MOCK_AUTH, + currentUser: { + username: "casey", + privileges: ["portal:user:createItem"], + } as unknown as IUser, + portal: { + name: "DC R&D Center", + id: "BRXFAKE", + urlKey: "fake-org", + properties: { + hub: { + enabled: true, + }, + }, + } as unknown as IPortal, + portalUrl: "https://org.maps.arcgis.com", + }); + }); + describe("capabilities:", () => { + it("handles missing settings hash", () => { + const spy = spyOn( + processEntitiesModule, + "processEntityCapabilities" + ).and.returnValue({ details: true, settings: false }); + const model: IModel = { + item: { + created: new Date().getTime(), + modified: new Date().getTime(), + }, + data: {}, + } as IModel; + const init: Partial = {}; + const chk = computeProps(model, init, authdCtxMgr.context.requestOptions); + expect(spy).toHaveBeenCalledTimes(1); + expect(chk.capabilities?.details).toBeTruthy(); + expect(chk.capabilities?.settings).toBeFalsy(); + expect(spy).toHaveBeenCalledWith({}, PageDefaultCapabilities); + }); + it("handles missing capabilities hash", () => { + const spy = spyOn( + processEntitiesModule, + "processEntityCapabilities" + ).and.returnValue({ details: true, settings: false }); + const model: IModel = { + item: { + id: "3ef", + type: "Hub Project", + created: new Date().getTime(), + modified: new Date().getTime(), + }, + data: { + settings: {}, + }, + } as unknown as IModel; + const init: Partial = {}; + const chk = computeProps(model, init, authdCtxMgr.context.requestOptions); + expect(spy).toHaveBeenCalledTimes(1); + expect(chk.capabilities?.details).toBeTruthy(); + expect(chk.capabilities?.settings).toBeFalsy(); + expect(spy).toHaveBeenCalledWith({}, PageDefaultCapabilities); + }); + it("passes capabilities hash", () => { + const spy = spyOn( + processEntitiesModule, + "processEntityCapabilities" + ).and.returnValue({ details: true, settings: false }); + const model: IModel = { + item: { + id: "3ef", + type: "Hub Page", + created: new Date().getTime(), + modified: new Date().getTime(), + }, + data: { + settings: { + capabilities: { + details: true, + }, + }, + }, + } as unknown as IModel; + const init: Partial = {}; + const chk = computeProps(model, init, authdCtxMgr.context.requestOptions); + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith( + model.data?.settings?.capabilities, + PageDefaultCapabilities + ); + expect(chk.capabilities?.details).toBeTruthy(); + expect(chk.capabilities?.settings).toBeFalsy(); + }); + }); +}); From a3adffcffa2d62102d7a3620585106c55bf43624 Mon Sep 17 00:00:00 2001 From: Michael Juniper Date: Mon, 17 Jul 2023 14:30:34 -0600 Subject: [PATCH 6/6] feat: hubpage entity --- packages/common/src/pages/HubPage.ts | 2 +- packages/common/src/pages/HubPages.ts | 28 +++++++++++++-------------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/packages/common/src/pages/HubPage.ts b/packages/common/src/pages/HubPage.ts index 351315e8012..dd301117b3f 100644 --- a/packages/common/src/pages/HubPage.ts +++ b/packages/common/src/pages/HubPage.ts @@ -59,7 +59,7 @@ export class HubPage /** * - * NOT IMPLEMENTED YET: Create a new HubPage, returning a HubPage instance. + * Create a new HubPage, returning a HubPage instance. * By default, this does not save the page to the backing store. * @param partialPage * @param context diff --git a/packages/common/src/pages/HubPages.ts b/packages/common/src/pages/HubPages.ts index a166ca97259..6f58cdaf006 100644 --- a/packages/common/src/pages/HubPages.ts +++ b/packages/common/src/pages/HubPages.ts @@ -38,11 +38,11 @@ import { /** * @private - * Create a new Hub Project item + * Create a new Hub Page item * * Minimal properties are name and org * - * @param project + * @param partialPage * @param requestOptions */ export async function createPage( @@ -62,7 +62,7 @@ export async function createPage( // add slug and status to keywords page.typeKeywords = setSlugKeyword(page.typeKeywords, page.slug); - // Map project object onto a default project Model + // Map page object onto a default page Model const mapper = new PropertyMapper, IModel>( getPropertyMap() ); @@ -70,16 +70,16 @@ export async function createPage( let model = mapper.entityToStore(page, cloneObject(DEFAULT_PAGE_MODEL)); // create the item model = await createModel(model, requestOptions); - // map the model back into a IHubProject - let newProject = mapper.storeToEntity(model, {}); - newProject = computeProps(model, newProject, requestOptions); + // map the model back into a IHubPage + let newPage = mapper.storeToEntity(model, {}); + newPage = computeProps(model, newPage, requestOptions); // and return it - return newProject as IHubPage; + return newPage as IHubPage; } /** * @private - * Update a Hub Project + * Update a Hub Page * @param page * @param requestOptions */ @@ -87,7 +87,7 @@ export async function updatePage( page: IHubPage, requestOptions: IUserRequestOptions ): Promise { - // verify that the slug is unique, excluding the current project + // verify that the slug is unique, excluding the current page page.slug = await getUniqueSlug( { slug: page.slug, existingId: page.id }, requestOptions @@ -105,12 +105,12 @@ export async function updatePage( const modelToUpdate = mapper.entityToStore(page, model); // update the backing item const updatedModel = await updateModel(modelToUpdate, requestOptions); - // now map back into a project and return that - let updatedProject = mapper.storeToEntity(updatedModel, page); - updatedProject = computeProps(model, updatedProject, requestOptions); + // now map back into a page and return that + let updatedPage = mapper.storeToEntity(updatedModel, page); + updatedPage = computeProps(model, updatedPage, requestOptions); // the casting is needed because modelToObject returns a `Partial` // where as this function returns a `T` - return updatedProject as IHubPage; + return updatedPage as IHubPage; } /** @@ -138,7 +138,7 @@ export async function fetchPage( /** * @private - * Convert an Hub Page Item into a Hub Project, fetching any additional + * Convert an Hub Page Item into a Hub Page, fetching any additional * information that may be required * @param item * @param auth